diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index 42b87b2..fe1fe29 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -493,7 +493,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): # check for success if pdu.bvlciResultCode == 0: # schedule for a refresh - self.install_task(_time() + self.bbmdTimeToLive) + self.install_task(delta=self.bbmdTimeToLive) return @@ -541,7 +541,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): self.bbmdTimeToLive = ttl # install this task to run when it gets a chance - self.install_task(0) + self.install_task(delta=0) def unregister(self): """Drop the registration with a BBMD.""" diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index f4b6006..56f41d5 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -489,7 +489,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): # check for success if pdu.bvlciResultCode == 0: # schedule for a refresh - self.install_task(_time() + self.bbmdTimeToLive) + self.install_task(delta=self.bbmdTimeToLive) return @@ -537,7 +537,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): self.bbmdTimeToLive = ttl # install this task to run when it gets a chance - self.install_task(0) + self.install_task(delta=0) def unregister(self): """Drop the registration with a BBMD.""" diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index c5e5a68..a4d99e8 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -488,7 +488,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): # check for success if pdu.bvlciResultCode == 0: # schedule for a refresh - self.install_task(_time() + self.bbmdTimeToLive) + self.install_task(delta=self.bbmdTimeToLive) return @@ -536,7 +536,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents): self.bbmdTimeToLive = ttl # install this task to run when it gets a chance - self.install_task(0) + self.install_task(delta=0) def unregister(self): """Drop the registration with a BBMD.""" diff --git a/tests/state_machine.py b/tests/state_machine.py index fc8071b..a6aea02 100755 --- a/tests/state_machine.py +++ b/tests/state_machine.py @@ -67,6 +67,15 @@ class TimeoutTransition(Transition): self.timeout = timeout +class CallTransition(Transition): + + def __init__(self, fnargs, next_state): + Transition.__init__(self, next_state) + + # a tuple of (fn, *args, *kwargs) + self.fnargs = fnargs + + # # match_pdu # @@ -134,6 +143,9 @@ class State(object): # timeout transition self.timeout_transition = None + # call transition + self.call_transition = None + def reset(self): """Override this method in a derived class if the state maintains counters or other information. Called when the associated state @@ -307,7 +319,7 @@ class State(object): def unexpected_receive(self, pdu): """Called with PDU that did not match. Unless this is trapped by the state, the default behaviour is to fail.""" - if _debug: State._debug("unexpected_receive %r", pdu) + if _debug: State._debug("unexpected_receive(%s) %r", self.doc_string, pdu) # pass along to the state machine self.state_machine.unexpected_receive(pdu) @@ -393,6 +405,48 @@ class State(object): # return the next state return next_state + def call(self, fn, *args, **kwargs): + """Create a CallTransition from this state to another, possibly new, + state. The next state is returned for method chaining. + + :param criteria: PDU to match + :param next_state: destination state after a successful match + + Simulate the function as if it was defined in Python3.5+ like this: + def receive(self, fn, *args, next_state=None, **kwargs) + """ + if _debug: State._debug("call(%s) %r %r %r", self.doc_string, fn, args, kwargs) + + # only one call per state + if self.call_transition: + raise RuntimeError("only one 'call' per state") + + # extract the next_state keyword argument + if 'next_state' in kwargs: + next_state = kwargs['next_state'] + if _debug: State._debug(" - next_state: %r", next_state) + + del kwargs['next_state'] + else: + next_state = None + + # maybe build a new state + if not next_state: + next_state = self.state_machine.new_state() + if _debug: State._debug(" - new next_state: %r", next_state) + elif next_state not in self.state_machine.states: + raise ValueError("off the rails") + + # create a bundle of the match criteria + fnargs = (fn, args, kwargs) + if _debug: State._debug(" - fnargs: %r", fnargs) + + # add this to the list of transitions + self.call_transition = CallTransition(fnargs, next_state) + + # return the next state + return next_state + def __repr__(self): return "<%s(%s) at %s>" % ( self.__class__.__name__, @@ -421,8 +475,12 @@ class StateMachine(object): unexpected_receive_state=None, machine_group=None, state_subclass=State, + name='', ): - if _debug: StateMachine._debug("__init__") + if _debug: StateMachine._debug("__init__(%s)", name) + + # save the name for debugging + self.name = name # no states to starting out, not running self.states = [] @@ -476,7 +534,7 @@ class StateMachine(object): self.timeout_task = None def new_state(self, doc="", state_subclass=None): - if _debug: StateMachine._debug("new_state %r %r", doc, state_subclass) + if _debug: StateMachine._debug("new_state(%s) %r %r", self.name, doc, state_subclass) # check for proper subclass if state_subclass and not issubclass(state_subclass, State): @@ -493,7 +551,7 @@ class StateMachine(object): return state def reset(self): - if _debug: StateMachine._debug("reset") + if _debug: StateMachine._debug("reset(%s)", self.name) # make sure we're not running if self.running: @@ -511,7 +569,7 @@ class StateMachine(object): state.reset() def run(self): - if _debug: StateMachine._debug("run") + if _debug: StateMachine._debug("run(%s)", self.name) if self.running: raise RuntimeError("state machine running") @@ -542,7 +600,7 @@ class StateMachine(object): def halt(self): """Called when the state machine should no longer be running.""" - if _debug: StateMachine._debug("halt") + if _debug: StateMachine._debug("halt(%s)", self.name) # make sure we're running if not self.running: @@ -558,14 +616,14 @@ class StateMachine(object): def success(self): """Called when the state machine has successfully completed.""" - if _debug: StateMachine._debug("success") + if _debug: StateMachine._debug("success(%s)", self.name) def fail(self): """Called when the state machine has failed.""" - if _debug: StateMachine._debug("fail") + if _debug: StateMachine._debug("fail(%s)", self.name) def goto_state(self, state): - if _debug: StateMachine._debug("goto_state %r", state) + if _debug: StateMachine._debug("goto_state(%s) %r", self.name, state) # where do you think you're going? if state not in self.states: @@ -586,6 +644,10 @@ class StateMachine(object): # here we are current_state = self.current_state = state + # let the state do something + current_state.enter_state() + if _debug: StateMachine._debug(" - state entered") + # events are managed by a state machine group if self.machine_group: # setting events @@ -626,10 +688,6 @@ class StateMachine(object): return - # let the state do something - current_state.enter_state() - if _debug: StateMachine._debug(" - state entered") - # assume we can stay next_state = None @@ -651,6 +709,21 @@ class StateMachine(object): else: if _debug: StateMachine._debug(" - not part of a group") + # call things that need to be called + if current_state.call_transition: + if _debug: StateMachine._debug(" - calling: %r", current_state.call_transition) + + # pull apart the pieces and call it + fn, args, kwargs = current_state.call_transition.fnargs + fn( *args, **kwargs) + if _debug: StateMachine._debug(" - called") + + # check for a transition + next_state = current_state.call_transition.next_state + if _debug: StateMachine._debug(" - next_state: %r", next_state) + else: + if _debug: StateMachine._debug(" - no calls") + # send everything that needs to be sent if not next_state: for transition in current_state.send_transitions: @@ -710,7 +783,7 @@ class StateMachine(object): self.transaction_log.append((">>>", pdu),) def receive(self, pdu): - if _debug: StateMachine._debug("receive %r", pdu) + if _debug: StateMachine._debug("receive(%s) %r", self.name, pdu) if not self.running: if _debug: StateMachine._debug(" - not running") @@ -726,6 +799,7 @@ class StateMachine(object): if not self.current_state: raise RuntimeError("no current state") current_state = self.current_state + if _debug: StateMachine._debug(" - current_state: %r", current_state) # let the state know this was received current_state.before_receive(pdu) @@ -766,7 +840,7 @@ class StateMachine(object): def unexpected_receive(self, pdu): """Called with PDU that did not match. Unless this is trapped by the state, the default behaviour is to fail.""" - if _debug: StateMachine._debug("unexpected_receive %r", pdu) + if _debug: StateMachine._debug("unexpected_receive(%s) %r", self.name, pdu) # go to the unexpected receive state (failing) self.goto_state(self.unexpected_receive_state) @@ -775,7 +849,7 @@ class StateMachine(object): """Called by the state machine group when an event is set, the state machine checks to see if it's waiting for the event and makes the state transition if there is a match.""" - if _debug: StateMachine._debug("event_set %r", event_id) + if _debug: StateMachine._debug("event_set(%s) %r", self.name, event_id) if not self.running: if _debug: StateMachine._debug(" - not running") @@ -815,7 +889,7 @@ class StateMachine(object): self.goto_state(next_state) def state_timeout(self): - if _debug: StateMachine._debug("state_timeout") + if _debug: StateMachine._debug("state_timeout(%s)", self.name) if not self.running: raise RuntimeError("state machine not running") @@ -826,7 +900,7 @@ class StateMachine(object): self.goto_state(self.current_state.timeout_transition.next_state) def state_machine_timeout(self): - if _debug: StateMachine._debug("state_machine_timeout") + if _debug: StateMachine._debug("state_machine_timeout(%s)", self.name) if not self.running: raise RuntimeError("state machine not running") @@ -835,7 +909,7 @@ class StateMachine(object): self.goto_state(self.timeout_state) def match_pdu(self, pdu, criteria): - if _debug: StateMachine._debug("match_pdu %r %r", pdu, criteria) + if _debug: StateMachine._debug("match_pdu(%s) %r %r", self.name, pdu, criteria) # separate the pdu_type and attributes to match pdu_type, pdu_attrs = criteria @@ -1062,7 +1136,12 @@ class StateMachineGroup(object): def fail(self): """Called when all of the machines in the group have halted and at at least one of them is in a 'fail' final state.""" - if _debug: StateMachineGroup._debug("fail") + if _debug: + StateMachineGroup._debug("fail") + for state_machine in self.state_machines: + StateMachineGroup._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + StateMachineGroup._debug(" %s %s", direction, str(pdu)) self.is_running = False self.is_fail_state = True @@ -1120,3 +1199,4 @@ class ServerStateMachine(Server, StateMachine): def indication(self, pdu): if _debug: ServerStateMachine._debug("indication %r", pdu) self.receive(pdu) + diff --git a/tests/test_bvll/helpers.py b/tests/test_bvll/helpers.py index beec9b7..58da6f7 100644 --- a/tests/test_bvll/helpers.py +++ b/tests/test_bvll/helpers.py @@ -112,7 +112,8 @@ class SnifferNode(_repr, ClientStateMachine): if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) - # save the address + # save the name and address + self.name = address self.address = Address(address) # create a promiscuous node, added to the network @@ -123,6 +124,31 @@ class SnifferNode(_repr, ClientStateMachine): bind(self, self.node) +# +# CodecNode +# + +@bacpypes_debugging +class CodecNode(_repr, ClientStateMachine): + + def __init__(self, address, vlan): + if _debug: CodecNode._debug("__init__ %r %r", address, vlan) + ClientStateMachine.__init__(self) + + # save the name and address + self.name = address + self.address = Address(address) + + # BACnet/IP interpreter + self.annexj = AnnexJCodec() + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self, self.annexj, self.mux) + + # # SimpleNode # @@ -134,7 +160,8 @@ class SimpleNode(_repr, ClientStateMachine): if _debug: SimpleNode._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) - # save the address + # save the name and address + self.name = address self.address = Address(address) # BACnet/IP interpreter @@ -159,7 +186,8 @@ class ForeignNode(_repr, ClientStateMachine): if _debug: ForeignNode._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) - # save the address + # save the name and address + self.name = address self.address = Address(address) # BACnet/IP interpreter @@ -183,17 +211,24 @@ class BBMDNode(_repr, ClientStateMachine): if _debug: BBMDNode._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) - # save the address + # save the name and address + self.name = address self.address = Address(address) # BACnet/IP interpreter self.bip = BIPBBMD(self.address) self.annexj = AnnexJCodec() + # build an address, full mask + bdt_address = "%s/32:%d" % self.address.addrTuple + if _debug: BBMDNode._debug(" - bdt_address: %r", bdt_address) + + # add itself as the first entry in the BDT + self.bip.add_peer(Address(bdt_address)) + # fake multiplexer has a VLAN node in it self.mux = FauxMultiplexer(self.address, vlan) # bind the stack together bind(self, self.bip, self.annexj, self.mux) - diff --git a/tests/test_bvll/test_foreign.py b/tests/test_bvll/test_foreign.py index 8e296a6..5ad1140 100644 --- a/tests/test_bvll/test_foreign.py +++ b/tests/test_bvll/test_foreign.py @@ -12,11 +12,12 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.pdu import Address, PDU, LocalBroadcast from bacpypes.vlan import IPNetwork, IPRouter +from bacpypes.bvll import ReadForeignDeviceTable, ReadForeignDeviceTableAck -from ..state_machine import match_pdu, StateMachineGroup +from ..state_machine import StateMachineGroup from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, SimpleNode, ForeignNode, BBMDNode +from .helpers import SnifferNode, CodecNode, SimpleNode, ForeignNode, BBMDNode # some debugging _debug = 0 @@ -49,26 +50,14 @@ class TNetwork(StateMachineGroup): self.home_vlan = IPNetwork() self.router.add_network(Address("192.168.5.1/24"), self.home_vlan) - # home sniffer node - self.home_sniffer = SnifferNode("192.168.5.254/24", self.home_vlan) - self.append(self.home_sniffer) - # make a remote LAN self.remote_vlan = IPNetwork() self.router.add_network(Address("192.168.6.1/24"), self.remote_vlan) - # remote sniffer node - self.remote_sniffer = SnifferNode("192.168.6.254/24", self.remote_vlan) - self.append(self.remote_sniffer) - # the foreign device self.fd = ForeignNode("192.168.6.2/24", self.remote_vlan) self.append(self.fd) - # intermediate test node - self.tnode = SimpleNode("192.168.5.2/24", self.home_vlan) - self.append(self.tnode) - # bbmd self.bbmd = BBMDNode("192.168.5.3/24", self.home_vlan) self.append(self.bbmd) @@ -81,12 +70,7 @@ class TNetwork(StateMachineGroup): # run it for some time run_time_machine(time_limit) - if _debug: - TNetwork._debug(" - time machine finished") - for state_machine in self.state_machines: - TNetwork._debug(" - machine: %r", state_machine) - for direction, pdu in state_machine.transaction_log: - TNetwork._debug(" %s %s", direction, str(pdu)) + if _debug: TNetwork._debug(" - time machine finished") # check for success all_success, some_failed = super(TNetwork, self).check_for_success() @@ -94,22 +78,227 @@ class TNetwork(StateMachineGroup): @bacpypes_debugging -class TestSimple(unittest.TestCase): +class TestForeign(unittest.TestCase): def test_idle(self): """Test an idle network, nothing happens is success.""" - if _debug: TestSimple._debug("test_idle") + if _debug: TestForeign._debug("test_idle") # create a network tnet = TNetwork() # all start states are successful - tnet.home_sniffer.start_state.success() - tnet.remote_sniffer.start_state.success() tnet.fd.start_state.success() - tnet.tnode.start_state.success() tnet.bbmd.start_state.success() # run the group tnet.run() + def test_registration(self): + """Test foreign device registration.""" + if _debug: TestForeign._debug("test_registration") + + # create a network + tnet = TNetwork() + + # add an addition codec node to the home vlan + cnode = CodecNode("192.168.5.2/24", tnet.home_vlan) + tnet.append(cnode) + + # home sniffer node + home_sniffer = SnifferNode("192.168.5.254/24", tnet.home_vlan) + tnet.append(home_sniffer) + + # remote sniffer node + remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + tnet.append(remote_sniffer) + + # tell the B/IP layer of the foreign device to register + tnet.fd.start_state \ + .call(tnet.fd.bip.register, tnet.bbmd.address, 30) \ + .success() + + # sniffer pieces + registration_request = xxtob('81.05.0006' # bvlci + '001e' # time-to-live + ) + registration_ack = xxtob('81.00.0006.0000') # simple ack + + # remote sniffer sees registration + remote_sniffer.start_state \ + .receive(PDU, pduData=registration_request).doc("--1-1") \ + .receive(PDU, pduData=registration_ack).doc("--1-2") \ + .set_event('fd-registered').doc("--1-3") \ + .success() + + # the bbmd is idle + tnet.bbmd.start_state.success() + + # read the FDT + cnode.start_state \ + .wait_event('fd-registered').doc("--1-4") \ + .send(ReadForeignDeviceTable(destination=tnet.bbmd.address)).doc("--1-5") \ + .receive(ReadForeignDeviceTableAck).doc("--1-6") \ + .success() + + # the tnode reads the registration table + read_fdt_request = xxtob('81.06.0004') # bvlci + read_fdt_ack = xxtob('81.07.000e' # read-ack + 'c0.a8.06.02.ba.c0 001e 0023' # address, ttl, remaining + ) + + # home sniffer sees registration + home_sniffer.start_state \ + .receive(PDU, pduData=registration_request).doc("--1-7") \ + .receive(PDU, pduData=registration_ack).doc("--1-8") \ + .receive(PDU, pduData=read_fdt_request).doc("--1-9") \ + .receive(PDU, pduData=read_fdt_ack).doc("--1-A") \ + .success() + + # run the group + tnet.run() + + def test_refresh_registration(self): + """Test refreshing foreign device registration.""" + if _debug: TestForeign._debug("test_refresh_registration") + + # create a network + tnet = TNetwork() + + # tell the B/IP layer of the foreign device to register + tnet.fd.start_state \ + .call(tnet.fd.bip.register, tnet.bbmd.address, 10) \ + .success() + + # the bbmd is idle + tnet.bbmd.start_state.success() + + # remote sniffer node + remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + tnet.append(remote_sniffer) + + # sniffer pieces + registration_request = xxtob('81.05.0006' # bvlci + '000a' # time-to-live + ) + registration_ack = xxtob('81.00.0006.0000') # simple ack + + # remote sniffer sees registration + remote_sniffer.start_state \ + .receive(PDU, pduData=registration_request).doc("--2-1") \ + .receive(PDU, pduData=registration_ack).doc("--2-2") \ + .receive(PDU, pduData=registration_request).doc("--2-3") \ + .receive(PDU, pduData=registration_ack).doc("--2-4") \ + .success() + + # run the group + tnet.run() + + def test_unicast(self): + """Test a unicast message from the foreign device to the bbmd.""" + if _debug: TestForeign._debug("test_unicast") + + # create a network + tnet = TNetwork() + + # make a PDU from node 1 to node 2 + pdu_data = xxtob('dead.beef') + pdu = PDU(pdu_data, source=tnet.fd.address, destination=tnet.bbmd.address) + if _debug: TestForeign._debug(" - pdu: %r", pdu) + + # register, wait for ack, send some beef + tnet.fd.start_state \ + .call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("--3-1") \ + .wait_event('fd-registered').doc("--3-2") \ + .send(pdu).doc("--3-3") \ + .success() + + # the bbmd is happy when it gets the pdu + tnet.bbmd.start_state \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \ + .success() + + # remote sniffer node + remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + tnet.append(remote_sniffer) + + # sniffer pieces + registration_request = xxtob('81.05.0006' # bvlci + '003c' # time-to-live (60) + ) + registration_ack = xxtob('81.00.0006.0000') # simple ack + unicast_pdu = xxtob('81.0a.0008' # original unicast bvlci + 'dead.beef' # PDU being unicast + ) + + # remote sniffer sees registration + remote_sniffer.start_state \ + .receive(PDU, pduData=registration_request).doc("--3-4") \ + .receive(PDU, pduData=registration_ack).doc("--3-5") \ + .set_event('fd-registered').doc("--3-6") \ + .receive(PDU, pduData=unicast_pdu).doc("--3-7") \ + .success() + + # run the group + tnet.run() + + def test_broadcast(self): + """Test a broadcast message from the foreign device to the bbmd.""" + if _debug: TestForeign._debug("+test_broadcast") + + # create a network + tnet = TNetwork() + + # make a broadcast pdu + pdu_data = xxtob('dead.beef') + pdu = PDU(pdu_data, destination=LocalBroadcast()) + if _debug: TestForeign._debug(" - pdu: %r", pdu) + + # register, wait for ack, send some beef + tnet.fd.start_state \ + .call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("--4-1") \ + .wait_event('4-registered').doc("--4-2") \ + .send(pdu).doc("--4-3") \ + .success() + + # the bbmd is happy when it gets the pdu + tnet.bbmd.start_state \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \ + .success() + + # home sniffer node + home_node = SimpleNode("192.168.5.254/24", tnet.home_vlan) + tnet.append(home_node) + + # home node happy when getting the pdu, broadcast by the bbmd + home_node.start_state.doc("--4-4") \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("--4-5") \ + .success() + + # remote sniffer node + remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + tnet.append(remote_sniffer) + + # sniffer pieces + registration_request = xxtob('81.05.0006' # bvlci + '003c' # time-to-live (60) + ) + registration_ack = xxtob('81.00.0006.0000') # simple ack + distribute_pdu = xxtob('81.09.0008' # bvlci + 'deadbeef' # PDU to broadcast + ) + + # remote sniffer sees registration + remote_sniffer.start_state \ + .receive(PDU, pduData=registration_request).doc("--4-6") \ + .call(TestForeign._debug, "::--4-5").doc("--4-7") \ + .receive(PDU, pduData=registration_ack).doc("--4-8") \ + .set_event('4-registered') \ + .call(TestForeign._debug, "::--4-7").doc("--4-9") \ + .receive(PDU, pduData=distribute_pdu).doc("--4-10") \ + .success() + + # run the group + tnet.run(4.0) + if _debug: TestForeign._debug("-test_broadcast") + diff --git a/tests/test_utilities/test_state_machine.py b/tests/test_utilities/test_state_machine.py index 74e8c9d..f9df30c 100644 --- a/tests/test_utilities/test_state_machine.py +++ b/tests/test_utilities/test_state_machine.py @@ -262,6 +262,26 @@ class TestStateMachine(unittest.TestCase): assert tsm.transaction_log[0][1] is bad_pdu if _debug: TestStateMachine._debug(" - passed") + def test_state_machine_call(self): + if _debug: TestStateMachine._debug("test_state_machine_call") + + # simple hook + self._called = False + + # create a trapped state machine + tsm = TrappedStateMachine() + + # make a send transition from start to success, run the machine + tsm.start_state.call(setattr, self, '_called', True).success() + tsm.run() + + # check for success + assert not tsm.running + assert tsm.current_state.is_success_state + + # check for the call + assert self._called + def test_state_machine_loop_01(self): if _debug: TestStateMachine._debug("test_state_machine_loop_01")