def test_on_enter_on_exit(self): enter_transitions = [] exit_transitions = [] def on_exit(state, event): exit_transitions.append((state, event)) def on_enter(state, event): enter_transitions.append((state, event)) m = fsm.FSM('start') m.add_state('start', on_exit=on_exit) m.add_state('down', on_enter=on_enter, on_exit=on_exit) m.add_state('up', on_enter=on_enter, on_exit=on_exit) m.add_transition('start', 'down', 'beat') m.add_transition('down', 'up', 'jump') m.add_transition('up', 'down', 'fall') m.initialize() m.process_event('beat') m.process_event('jump') m.process_event('fall') self.assertEqual([('down', 'beat'), ('up', 'jump'), ('down', 'fall')], enter_transitions) self.assertEqual([('start', 'beat'), ('down', 'jump'), ('up', 'fall')], exit_transitions)
def test_copy_states(self): c = fsm.FSM('down') self.assertEqual(0, len(c.states)) d = c.copy() c.add_state('up') c.add_state('down') self.assertEqual(2, len(c.states)) self.assertEqual(0, len(d.states))
def test_bad_transition(self): m = fsm.FSM('unknown') m.add_state('unknown') m.add_state('fire') self.assertRaises(excp.NotFound, m.add_transition, 'unknown', 'something', 'boom') self.assertRaises(excp.NotFound, m.add_transition, 'something', 'unknown', 'boom')
def setUp(self): super(FSMTest, self).setUp() # NOTE(harlowja): this state machine will never stop if run() is used. self.jumper = fsm.FSM("down") self.jumper.add_state('up') self.jumper.add_state('down') self.jumper.add_transition('down', 'up', 'jump') self.jumper.add_transition('up', 'down', 'fall') self.jumper.add_reaction('up', 'jump', lambda *args: 'fall') self.jumper.add_reaction('down', 'fall', lambda *args: 'jump')
def make_machine(start_state, transitions, disallowed): machine = fsm.FSM(start_state) machine.add_state(start_state) for (start_state, end_state) in transitions: if start_state in disallowed or end_state in disallowed: continue if start_state not in machine: machine.add_state(start_state) if end_state not in machine: machine.add_state(end_state) # Make a fake event (not used anyway)... event = "on_%s" % (end_state) machine.add_transition(start_state, end_state, event.lower()) return machine
def test_copy_reactions(self): c = fsm.FSM('down') d = c.copy() c.add_state('down') c.add_state('up') c.add_reaction('down', 'jump', lambda *args: 'up') c.add_transition('down', 'up', 'jump') self.assertEqual(1, c.events) self.assertEqual(0, d.events) self.assertNotIn('down', d) self.assertNotIn('up', d) self.assertEqual([], list(d)) self.assertEqual([('down', 'jump', 'up')], list(c))
def test_run(self): m = fsm.FSM('down') m.add_state('down') m.add_state('up') m.add_state('broken', terminal=True) m.add_transition('down', 'up', 'jump') m.add_transition('up', 'broken', 'hit-wall') m.add_reaction('up', 'jump', lambda *args: 'hit-wall') self.assertEqual(['broken', 'down', 'up'], sorted(m.states)) self.assertEqual(2, m.events) m.initialize() self.assertEqual('down', m.current_state) self.assertFalse(m.terminated) m.run('jump') self.assertTrue(m.terminated) self.assertEqual('broken', m.current_state) self.assertRaises(excp.InvalidState, m.run, 'jump', initialize=False)
def test_invalid_callbacks(self): m = fsm.FSM('working') m.add_state('working') m.add_state('broken') self.assertRaises(ValueError, m.add_state, 'b', on_enter=2) self.assertRaises(ValueError, m.add_state, 'b', on_exit=2)
def test_bad_reaction(self): m = fsm.FSM('unknown') m.add_state('unknown') self.assertRaises(excp.NotFound, m.add_reaction, 'something', 'boom', lambda *args: 'cough')
def test_duplicate_state(self): m = fsm.FSM('unknown') m.add_state('unknown') self.assertRaises(excp.Duplicate, m.add_state, 'unknown')
def test_contains(self): m = fsm.FSM('unknown') self.assertNotIn('unknown', m) m.add_state('unknown') self.assertIn('unknown', m)
def test_bad_start_state(self): m = fsm.FSM('unknown') self.assertRaises(excp.NotFound, m.run, 'unknown')
def build(self, timeout=None): memory = _MachineMemory() if timeout is None: timeout = _WAITING_TIMEOUT def resume(old_state, new_state, event): memory.next_nodes.update(self._completer.resume()) memory.next_nodes.update(self._analyzer.get_next_nodes()) return 'schedule' def game_over(old_state, new_state, event): if memory.failures: return 'failed' if self._analyzer.get_next_nodes(): return 'suspended' elif self._analyzer.is_success(): return 'success' else: return 'reverted' def schedule(old_state, new_state, event): if self.runnable() and memory.next_nodes: not_done, failures = self._scheduler.schedule( memory.next_nodes) if not_done: memory.not_done.update(not_done) if failures: memory.failures.extend(failures) memory.next_nodes.clear() return 'wait' def wait(old_state, new_state, event): # TODO(harlowja): maybe we should start doing 'yield from' this # call sometime in the future, or equivalent that will work in # py2 and py3. if memory.not_done: done, not_done = self._waiter.wait_for_any( memory.not_done, timeout) memory.done.update(done) memory.not_done = not_done return 'analyze' def analyze(old_state, new_state, event): next_nodes = set() while memory.done: fut = memory.done.pop() try: node, event, result = fut.result() retain = self._completer.complete(node, event, result) if retain and isinstance(result, misc.Failure): memory.failures.append(result) except Exception: memory.failures.append(misc.Failure()) else: try: more_nodes = self._analyzer.get_next_nodes(node) except Exception: memory.failures.append(misc.Failure()) else: next_nodes.update(more_nodes) if self.runnable() and next_nodes and not memory.failures: memory.next_nodes.update(next_nodes) return 'schedule' elif memory.not_done: return 'wait' else: return 'finished' def on_exit(old_state, event): LOG.debug("Exiting old state '%s' in response to event '%s'", old_state, event) def on_enter(new_state, event): LOG.debug("Entering new state '%s' in response to event '%s'", new_state, event) # NOTE(harlowja): when ran in debugging mode it is quite useful # to track the various state transitions as they happen... watchers = {} if LOG.isEnabledFor(logging.DEBUG): watchers['on_exit'] = on_exit watchers['on_enter'] = on_enter m = fsm.FSM(_UNDEFINED) m.add_state(_GAME_OVER, **watchers) m.add_state(_UNDEFINED, **watchers) m.add_state(st.ANALYZING, **watchers) m.add_state(st.RESUMING, **watchers) m.add_state(st.REVERTED, terminal=True, **watchers) m.add_state(st.SCHEDULING, **watchers) m.add_state(st.SUCCESS, terminal=True, **watchers) m.add_state(st.SUSPENDED, terminal=True, **watchers) m.add_state(st.WAITING, **watchers) m.add_state(st.FAILURE, terminal=True, **watchers) m.add_transition(_GAME_OVER, st.REVERTED, 'reverted') m.add_transition(_GAME_OVER, st.SUCCESS, 'success') m.add_transition(_GAME_OVER, st.SUSPENDED, 'suspended') m.add_transition(_GAME_OVER, st.FAILURE, 'failed') m.add_transition(_UNDEFINED, st.RESUMING, 'start') m.add_transition(st.ANALYZING, _GAME_OVER, 'finished') m.add_transition(st.ANALYZING, st.SCHEDULING, 'schedule') m.add_transition(st.ANALYZING, st.WAITING, 'wait') m.add_transition(st.RESUMING, st.SCHEDULING, 'schedule') m.add_transition(st.SCHEDULING, st.WAITING, 'wait') m.add_transition(st.WAITING, st.ANALYZING, 'analyze') m.add_reaction(_GAME_OVER, 'finished', game_over) m.add_reaction(st.ANALYZING, 'analyze', analyze) m.add_reaction(st.RESUMING, 'start', resume) m.add_reaction(st.SCHEDULING, 'schedule', schedule) m.add_reaction(st.WAITING, 'wait', wait) m.freeze() return (m, memory)
def build(self, timeout=None): memory = _MachineMemory() if timeout is None: timeout = _WAITING_TIMEOUT def resume(old_state, new_state, event): # This reaction function just updates the state machines memory # to include any nodes that need to be executed (from a previous # attempt, which may be empty if never ran before) and any nodes # that are now ready to be ran. memory.next_nodes.update(self._completer.resume()) memory.next_nodes.update(self._analyzer.get_next_nodes()) return _SCHEDULE def game_over(old_state, new_state, event): # This reaction function is mainly a intermediary delegation # function that analyzes the current memory and transitions to # the appropriate handler that will deal with the memory values, # it is *always* called before the final state is entered. if memory.failures: return _FAILED if self._analyzer.get_next_nodes(): return _SUSPENDED elif self._analyzer.is_success(): return _SUCCESS else: return _REVERTED def schedule(old_state, new_state, event): # This reaction function starts to schedule the memory's next # nodes (iff the engine is still runnable, which it may not be # if the user of this engine has requested the engine/storage # that holds this information to stop or suspend); handles failures # that occur during this process safely... if self.runnable() and memory.next_nodes: not_done, failures = self._scheduler.schedule( memory.next_nodes) if not_done: memory.not_done.update(not_done) if failures: memory.failures.extend(failures) memory.next_nodes.clear() return _WAIT def wait(old_state, new_state, event): # TODO(harlowja): maybe we should start doing 'yield from' this # call sometime in the future, or equivalent that will work in # py2 and py3. if memory.not_done: done, not_done = self._waiter.wait_for_any( memory.not_done, timeout) memory.done.update(done) memory.not_done = not_done return _ANALYZE def analyze(old_state, new_state, event): # This reaction function is responsible for analyzing all nodes # that have finished executing and completing them and figuring # out what nodes are now ready to be ran (and then triggering those # nodes to be scheduled in the future); handles failures that # occur during this process safely... next_nodes = set() while memory.done: fut = memory.done.pop() node = fut.atom try: event, result = fut.result() retain = self._completer.complete(node, event, result) if isinstance(result, failure.Failure): if retain: memory.failures.append(result) else: # NOTE(harlowja): avoid making any # intention request to storage unless we are # sure we are in DEBUG enabled logging (otherwise # we will call this all the time even when DEBUG # is not enabled, which would suck...) if LOG.isEnabledFor(logging.DEBUG): intention = self._storage.get_atom_intention( node.name) LOG.debug( "Discarding failure '%s' (in" " response to event '%s') under" " completion units request during" " completion of node '%s' (intention" " is to %s)", result, event, node, intention) except Exception: memory.failures.append(failure.Failure()) else: try: more_nodes = self._analyzer.get_next_nodes(node) except Exception: memory.failures.append(failure.Failure()) else: next_nodes.update(more_nodes) if self.runnable() and next_nodes and not memory.failures: memory.next_nodes.update(next_nodes) return _SCHEDULE elif memory.not_done: return _WAIT else: return _FINISH def on_exit(old_state, event): LOG.debug("Exiting old state '%s' in response to event '%s'", old_state, event) def on_enter(new_state, event): LOG.debug("Entering new state '%s' in response to event '%s'", new_state, event) # NOTE(harlowja): when ran in debugging mode it is quite useful # to track the various state transitions as they happen... watchers = {} if LOG.isEnabledFor(logging.DEBUG): watchers['on_exit'] = on_exit watchers['on_enter'] = on_enter m = fsm.FSM(_UNDEFINED) m.add_state(_GAME_OVER, **watchers) m.add_state(_UNDEFINED, **watchers) m.add_state(st.ANALYZING, **watchers) m.add_state(st.RESUMING, **watchers) m.add_state(st.REVERTED, terminal=True, **watchers) m.add_state(st.SCHEDULING, **watchers) m.add_state(st.SUCCESS, terminal=True, **watchers) m.add_state(st.SUSPENDED, terminal=True, **watchers) m.add_state(st.WAITING, **watchers) m.add_state(st.FAILURE, terminal=True, **watchers) m.add_transition(_GAME_OVER, st.REVERTED, _REVERTED) m.add_transition(_GAME_OVER, st.SUCCESS, _SUCCESS) m.add_transition(_GAME_OVER, st.SUSPENDED, _SUSPENDED) m.add_transition(_GAME_OVER, st.FAILURE, _FAILED) m.add_transition(_UNDEFINED, st.RESUMING, _START) m.add_transition(st.ANALYZING, _GAME_OVER, _FINISH) m.add_transition(st.ANALYZING, st.SCHEDULING, _SCHEDULE) m.add_transition(st.ANALYZING, st.WAITING, _WAIT) m.add_transition(st.RESUMING, st.SCHEDULING, _SCHEDULE) m.add_transition(st.SCHEDULING, st.WAITING, _WAIT) m.add_transition(st.WAITING, st.ANALYZING, _ANALYZE) m.add_reaction(_GAME_OVER, _FINISH, game_over) m.add_reaction(st.ANALYZING, _ANALYZE, analyze) m.add_reaction(st.RESUMING, _START, resume) m.add_reaction(st.SCHEDULING, _SCHEDULE, schedule) m.add_reaction(st.WAITING, _WAIT, wait) m.freeze() return (m, memory)