Exemplo n.º 1
0
def test_revert_to_previous_state():
    m = StateMachine('m')
    off = State('Off')
    on = State('On')
    m.add_state(off, initial=True)
    m.add_state(on)
    m.add_transition(on, off, events=['off'])
    m.add_transition(off, on, events=['on'])
    m.initialize()
    off.handlers = {
        'test_no_history': lambda s, e: m.revert_to_previous_leaf_state()
    }

    assert m.leaf_state == off
    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == []

    m.dispatch(_e('on'))
    assert m.leaf_state == on
    assert list(m.leaf_state_stack.deque) == [off]
    m.dispatch(_e('off'))
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == [off, on]

    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == on
    assert list(m.leaf_state_stack.deque) == [off, on]
Exemplo n.º 2
0
    def _get_state_machine(self):
        oven = StateMachine('Oven')
        door_closed = StateMachine('Door closed')
        door_open = State('Door open')
        heating = HeatingState('Heating')
        toasting = State('Toasting')
        baking = State('Baking')
        off = State('Off')

        oven.add_state(door_closed, initial=True)
        oven.add_state(door_open)
        door_closed.add_state(off, initial=True)
        door_closed.add_state(heating)
        heating.add_state(baking, initial=True)
        heating.add_state(toasting)

        oven.add_transition(door_closed, toasting, events=['toast'])
        oven.add_transition(door_closed, baking, events=['bake'])
        oven.add_transition(door_closed, off, events=['off', 'timeout'])
        oven.add_transition(door_closed, door_open, events=['open'])

        # This time, a state behaviour is handled by Oven's methods.
        door_open.handlers = {
            'enter': self.on_open_enter,
            'exit': self.on_open_exit,
            'close': self.on_door_close
        }

        oven.initialize()
        return oven
Exemplo n.º 3
0
def test_internal_transition():
    class Foo(object):
        def __init__(self):
            self.value = False
    foo = Foo()

    def on_enter(state, event):
        foo.value = True

    def on_exit(state, event):
        foo.value = True

    idling = State('idling')
    idling.handlers = {
        'enter': on_enter,
        'exit': on_exit,
    }

    sm = StateMachine('sm')
    sm.add_state(idling, initial=True)
    sm.add_transition(idling, None, events=['internal_transition'])
    sm.add_transition(idling, idling, events=['external_transition'])
    sm.initialize()
    sm.dispatch(_e('internal_transition'))
    assert foo.value is False
    sm.dispatch(_e('external_transition'))
    assert foo.value is True
Exemplo n.º 4
0
    def _get_state_machine(self):
        oven = StateMachine('Oven')
        door_closed = StateMachine('Door closed')
        door_open = State('Door open')
        heating = HeatingState('Heating')
        toasting = State('Toasting')
        baking = State('Baking')
        off = State('Off')

        oven.add_state(door_closed, initial=True)
        oven.add_state(door_open)
        door_closed.add_state(off, initial=True)
        door_closed.add_state(heating)
        heating.add_state(baking, initial=True)
        heating.add_state(toasting)

        oven.add_transition(door_closed, toasting, events=['toast'])
        oven.add_transition(door_closed, baking, events=['bake'])
        oven.add_transition(door_closed, off, events=['off', 'timeout'])
        oven.add_transition(door_closed, door_open, events=['open'])

        # This time, a state behaviour is handled by Oven's methods.
        door_open.handlers = {
            'enter': self.on_open_enter,
            'exit': self.on_open_exit,
            'close': self.on_door_close
        }

        oven.initialize()
        return oven
Exemplo n.º 5
0
def test_internal_transition():
    class Foo(object):
        def __init__(self):
            self.value = False

    foo = Foo()

    def on_enter(state, event):
        foo.value = True

    def on_exit(state, event):
        foo.value = True

    idling = State('idling')
    idling.handlers = {
        'enter': on_enter,
        'exit': on_exit,
    }

    sm = StateMachine('sm')
    sm.add_state(idling, initial=True)
    sm.add_transition(idling, None, events=['internal_transition'])
    sm.add_transition(idling, idling, events=['external_transition'])
    sm.initialize()
    sm.dispatch(_e('internal_transition'))
    assert foo.value is False
    sm.dispatch(_e('external_transition'))
    assert foo.value is True
Exemplo n.º 6
0
    def _get_state_machine(self):
        
        #define the states
        robot = StateMachine('Robot')
        init = State('Init')
        orient = State('Orient')
        error = State('Error')
        off = State('Off')
        
        #add states to state machine
        robot.add_state(init, initial = True)
        robot.add_state(orient)
        robot.add_state(error)
        robot.add_state(off)
        
        
        #external transitions
        robot.add_transition(init, orient, events = ['loop_timeout'],action=self.loop_timer)
        robot.add_transition(init,error, events=['timeout'])
        robot.add_transition(orient,off, events=['oriented'])
        robot.add_transition(orient,error, events=['timeout'])
        
        #attach event handlers to each state
        init.handlers = {'enter': self.on_enter,
                         'exit': self.on_exit}
        
        orient.handlers = {'enter': self.on_orient_enter,
                           'loop_timeout': self.compareHeading,
                           'exit': self.on_exit}
        
        off.handlers = {'enter': self.on_enter,
                        'loop_timeout': self.Off_wait,
                        'exit': self.on_exit}
        
        error.handlers = {'enter': self.on_enter,
                         'exit': self.on_exit}
        
#        # define the events for event checker TODO: do I need this?
#        robot.events.append('odoRx')
#        robot.events.append('oriented')
#        robot.events.append('loop_timeout')
#        robot.events.append('timeout')
        
        robot.initialize()
        return robot 
Exemplo n.º 7
0
def test_set_previous_state_no_history():
    m = StateMachine('m')
    off = State('Off')
    m.add_state(off, initial=True)
    m.initialize()
    off.handlers = {'test_no_history': lambda s, e: m.set_previous_leaf_state()}

    assert m.leaf_state == off
    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == []
Exemplo n.º 8
0
        def _get_state_machine(self):
            state_machine = StateMachine('Test')
            state_1 = State('One')
            state_2 = State('Two')

            self.state_1 = state_1
            self.state_2 = state_2

            state_machine.add_state(state_1, initial=True)
            state_machine.add_state(state_2)
            state_machine.add_transition(state_1, state_2, events=['change_state'])
            state_machine.add_transition(state_2, state_1, events=['change_state'])
            state_1.handlers = {
                        'enter': self.entry_func,
                        'exit': self.exit_func
                        }
            state_2.handlers = {
                        'enter': self.entry_func,
                        'exit': self.exit_func
                        }

            state_machine.initialize()
            return state_machine
Exemplo n.º 9
0
def test_set_previous_state_no_history():
    m = StateMachine('m')
    off = State('Off')
    m.add_state(off, initial=True)
    m.initialize()
    off.handlers = {
        'test_no_history': lambda s, e: m.set_previous_leaf_state()
    }

    assert m.leaf_state == off
    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == []
Exemplo n.º 10
0
    def _get_state_machine(self):
        oven = StateMachine(
            'Oven'
        )  # composition: Oven not isa StateMachine but contains one, linked below ((KXOQEIF))
        door_closed = StateMachine('Door closed')  # a StateMachine isa State
        door_open = State('Door open')
        heating = HeatingState()
        off = State('Off')

        oven.add_state(door_closed, initial=True)
        oven.add_state(door_open)
        door_closed.add_state(off, initial=True)
        door_closed.add_state(heating)

        # https://pysm.readthedocs.io/en/latest/pysm_module.html
        # So the order of calls on an event is as follows:
        # State’s event handler
        # condition callback
        # # before callback
        # exit handlers
        # action callback
        # enter handlers
        # after callback

        oven.add_transition(door_closed,
                            heating.toasting,
                            events=['toast'],
                            after=heating.action)
        oven.add_transition(door_closed,
                            heating.baking,
                            events=['bake'],
                            after=heating.action)
        oven.add_transition(door_closed, off, events=['off', 'timeout'])
        oven.add_transition(door_closed, door_open, events=['open'])

        # This time, a state behaviour is handled by Oven's methods.
        # ((KXOQEIF))
        door_open.handlers = {
            'enter': self.on_open_enter,
            'exit': self.on_open_exit,
            'close': self.on_door_close
        }

        # https://pysm.readthedocs.io/en/latest/pysm_module.html
        # If using nested state machines (HSM), initialize() has to be called on a root state machine in the hierarchy.
        oven.initialize()
        return oven
Exemplo n.º 11
0
def test_event_propagate_enter_exit():
    data = []

    m = StateMachine('m')
    s0 = StateMachine('s0')
    s1 = StateMachine('s1')
    s2 = StateMachine('s2')
    s3 = StateMachine('s3')
    s4 = State('s4')

    def do(state, event):
        event.cargo['source_event'].cargo['data'].append(state)
        assert state == m.leaf_state

    def do_with_propagate(state, event):
        event.cargo['source_event'].cargo['data'].append(state)
        event.propagate = True
        assert state == m.leaf_state

    m.add_state(s0, initial=True)
    s0.add_state(s1, initial=True)
    s1.add_state(s2, initial=True)
    s2.add_state(s3, initial=True)
    s3.add_state(s4, initial=True)

    m.add_transition(s0, s0, events='a')

    for state in s0, s1, s2, s3, s4:
        state.handlers = {'enter': do, 'exit': do}

    m.initialize()

    m.dispatch(_e('a', data=data))
    assert data == [s4, s3, s2, s1, s0, s0, s1, s2, s3, s4]

    data = []
    s1.handlers = {}
    s3.handlers = {}
    m.dispatch(_e('a', data=data))
    assert data == [s4, s2, s0, s0, s2, s4]

    # Never propagate exit/enter events, even if propagate is set to True
    data = []
    s4.handlers = {'enter': do_with_propagate, 'exit': do_with_propagate}
    m.dispatch(_e('a', data=data))
    assert data == [s4, s2, s0, s0, s2, s4]
Exemplo n.º 12
0
def test_transition_to_history():
    oven = StateMachine('Oven')
    door_closed = StateMachine('DoorClosed')
    door_open = State('DoorOpen')
    heating = StateMachine('Heating')
    toasting = State('Toasting')
    baking = State('Baking')
    off = State('Off')

    oven.add_state(door_closed, initial=True)
    oven.add_state(door_open)
    door_closed.add_state(off, initial=True)
    door_closed.add_state(heating)
    heating.add_state(baking, initial=True)
    heating.add_state(toasting)

    oven.add_transition(door_closed, toasting, events=['toast'])
    oven.add_transition(door_closed, baking, events=['bake'])
    oven.add_transition(door_closed, off, events=['off', 'timeout'])
    oven.add_transition(door_closed, door_open, events=['open'])

    door_open.handlers = {'close': lambda s, e: oven.set_previous_leaf_state()}

    oven.initialize()

    assert oven.leaf_state == off
    oven.dispatch(_e('open'))
    assert oven.leaf_state == door_open
    try:
        oven.dispatch(_e('close'))
    except Exception as exc:
        assert not exc
    assert oven.leaf_state == off
    assert list(oven.leaf_state_stack.deque) == [off, door_open]

    oven.dispatch(_e('bake'))
    assert oven.leaf_state == baking
    oven.dispatch(_e('open'))
    assert oven.leaf_state == door_open
    try:
        oven.dispatch(_e('close'))
    except Exception as exc:
        assert not exc
    assert oven.leaf_state == baking
    expected = [off, door_open, off, baking, door_open]
    assert list(oven.leaf_state_stack.deque) == expected
Exemplo n.º 13
0
def test_transition_to_history():
    oven = StateMachine('Oven')
    door_closed = StateMachine('DoorClosed')
    door_open = State('DoorOpen')
    heating = StateMachine('Heating')
    toasting = State('Toasting')
    baking = State('Baking')
    off = State('Off')

    oven.add_state(door_closed, initial=True)
    oven.add_state(door_open)
    door_closed.add_state(off, initial=True)
    door_closed.add_state(heating)
    heating.add_state(baking, initial=True)
    heating.add_state(toasting)

    oven.add_transition(door_closed, toasting, events=['toast'])
    oven.add_transition(door_closed, baking, events=['bake'])
    oven.add_transition(door_closed, off, events=['off', 'timeout'])
    oven.add_transition(door_closed, door_open, events=['open'])

    door_open.handlers = {'close': lambda s, e: oven.set_previous_leaf_state()}

    oven.initialize()

    assert oven.leaf_state == off
    oven.dispatch(_e('open'))
    assert oven.leaf_state == door_open
    try:
        oven.dispatch(_e('close'))
    except Exception as exc:
        assert not exc
    assert oven.leaf_state == off
    assert list(oven.leaf_state_stack.deque) == [off, door_open]

    oven.dispatch(_e('bake'))
    assert oven.leaf_state == baking
    oven.dispatch(_e('open'))
    assert oven.leaf_state == door_open
    try:
        oven.dispatch(_e('close'))
    except Exception as exc:
        assert not exc
    assert oven.leaf_state == baking
    expected = [off, door_open, off, baking, door_open]
    assert list(oven.leaf_state_stack.deque) == expected
Exemplo n.º 14
0
def test_event_propagate_enter_exit():
    data = []

    def do(state, event):
        event.cargo['source_event'].cargo['data'].append(state)

    def do_with_propagate(state, event):
        event.cargo['source_event'].cargo['data'].append(state)
        event.propagate = True

    m = StateMachine('m')
    s0 = StateMachine('s0')
    s1 = StateMachine('s1')
    s2 = StateMachine('s2')
    s3 = StateMachine('s3')
    s4 = State('s4')

    m.add_state(s0, initial=True)
    s0.add_state(s1, initial=True)
    s1.add_state(s2, initial=True)
    s2.add_state(s3, initial=True)
    s3.add_state(s4, initial=True)

    m.add_transition(s0, s0, events='a')

    for state in s0, s1, s2, s3, s4:
        state.handlers = {'enter': do, 'exit': do}

    m.initialize()

    m.dispatch(_e('a', data=data))
    assert data == [s4, s3, s2, s1, s0, s0, s1, s2, s3, s4]

    data = []
    s1.handlers = {}
    s3.handlers = {}
    m.dispatch(_e('a', data=data))
    assert data == [s4, s2, s0, s0, s2, s4]

    # Never propagate exit/enter events, even if propagate is set to True
    data = []
    s4.handlers = {'enter': do_with_propagate, 'exit': do_with_propagate}
    m.dispatch(_e('a', data=data))
    assert data == [s4, s2, s0, s0, s2, s4]
Exemplo n.º 15
0
def test_revert_to_previous_state():
    m = StateMachine('m')
    off = State('Off')
    on = State('On')
    m.add_state(off, initial=True)
    m.add_state(on)
    m.add_transition(on, off, events=['off'])
    m.add_transition(off, on, events=['on'])
    m.initialize()
    off.handlers = {
        'test_no_history': lambda s, e: m.revert_to_previous_leaf_state()
    }

    assert m.leaf_state == off
    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == []

    m.dispatch(_e('on'))
    assert m.leaf_state == on
    assert list(m.leaf_state_stack.deque) == [off]
    m.dispatch(_e('off'))
    assert m.leaf_state == off
    assert list(m.leaf_state_stack.deque) == [off, on]

    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == on
    assert list(m.leaf_state_stack.deque) == [off]

    # Nothing should change now as the "on" state doesn't handle the
    # "test_no_history"" event.
    try:
        m.dispatch(_e('test_no_history'))
    except Exception as exc:
        assert not exc
    assert m.leaf_state == on
    assert list(m.leaf_state_stack.deque) == [off]
Exemplo n.º 16
0
def test_event_propagate():
    data = []

    def do(state, event):
        event.cargo['data'].append(state)

    def do_with_propagate(state, event):
        event.cargo['data'].append(state)
        event.propagate = True

    m = StateMachine('m')
    s0 = StateMachine('s0')
    s1 = StateMachine('s1')
    s2 = StateMachine('s2')
    s3 = StateMachine('s3')
    s4 = State('s4')

    m.add_state(s0, initial=True)
    s0.add_state(s1, initial=True)
    s1.add_state(s2, initial=True)
    s2.add_state(s3, initial=True)
    s3.add_state(s4, initial=True)

    for state in s0, s1, s2, s3, s4:
        state.handlers = {'do': do}

    m.initialize()

    # No propagate by default
    m.dispatch(_e('do', data=data))
    assert data == [s4]

    # Should propagate only to the next state that can handle the event
    data = []
    s4.handlers = {'do': do_with_propagate}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s3]

    data = []
    s4.handlers = {'do': do_with_propagate}
    s3.handlers = {}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s2]

    data = []
    s4.handlers = {'do': do_with_propagate}
    s3.handlers = {'do': do}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s3]

    data = []
    s4.handlers = {}
    s3.handlers = {}
    m.dispatch(_e('do', data=data))
    assert data == [s2]

    data = []
    s4.handlers = {}
    s3.handlers = {}
    s2.handlers = {'do': do_with_propagate}
    m.dispatch(_e('do', data=data))
    assert data == [s2, s1]
Exemplo n.º 17
0
def test_new_sm():
    run_call_mock = mock.Mock()
    stop_call_mock = mock.Mock()
    idling_mock = mock.Mock()
    running_mock = mock.Mock()
    action_mock = mock.Mock()

    class Idling(State):
        # @event('run')
        def run(self, state, event):
            run_call_mock(self, event.input, event.cargo)

        def do(self, state, event):
            entity = event.cargo['entity']
            entity.do()

        def on_enter(self, state, event):
            idling_mock(self, 'on_enter')

        def on_exit(self, state, event):
            idling_mock(self, 'on_exit')

        def register_handlers(self):
            self.handlers = {
                'run': self.run,
                'do': self.do,
                'enter': self.on_enter,
                'exit': self.on_exit,
            }

    def stop(state, event):
        stop_call_mock('stopping...', event.cargo)

    def do(state, event):
        entity = event.cargo['entity']
        entity.do()

    def enter(state, event):
        running_mock('running, enter')

    def exit(state, event):
        running_mock('running, exit')

    def update(state, event):
        print('update', event)

    def do_on_transition(state, event):
        action_mock('action on transition')

    class Entity(object):
        def do(self):
            print(self, self.do)

    idling = Idling('idling')
    running = State('running')
    running.handlers = {
        'stop': stop,
        'do': do,
        'update': update,
        'enter': enter,
        'exit': exit,
    }

    entity = Entity()

    sm = StateMachine('sm')
    sm.add_state(idling, initial=True)
    sm.add_state(running)
    sm.add_transition(idling, running, events=['run'], action=do_on_transition)
    sm.add_transition(running, idling, events=['stop'])
    sm.initialize()
    assert sm.state == idling
    sm.dispatch(_e('run'))
    assert sm.state == running
    assert run_call_mock.call_count == 1
    assert run_call_mock.call_args[0] == (idling, None, {})
    assert idling_mock.call_count == 1
    assert idling_mock.call_args[0] == (idling, 'on_exit')
    assert running_mock.call_count == 1
    assert running_mock.call_args[0] == ('running, enter',)
    assert action_mock.call_count == 1
    assert action_mock.call_args[0] == ('action on transition',)

    # Nothing should happen - running state has no 'run' handler
    sm.dispatch(_e('run'))
    assert sm.state == running
    assert run_call_mock.call_count == 1
    assert run_call_mock.call_args[0] == (idling, None, {})

    sm.dispatch(_e('stop'))
    assert sm.state == idling
    assert idling_mock.call_count == 2
    assert idling_mock.call_args[0] == (idling, 'on_enter')
    assert running_mock.call_count == 2
    assert running_mock.call_args[0] == ('running, exit',)
    assert stop_call_mock.call_count == 1
    assert stop_call_mock.call_args[0] == ('stopping...', {})

    # Unknown events must be ignored
    sm.dispatch(_e('blah'))
    sm.dispatch(_e('blah blah'))
    assert sm.state == idling
Exemplo n.º 18
0
def test_new_sm():
    run_call_mock = mock.Mock()
    stop_call_mock = mock.Mock()
    idling_mock = mock.Mock()
    running_mock = mock.Mock()
    action_mock = mock.Mock()

    class Idling(State):
        # @event('run')
        def run(self, state, event):
            run_call_mock(self, event.input, event.cargo)

        def do(self, state, event):
            entity = event.cargo['entity']
            entity.do()

        def on_enter(self, state, event):
            idling_mock(self, 'on_enter')

        def on_exit(self, state, event):
            idling_mock(self, 'on_exit')

        def register_handlers(self):
            self.handlers = {
                'run': self.run,
                'do': self.do,
                'enter': self.on_enter,
                'exit': self.on_exit,
            }

    def stop(state, event):
        stop_call_mock('stopping...', event.cargo)

    def do(state, event):
        entity = event.cargo['entity']
        entity.do()

    def enter(state, event):
        running_mock('running, enter')

    def exit(state, event):
        running_mock('running, exit')

    def update(state, event):
        print('update', event)

    def do_on_transition(state, event):
        action_mock('action on transition')

    class Entity(object):
        def do(self):
            print(self, self.do)

    idling = Idling('idling')
    running = State('running')
    running.handlers = {
        'stop': stop,
        'do': do,
        'update': update,
        'enter': enter,
        'exit': exit,
    }

    entity = Entity()

    sm = StateMachine('sm')
    sm.add_state(idling, initial=True)
    sm.add_state(running)
    sm.add_transition(idling, running, events=['run'], action=do_on_transition)
    sm.add_transition(running, idling, events=['stop'])
    sm.initialize()
    assert sm.state == idling
    sm.dispatch(_e('run'))
    assert sm.state == running
    assert run_call_mock.call_count == 1
    assert run_call_mock.call_args[0] == (idling, None, {})
    assert idling_mock.call_count == 1
    assert idling_mock.call_args[0] == (idling, 'on_exit')
    assert running_mock.call_count == 1
    assert running_mock.call_args[0] == ('running, enter', )
    assert action_mock.call_count == 1
    assert action_mock.call_args[0] == ('action on transition', )

    # Nothing should happen - running state has no 'run' handler
    sm.dispatch(_e('run'))
    assert sm.state == running
    assert run_call_mock.call_count == 1
    assert run_call_mock.call_args[0] == (idling, None, {})

    sm.dispatch(_e('stop'))
    assert sm.state == idling
    assert idling_mock.call_count == 2
    assert idling_mock.call_args[0] == (idling, 'on_enter')
    assert running_mock.call_count == 2
    assert running_mock.call_args[0] == ('running, exit', )
    assert stop_call_mock.call_count == 1
    assert stop_call_mock.call_args[0] == ('stopping...', {})

    # Unknown events must be ignored
    sm.dispatch(_e('blah'))
    sm.dispatch(_e('blah blah'))
    assert sm.state == idling
Exemplo n.º 19
0
def test_event_propagate():
    data = []

    def do(state, event):
        event.cargo['data'].append(state)

    def do_with_propagate(state, event):
        event.cargo['data'].append(state)
        event.propagate = True

    m = StateMachine('m')
    s0 = StateMachine('s0')
    s1 = StateMachine('s1')
    s2 = StateMachine('s2')
    s3 = StateMachine('s3')
    s4 = State('s4')

    m.add_state(s0, initial=True)
    s0.add_state(s1, initial=True)
    s1.add_state(s2, initial=True)
    s2.add_state(s3, initial=True)
    s3.add_state(s4, initial=True)

    for state in s0, s1, s2, s3, s4:
        state.handlers = {'do': do}

    m.initialize()

    # No propagate by default
    m.dispatch(_e('do', data=data))
    assert data == [s4]

    # Should propagate only to the next state that can handle the event
    data = []
    s4.handlers = {'do': do_with_propagate}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s3]

    data = []
    s4.handlers = {'do': do_with_propagate}
    s3.handlers = {}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s2]

    data = []
    s4.handlers = {'do': do_with_propagate}
    s3.handlers = {'do': do}
    m.dispatch(_e('do', data=data))
    assert data == [s4, s3]

    data = []
    s4.handlers = {}
    s3.handlers = {}
    m.dispatch(_e('do', data=data))
    assert data == [s2]

    data = []
    s4.handlers = {}
    s3.handlers = {}
    s2.handlers = {'do': do_with_propagate}
    m.dispatch(_e('do', data=data))
    assert data == [s2, s1]