예제 #1
0
    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)
예제 #2
0
 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))
예제 #3
0
 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')
예제 #4
0
 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')
예제 #5
0
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
예제 #6
0
    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))
예제 #7
0
 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)
예제 #8
0
 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)
예제 #9
0
 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')
예제 #10
0
 def test_duplicate_state(self):
     m = fsm.FSM('unknown')
     m.add_state('unknown')
     self.assertRaises(excp.Duplicate, m.add_state, 'unknown')
예제 #11
0
 def test_contains(self):
     m = fsm.FSM('unknown')
     self.assertNotIn('unknown', m)
     m.add_state('unknown')
     self.assertIn('unknown', m)
예제 #12
0
 def test_bad_start_state(self):
     m = fsm.FSM('unknown')
     self.assertRaises(excp.NotFound, m.run, 'unknown')
예제 #13
0
    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)
예제 #14
0
    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)