def test_nextStateNotMissingIfInitial(self): """ L{MissingTransitionNextState} is not raised if a value defined by C{state} appears nowhere in C{transitions} as a next state but is given as C{initial}. """ transitions = TransitionTable() transitions = transitions.addTransition( MoreState.amber, Input.apple, [Output.aardvark], MoreState.amber) transitions = transitions.addTerminalState(MoreState.blue) constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.blue, [], {}, NULL_WORLD)
def test_nextStateNotMissingIfInitial(self): """ L{MissingTransitionNextState} is not raised if a value defined by C{state} appears nowhere in C{transitions} as a next state but is given as C{initial}. """ transitions = TransitionTable() transitions = transitions.addTransition(MoreState.amber, Input.apple, [Output.aardvark], MoreState.amber) transitions = transitions.addTerminalState(MoreState.blue) constructFiniteStateMachine(Input, Output, MoreState, transitions, MoreState.blue, [], {}, NULL_WORLD)
def __init__(self): self._fsm = constructFiniteStateMachine( inputs=Input, outputs=Output, states=State, table=table, initial=State.IDLE, richInputs=[RequestStart, InstanceStarted, StartFailed, RequestStop, InstanceStopped, StopFailed], inputContext={}, world=MethodSuffixOutputer(self))
def test_noRepeatedTerminalLogging(self, logger): """ When the L{IFiniteStateMachine} receives an input in a terminal state (and does not generate an output or change to a different state, as is required in terminal states) it does not re-log the completion of its initialization event. """ # Accept this input in MoreState.blue but remain a terminal state (no # output, no state change). transitions = TRANSITIONS.addTransition(MoreState.blue, Input.apple, [], MoreState.blue) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) howMany = len(logger.messages) fsm.receive(Gravenstein()) # No additional initialization messages please! self.assertEqual([], [ msg for msg in logger.messages[howMany:] if msg[u"action_type"] == u"fsm:initialize" ])
def main(): hardware = TurnstileController(digitalPin=0x13) turnstileFSM = constructFiniteStateMachine( inputs=TurnstileInput, outputs=TurnstileOutput, states=TurnstileState, table=table, initial=TurnstileState.LOCKED, richInputs={}, inputContext={}, world=MethodSuffixOutputer(Turnstile(hardware)), ) while True: if hardware.paymentMade(): hardware.resetNotification() turnstileFSM.receive(TurnstileInput.FARE_PAID) elif hardware.armTurned(): hardware.resetNotification() turnstileFSM.receive(TurnstileInput.ARM_TURNED) elif hardware.finishedLocking(): hardware.resetNotification() turnstileFSM.receive(TurnstileInput.ARM_LOCKED) elif hardware.finishedUnlocking(): hardware.resetNotification() turnstileFSM.receive(TurnstileInput.ARM_UNLOCKED) else: time.sleep(0.1)
def build_convergence_loop_fsm(reactor, deployer): """ Create a convergence loop FSM. :param IReactorTime reactor: Used to schedule delays in the loop. :param IDeployer deployer: Used to discover local state and calcualte necessary changes to match desired configuration. """ I = ConvergenceLoopInputs O = ConvergenceLoopOutputs S = ConvergenceLoopStates table = TransitionTable() table = table.addTransition( S.STOPPED, I.STATUS_UPDATE, [O.STORE_INFO, O.CONVERGE], S.CONVERGING) table = table.addTransitions( S.CONVERGING, { I.STATUS_UPDATE: ([O.STORE_INFO], S.CONVERGING), I.STOP: ([], S.CONVERGING_STOPPING), I.ITERATION_DONE: ([O.CONVERGE], S.CONVERGING), }) table = table.addTransitions( S.CONVERGING_STOPPING, { I.STATUS_UPDATE: ([O.STORE_INFO], S.CONVERGING), I.ITERATION_DONE: ([], S.STOPPED), }) loop = ConvergenceLoop(reactor, deployer) fsm = constructFiniteStateMachine( inputs=I, outputs=O, states=S, initial=S.STOPPED, table=table, richInputs=[_ClientStatusUpdate], inputContext={}, world=MethodSuffixOutputer(loop)) loop.fsm = fsm return fsm
def setup_driver(): """ Instantiate all of the driver components includes the following objects: trace, dbus, radio, state machine actions, finite state machine. Returns list of [fsm, radio, dbus] object references. """ trace = si446xtrace.Trace(1000) dbus = Si446xDbus(OBJECT_PATH, trace=trace) radio = Si446xRadio(device=0, callback=dbus.async_interrupt, trace=trace) print('init radio done') actions = Si446xFsmActionHandlers(radio, dbus) machine = constructFiniteStateMachine( inputs=Events, outputs=Actions, states=States, table=table, initial=States.S_SDN, richInputs=[], inputContext={}, world=MethodSuffixOutputer(actions), ) fsm = {'actions': actions, 'machine': machine, 'trace': trace} dbus.marry(fsm, radio) return [fsm, radio, dbus]
def test_noRepeatedTerminalLogging(self, logger): """ When the L{IFiniteStateMachine} receives an input in a terminal state (and does not generate an output or change to a different state, as is required in terminal states) it does not re-log the completion of its initialization event. """ # Accept this input in MoreState.blue but remain a terminal state (no # output, no state change). transitions = TRANSITIONS.addTransition( MoreState.blue, Input.apple, [], MoreState.blue) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) howMany = len(logger.messages) fsm.receive(Gravenstein()) # No additional initialization messages please! self.assertEqual([], [ msg for msg in logger.messages[howMany:] if msg[u"action_type"] == u"fsm:initialize"])
def test_initializationLogging(self, logger): """ The initialization of the L{IFiniteStateMachine} created by L{constructFiniteStateMachine} is logged. """ constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) self.assertTrue( issuperset(logger.messages[0], { u"fsm_identifier": u"<AnimalWorld>", u"fsm_state": u"<MoreState=amber>", u"action_status": u"started", u"action_type": u"fsm:initialize", }))
def build_cluster_status_fsm(convergence_loop_fsm): """ Create a new cluster status FSM. The automatic reconnection logic is handled by the ``AgentLoopService``; the world object here just gets notified of disconnects, it need schedule the reconnect itself. :param convergence_loop_fsm: A convergence loop FSM as output by ``build_convergence_loop_fsm``. """ S = ClusterStatusStates I = ClusterStatusInputs O = ClusterStatusOutputs table = TransitionTable() # We may be shut down in any state, in which case we disconnect if # necessary. table = table.addTransitions( S.DISCONNECTED, { # Store the client, then wait for cluster status to be sent # over AMP: I.CONNECTED_TO_CONTROL_SERVICE: ([O.STORE_CLIENT], S.IGNORANT), I.SHUTDOWN: ([], S.SHUTDOWN), }, ) table = table.addTransitions( S.IGNORANT, { # We never told agent to start, so no need to tell it to stop: I.DISCONNECTED_FROM_CONTROL_SERVICE: ([], S.DISCONNECTED), # Tell agent latest cluster status, implicitly starting it: I.STATUS_UPDATE: ([O.UPDATE_STATUS], S.KNOWLEDGEABLE), I.SHUTDOWN: ([O.DISCONNECT], S.SHUTDOWN), }, ) table = table.addTransitions( S.KNOWLEDGEABLE, { # Tell agent latest cluster status: I.STATUS_UPDATE: ([O.UPDATE_STATUS], S.KNOWLEDGEABLE), I.DISCONNECTED_FROM_CONTROL_SERVICE: ([O.STOP], S.DISCONNECTED), I.SHUTDOWN: ([O.STOP, O.DISCONNECT], S.SHUTDOWN), }, ) table = table.addTransitions( S.SHUTDOWN, {I.DISCONNECTED_FROM_CONTROL_SERVICE: ([], S.SHUTDOWN), I.STATUS_UPDATE: ([], S.SHUTDOWN)} ) return constructFiniteStateMachine( inputs=I, outputs=O, states=S, initial=S.DISCONNECTED, table=table, richInputs=[_ConnectedToControlService, _StatusUpdate], inputContext={}, world=MethodSuffixOutputer(ClusterStatus(convergence_loop_fsm)), )
def setUp(self): self.animals = [] self.initial = MoreState.amber self.world = AnimalWorld(self.animals) self.fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, self.initial, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(self.world))
def build_cluster_status_fsm(convergence_loop_fsm): """ Create a new cluster status FSM. The automatic reconnection logic is handled by the ``AgentLoopService``; the world object here just gets notified of disconnects, it need schedule the reconnect itself. :param convergence_loop_fsm: A convergence loop FSM as output by ``build_convergence_loop_fsm``. """ S = ClusterStatusStates I = ClusterStatusInputs O = ClusterStatusOutputs table = TransitionTable() # We may be shut down in any state, in which case we disconnect if # necessary. table = table.addTransitions( S.DISCONNECTED, { # Store the client, then wait for cluster status to be sent # over AMP: I.CONNECTED_TO_CONTROL_SERVICE: ([O.STORE_CLIENT], S.IGNORANT), I.SHUTDOWN: ([], S.SHUTDOWN), }) table = table.addTransitions( S.IGNORANT, { # We never told agent to start, so no need to tell it to stop: I.DISCONNECTED_FROM_CONTROL_SERVICE: ([], S.DISCONNECTED), # Tell agent latest cluster status, implicitly starting it: I.STATUS_UPDATE: ([O.UPDATE_STATUS], S.KNOWLEDGEABLE), I.SHUTDOWN: ([O.DISCONNECT], S.SHUTDOWN), }) table = table.addTransitions( S.KNOWLEDGEABLE, { # Tell agent latest cluster status: I.STATUS_UPDATE: ([O.UPDATE_STATUS], S.KNOWLEDGEABLE), I.DISCONNECTED_FROM_CONTROL_SERVICE: ([O.STOP], S.DISCONNECTED), I.SHUTDOWN: ([O.STOP, O.DISCONNECT], S.SHUTDOWN), }) table = table.addTransitions( S.SHUTDOWN, { I.DISCONNECTED_FROM_CONTROL_SERVICE: ([], S.SHUTDOWN), I.STATUS_UPDATE: ([], S.SHUTDOWN), }) return constructFiniteStateMachine( inputs=I, outputs=O, states=S, initial=S.DISCONNECTED, table=table, richInputs=[_ConnectedToControlService, _StatusUpdate], inputContext={}, world=MethodSuffixOutputer(ClusterStatus(convergence_loop_fsm)))
def test_initializationLogging(self, logger): """ The initialization of the L{IFiniteStateMachine} created by L{constructFiniteStateMachine} is logged. """ constructFiniteStateMachine(Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) self.assertTrue( issuperset( logger.messages[0], { u"fsm_identifier": u"<AnimalWorld>", u"fsm_state": u"<MoreState=amber>", u"action_status": u"started", u"action_type": u"fsm:initialize", }))
def test_empty(self): """ L{_FiniteStateMachine._isTerminal} returns C{True} if a state that defines handling of no input symbols. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertTrue(fsm._isTerminal(MoreState.blue))
def test_logger(self): """ L{constructFiniteStateMachine} returns a FSM that also has a C{logger} attribute that is an L{eliot.Logger} instance. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertIsInstance(fsm.logger, Logger)
def test_loggerOverride(self, logger): """ If an argument is given for the C{logger} argument to L{constructFiniteStateMachine} then that object is used as the logger of the resulting finite state machine. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) self.assertIs(logger, fsm.logger)
def test_stateChange(self): """ L{_FiniteStateMachine._isTerminal} returns C{False} if a state defines handling of inputs that cause a state change. """ transitions = TRANSITIONS.addTransition( MoreState.blue, Input.apple, [], MoreState.amber) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertFalse(fsm._isTerminal(MoreState.blue))
def test_nextStateGivenSymbolInput(self, logger): """ L{IFiniteStateMachine.receive} changes L{IFiniteStateMachine.state} to the next state defined for the given symbolic input in the machine's current state. """ self.fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {}, MethodSuffixOutputer(AnimalWorld([])), logger) self.fsm.logger = logger self.fsm.receive(Input.apple) self.assertEqual(MoreState.blue, self.fsm.state)
def test_stateChange(self): """ L{_FiniteStateMachine._isTerminal} returns C{False} if a state defines handling of inputs that cause a state change. """ transitions = TRANSITIONS.addTransition(MoreState.blue, Input.apple, [], MoreState.amber) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertFalse(fsm._isTerminal(MoreState.blue))
def test_selfTransition(self): """ L{_FiniteStateMachine._isTerminal} returns C{True} if a state defines handling of inputs that generate no outputs and do not change the state of the machine. """ transitions = TRANSITIONS.addTransition(MoreState.blue, Input.apple, [], MoreState.blue) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertTrue(fsm._isTerminal(MoreState.blue))
def test_selfTransition(self): """ L{_FiniteStateMachine._isTerminal} returns C{True} if a state defines handling of inputs that generate no outputs and do not change the state of the machine. """ transitions = TRANSITIONS.addTransition( MoreState.blue, Input.apple, [], MoreState.blue) fsm = constructFiniteStateMachine( Input, Output, MoreState, transitions, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([]))) self.assertTrue(fsm._isTerminal(MoreState.blue))
def test_outputFromSymbolInput(self, logger): """ L{IFiniteStateMachine.receive} finds the transition for the symbol input in the machine's current state and returns the corresponding output. """ self.fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, self.initial, [Gravenstein], {}, MethodSuffixOutputer(self.world)) self.fsm.logger = logger self.world.logger = logger self.assertEqual([Output.aardvark], self.fsm.receive(Input.apple))
def test_FiniteStateInterpreterRepr(self): """ The result of L{_FiniteStateInterpreter.__repr__} is a string that includes the L{IOutputExecutor} provider that L{_FiniteStateInterpreter} can drive. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, self.initial, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(self.world), None) self.assertEqual( repr(fsm), "<FSM / %s>" % (MethodSuffixOutputer(self.world),))
def test_FiniteStateInterpreterRepr(self): """ The result of L{_FiniteStateInterpreter.__repr__} is a string that includes the L{IOutputExecutor} provider that L{_FiniteStateInterpreter} can drive. """ fsm = constructFiniteStateMachine(Input, Output, MoreState, TRANSITIONS, self.initial, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(self.world), None) self.assertEqual(repr(fsm), "<FSM / %s>" % (MethodSuffixOutputer(self.world), ))
def build_convergence_loop_fsm(reactor, deployer): """ Create a convergence loop FSM. Once cluster config+cluster state updates from control service are received the basic loop is: 1. Discover local state. 2. Calculate ``IStateChanges`` based on local state and cluster configuration and cluster state we received from control service. 3. Execute the change. 4. Sleep. However, if an update is received during sleep then we calculate based on that updated config+state whether a ``IStateChange`` needs to happen. If it does that means this change will have impact on what we do, so we interrupt the sleep. If calculation suggests a no-op then we keep sleeping. Notably we do **not** do a discovery of local state when an update is received while sleeping, since that is an expensive operation that can involve talking to external resources. Moreover an external update only implies external state/config changed, so we're not interested in the latest local state in trying to decide if this update requires us to do something; a recently cached version should suffice. :param IReactorTime reactor: Used to schedule delays in the loop. :param IDeployer deployer: Used to discover local state and calcualte necessary changes to match desired configuration. """ loop = ConvergenceLoop(reactor, deployer) fsm = constructFiniteStateMachine( inputs=ConvergenceLoopInputs, outputs=ConvergenceLoopOutputs, states=ConvergenceLoopStates, initial=ConvergenceLoopStates.STOPPED, table=_CONVERGENCE_LOOP_FSM_TABLE, richInputs=[_ClientStatusUpdate, _Sleep], inputContext={}, world=MethodSuffixOutputer(loop)) loop.fsm = fsm return fsm
def test_terminalLogging(self, logger): """ When the L{IFiniteStateMachine} enters a terminal state the initialization action is finished successfully. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) (initialize, ) = LoggedAction.of_type(logger.messages, LOG_FSM_INITIALIZE) assertContainsFields( self, initialize.end_message, { u"fsm_terminal_state": u"<MoreState=blue>", u"action_status": u"succeeded", })
def build_cluster_status_fsm(convergence_loop_fsm): """ Create a new cluster status FSM. The automatic reconnection logic is handled by the ``AgentLoopService``; the world object here just gets notified of disconnects, it need schedule the reconnect itself. :param convergence_loop_fsm: A convergence loop FSM as output by ``build_convergence_loop_fsm``. """ return constructFiniteStateMachine( inputs=ClusterStatusInputs, outputs=ClusterStatusOutputs, states=ClusterStatusStates, initial=ClusterStatusStates.DISCONNECTED, table=_CLUSTER_STATUS_FSM_TABLE, richInputs=[_ConnectedToControlService, _StatusUpdate], inputContext={}, world=MethodSuffixOutputer(ClusterStatus(convergence_loop_fsm)))
def test_terminalLogging(self, logger): """ When the L{IFiniteStateMachine} enters a terminal state the initialization action is finished successfully. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) self.assertTrue( issuperset(logger.messages[3], { u"fsm_terminal_state": u"<MoreState=blue>", # Prove it associates with the initialization action. u"action_type": u"fsm:initialize", u"action_status": u"succeeded", u"task_uuid": logger.messages[0][u"task_uuid"], u"task_level": u"/", }))
def test_terminalLogging(self, logger): """ When the L{IFiniteStateMachine} enters a terminal state the initialization action is finished successfully. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) (initialize,) = LoggedAction.of_type( logger.messages, LOG_FSM_INITIALIZE ) assertContainsFields( self, initialize.end_message, { u"fsm_terminal_state": u"<MoreState=blue>", u"action_status": u"succeeded", } )
def test_terminalLogging(self, logger): """ When the L{IFiniteStateMachine} enters a terminal state the initialization action is finished successfully. """ fsm = constructFiniteStateMachine( Input, Output, MoreState, TRANSITIONS, MoreState.amber, [Gravenstein], {Output.aardvark: IFood}, MethodSuffixOutputer(AnimalWorld([])), logger) fsm.receive(Gravenstein()) self.assertTrue( issuperset( logger.messages[3], { u"fsm_terminal_state": u"<MoreState=blue>", # Prove it associates with the initialization action. u"action_type": u"fsm:initialize", u"action_status": u"succeeded", u"task_uuid": logger.messages[0][u"task_uuid"], u"task_level": u"/", }))
print("Engaging the lock.") def output_DISENGAGE_LOCK(self, disengage): print("Disengaging the lock.") outputer = MethodSuffixOutputer(Outputer()) # end outputer # begin construct from machinist import constructFiniteStateMachine turnstile = constructFiniteStateMachine( inputs=Input, outputs=Output, states=State, table=table, initial=State.LOCKED, richInputs=[], inputContext={}, world=outputer, ) # end construct # begin inputs def cycle(): turnstile.receive(Input.FARE_PAID) turnstile.receive(Input.ARM_UNLOCKED) turnstile.receive(Input.ARM_TURNED) turnstile.receive(Input.ARM_LOCKED) # end inputs if __name__ == '__main__':
def output_DISENGAGE_LOCK(self, disengage): print("Disengaging the lock.") outputer = MethodSuffixOutputer(Outputer()) # end outputer # begin construct from machinist import constructFiniteStateMachine turnstile = constructFiniteStateMachine( inputs=Input, outputs=Output, states=State, table=table, initial=State.LOCKED, richInputs=[], inputContext={}, world=outputer, ) # end construct # begin inputs def cycle(): turnstile.receive(Input.FARE_PAID) turnstile.receive(Input.ARM_UNLOCKED) turnstile.receive(Input.ARM_TURNED) turnstile.receive(Input.ARM_LOCKED)