package simulation.finitestatemachine; /** * GOAL: Try to create a DSL for a finite state machine. * Inspired by Chris vanBuskirk version on this site and the Ruby article at * http://www-128.ibm.com/developerworks/java/library/j-cb03137/index.html, */ class FiniteStateMachineTest extends GroovyTestCase { void testNormalProcess() { def fsm = FiniteStateMachine.newInstance('submitted') def recorder = fsm.record() recorder.on('accept') { from('processing').to('accepted') from('nonprofit_reviewing').to('accepted') } recorder.on('receive').from('submitted').to('processing') recorder.on('send_for_review') { from('processing').to('nonprofit_reviewing') from('nonprofit_reviewing').to('processing') from('accepted').to('nonprofit_reviewing') } recorder.on('revise').from('accepted').to('processing') assert 'processing' == fsm.fire('receive') assert 'accepted' == fsm.fire('accept') } void testReset() { def fsm = FiniteStateMachine.newInstance('1') def recorder = fsm.record() recorder.on('a') { from('1').to('2') from('2').to('3') } assert '2' == fsm.fire('a') fsm.reset() assert '2' == fsm.fire('a') } void testNoTransitionForEvent() { def fsm = FiniteStateMachine.newInstance('1') def recorder = fsm.record() recorder.on('a').from('1').to('2') fsm.fire('a') shouldFail { fsm.fire('a') } } void testNoInitialState() { shouldFail { FiniteStateMachine.newInstance() } } void testInvalidInitialState() { def fsm = FiniteStateMachine.newInstance('INVALID') fsm.record().on('1').from('a').to('b') shouldFail { fsm.fire('1') } } void testNoStateForEvent() { def fsm = FiniteStateMachine.newInstance('a') fsm.record().on('1').from('a').to('b') fsm.record().on('2').from('X').to('Y') fsm.fire('1') shouldFail { fsm.fire('2') } } void testDupFromState() { def fsm = FiniteStateMachine.newInstance('a') def recorder = fsm.record() recorder.on('SAME').from('DUPLICATESTATE').to('a') shouldFail { recorder.on('SAME').from('DUPLICATESTATE').to('b') } } void testInvalidToState() { def fsm = FiniteStateMachine.newInstance('a') def recorder = fsm.record() shouldFail { recorder.on('1').from('DUPLICATESTATE').to(null) } } void testIncompleteTransition() { def fsm = FiniteStateMachine.newInstance('a') def recorder = fsm.record() shouldFail { recorder.on('1').to('a') } } void testIncompleteTransitionWithClosure() { def fsm = FiniteStateMachine.newInstance('a') def recorder = fsm.record() shouldFail { recorder.on('1') { to('a') } } } } class FiniteStateMachine { /* * A map or maps to store events and from states of each * transition. */ def transitions = [:] def initialState def currentState FiniteStateMachine(a_initialState) { assert a_initialState, "You need to provide an initial state" initialState = a_initialState currentState = a_initialState } def record() { Grammer.newInstance(this); } def registerTransition(a_grammer) { assert a_grammer.isValid(), "Invalid transition (${a_grammer})" def transition def event = a_grammer.event def fromState = a_grammer.fromState def toState = a_grammer.toState if (!transitions[event]) { transitions[event] = [:] } transition = transitions[event] assert !transition[fromState], "Duplicate fromState ${fromState} for transition ${a_grammer}" transition[fromState] = toState } def reset() { currentState = initialState } def fire(a_event) { assert currentState, "Invalid current state '${currentState}': pass into constructor" assert transitions.containsKey(a_event), "Invalid event '${a_event}', should be one of ${transitions.keySet()}" def transition = transitions[a_event] def nextState = transition[currentState] assert nextState, "There is no transition from '${currentState}' to any other state" currentState = nextState currentState } def isState(a_state) { currentState == a_state } } class Grammer { def fsm def event def fromState def toState Grammer(a_fsm) { fsm = a_fsm } def on(a_event) { event = a_event this } def on(a_event, a_transitioner) { on(a_event) a_transitioner.delegate = this a_transitioner.call() this } def from(a_fromState) { fromState = a_fromState this } def to(a_toState) { assert a_toState, "Invalid toState: ${a_toState}" toState = a_toState fsm.registerTransition(this) this } def isValid() { event && fromState && toState } public String toString() { "${event}:${fromState}=>${toState}" } }