diff options
-rw-r--r-- | src/sugar/tutorius/Makefile.am | 3 | ||||
-rw-r--r-- | src/sugar/tutorius/core.py | 231 | ||||
-rw-r--r-- | src/sugar/tutorius/filters.py | 7 | ||||
-rw-r--r-- | src/sugar/tutorius/gtkutils.py | 6 | ||||
-rw-r--r-- | src/sugar/tutorius/linear_creator.py | 97 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/coretests.py | 271 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/linear_creatortests.py | 71 |
7 files changed, 653 insertions, 33 deletions
diff --git a/src/sugar/tutorius/Makefile.am b/src/sugar/tutorius/Makefile.am index 1fb11e1..9ff425e 100644 --- a/src/sugar/tutorius/Makefile.am +++ b/src/sugar/tutorius/Makefile.am @@ -8,4 +8,5 @@ sugar_PYTHON = \ filters.py \ services.py \ overlayer.py \ - editor.py + editor.py \ + linear_creator.py diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 14bb751..901820f 100644 --- a/src/sugar/tutorius/core.py +++ b/src/sugar/tutorius/core.py @@ -23,6 +23,7 @@ This module contains the core classes for tutorius import gtk import logging +import copy from sugar.tutorius.dialog import TutoriusDialog from sugar.tutorius.gtkutils import find_widget @@ -81,7 +82,7 @@ class Tutorial (object): """ Switch to a new state """ - logger.debug("====NEW STATE: %s====" % name) + logger.debug("==== NEW STATE: %s ====" % name) self.state_machine.set_state(name) @@ -97,14 +98,14 @@ class Tutorial (object): #Swith to the next state pointed by the eventfilter self.set_state(eventfilter.get_next_state()) -class State: +class State(object): """ This is a step in a tutorial. The state represents a collection of actions to undertake when entering the state, and a series of event filters with associated actions that point to a possible next state. """ - def __init__(self, name, action_list=[], event_filter_list=[], tutorial=None): + def __init__(self, name, action_list=None, event_filter_list=None, tutorial=None): """ Initializes the content of the state, like loading the actions that are required and building the correct tests. @@ -116,12 +117,16 @@ class State: this state @param tutorial The higher level container of the state """ - self._actions = action_list + object.__init__(self) + + self.name = name + + self._actions = action_list or [] # Unused for now #self.tests = [] - self._event_filters = event_filter_list + self._event_filters = event_filter_list or [] self.tutorial = tutorial @@ -177,19 +182,64 @@ class State: # Warn the higher level that we wish to change state self.tutorial.set_state(event_filter.get_next_state()) + + # Model manipulation + # These functions are used to simplify the creation of states + def add_action(self, new_action): + """ + Adds an action to the state (only if it wasn't added before) + + @param new_action The new action to execute when in this state + @return True if added, False otherwise + """ + if new_action not in self._actions: + self._actions.append(new_action) + return True + return False + + # remove_action - We did not define names for the action, hence they're + # pretty hard to remove on a precise basis - # Unused for now -## def verify(self): -## """Run the internal tests to see if one of them passes. If it does, -## then do the associated processing to go in the next state.""" -## for test in self.tests: -## if test.verify() == True: -## actions = test.get_actions() -## for act in actions: -## act.do() -## # Now that we execute the actions related to a test, we might -## # want to undo them right after --- should we use a callback or -## # a timer? + def get_action_list(self): + """ + @return A list of actions that the state will execute + """ + return self._actions + + def clear_actions(self): + """ + Removes all the action associated with this state. A cleared state will + not do anything when entered or exited. + """ + self._actions = [] + + def add_event_filter(self, event_filter): + """ + Adds an event filter that will cause a transition from this state. + + The same event filter may not be added twice. + + @param event_filter The new event filter that will trigger a transition + @return True if added, False otherwise + """ + if event_filter not in self._event_filters: + self._event_filters.append(event_filter) + return True + return False + + def get_event_filter_list(self): + """ + @return The list of event filters associated with this state. + """ + return self._event_filters + + def clear_event_filters(self): + """ + Removes all the event filters associated with this state. A state that + was just cleared will become a sink and will be the end of the + tutorial. + """ + self._event_filters = [] class FiniteStateMachine(State): """ @@ -202,7 +252,7 @@ class FiniteStateMachine(State): inserted in the FSM, and that there are no nested FSM inside. """ - def __init__(self, name, tutorial=None, state_dict={}, start_state_name="INIT", action_list=[]): + def __init__(self, name, tutorial=None, state_dict=None, start_state_name="INIT", action_list=None): """ The constructor for a FSM. Pass in the start state and the setup actions that need to be taken when the FSM itself start (which may be @@ -224,18 +274,20 @@ class FiniteStateMachine(State): self.tutorial = tutorial # Dictionnary of states contained in the FSM - self._states = state_dict + self._states = state_dict or {} - # Remember the initial state - we might want to reset - # or rewind the FSM at a later moment - self.start_state = state_dict[start_state_name] - self.current_state = self.start_state + self.start_state_name = start_state_name + # If we have a filled input dictionary + if len(self._states) > 0: + self.current_state = self._states[self.start_state_name] + else: + self.current_state = None # Register the actions for the FSM - They will be processed at the # FSM level, meaning that when the FSM will start, it will first # execute those actions. When the FSM closes, it will tear down the # inner actions of the state, then close its own actions - self.actions = action_list + self.actions = action_list or [] # Flag to mention that the FSM was initialized self._fsm_setup_done = False @@ -260,8 +312,7 @@ class FiniteStateMachine(State): state.set_tutorial(tutorial) else: raise RuntimeWarning(\ - "The FSM %s is already associated with a tutorial."%self.name\ - ) + "The FSM %s is already associated with a tutorial."%self.name) def setup(self): """ @@ -275,6 +326,10 @@ class FiniteStateMachine(State): # If we never initialized the FSM itself, then we need to run all the # actions associated with the FSM. if self._fsm_setup_done == False: + # Remember the initial state - we might want to reset + # or rewind the FSM at a later moment + self.start_state = self._states[self.start_state_name] + self.current_state = self.start_state # Flag the FSM level setup as done self._fsm_setup_done = True # Execute all the FSM level actions @@ -314,13 +369,21 @@ class FiniteStateMachine(State): # Call the initial actions in the new state self.setup() + def get_current_state_name(self): + """ + Returns the name of the current state. + + @return A string representing the name of the current state + """ + return self.current_state.name def teardown(self): """ Revert any changes done by setup() """ # Teardown the current state - self.current_state.teardown() + if self.current_state is not None: + self.current_state.teardown() # If we just finished the whole FSM, we need to also call the teardown # on the FSM level actions @@ -331,7 +394,113 @@ class FiniteStateMachine(State): for action in self.actions: action.undo() - #Unused for now -## def verify(self): -## """Verify if the current state passes its tests""" -## return self.current_state.verify() + # TODO : It might be nice to have a start() and stop() method for the + # FSM. + + # Data manipulation section + # These functions are dedicated to the building and editing of a graph. + def add_state(self, new_state): + """ + Inserts a new state in the FSM. + + @param new_state The State object that will now be part of the FSM + @raise KeyError In the case where a state with this name already exists + """ + if self._states.has_key(new_state.name): + raise KeyError("There is already a state by this name in the FSM") + + self._states[new_state.name] = new_state + + # Not such a great name for the state accessor... We already have a + # set_state name, so get_state would conflict with the notion of current + # state - I would recommend having a set_current_state instead. + def get_state_by_name(self, state_name): + """ + Fetches a state from the FSM, based on its name. If there is no + such state, the method will throw a KeyError. + + @param state_name The name of the desired state + @return The State object having the given name + """ + return self._states[state_name] + + def remove_state(self, state_name): + """ + Removes a state from the FSM. Raises a KeyError when the state is + not existent. + + Warning : removing a state will also remove all the event filters that + point to this given name, to preserve the FSM's integrity. If you only + want to edit a state, you would be better off fetching this state with + get_state_by_name(). + + @param state_name A string being the name of the state to remove + @raise KeyError When the state_name does not a represent a real state + stored in the dictionary + """ + + state_to_remove = self._states[state_name] + + # Remove the state from the states' dictionnary + for st in self._states.itervalues(): + # Iterate through the list of event filters and remove those + # that point to the state that will be removed + + #TODO : Move this code inside the State itself - we're breaking + # encap :P + for event_filter in st._event_filters: + if event_filter.get_next_state() == state_name: + st._event_filters.remove(event_filter) + + # Remove the state from the dictionary + del self._states[state_name] + + # Exploration methods - used to know more about a given state + def get_following_states(self, state_name): + """ + Returns a tuple of the names of the states that point to the given + state. If there is no such state, the function raises a KeyError. + + @param state_name The name of the state to analyse + @raise KeyError When there is no state by this name in the FSM + """ + state = self._states[state_name] + + next_states = set() + + for event_filter in state._event_filters: + next_states.add(event_filter.get_next_state()) + + return tuple(next_states) + + def get_previous_states(self, state_name): + """ + Returns a tuple of the names of the state that can transition to + the given state. If there is no such state, the function raises a + KeyError. + + @param state_name The name of the state that the returned states might + transition to. + """ + # This might seem a bit funny, but we don't verify if the given + # state is present or not in the dictionary. + # This is due to the fact that when building a graph, we might have a + # prototypal state that has not been inserted yet. We could not know + # which states are pointing to it until we insert it in the graph. + + states = [] + # Walk through the list of states + for st in self._states.itervalues(): + for event_filter in st._event_filters: + if event_filter.get_next_state() == state_name: + states.append(event_filter.get_next_state()) + continue + + return tuple(states) + + # Convenience methods to see the content of a FSM + def __str__(self): + out_string = "" + for st in self._states.itervalues(): + out_string += st.name + ", " + return out_string
\ No newline at end of file diff --git a/src/sugar/tutorius/filters.py b/src/sugar/tutorius/filters.py index 4c04cf6..3acbb36 100644 --- a/src/sugar/tutorius/filters.py +++ b/src/sugar/tutorius/filters.py @@ -37,6 +37,13 @@ class EventFilter(object): """ return self._next_state + def set_next_state(self, new_next_name): + """ + Setter for the next state. Should only be used during construction of + the event_fitler, not while the tutorial is running. + """ + self._next_state = new_next_name + def install_handlers(self, callback, **kwargs): """ install_handlers is called for eventfilters to setup all diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py index efa6eef..c5c1e12 100644 --- a/src/sugar/tutorius/gtkutils.py +++ b/src/sugar/tutorius/gtkutils.py @@ -19,6 +19,12 @@ Utility classes and functions that are gtk related """ import gtk +def activity(activity=None, singleton=[]): + if activity: + singleton.append(activity) + return singleton[0] + + def find_widget(base, target_fqdn): """Find a widget by digging into a parent widget's children tree @param base the parent widget diff --git a/src/sugar/tutorius/linear_creator.py b/src/sugar/tutorius/linear_creator.py new file mode 100644 index 0000000..02bb497 --- /dev/null +++ b/src/sugar/tutorius/linear_creator.py @@ -0,0 +1,97 @@ +# Copyright (C) 2009, Tutorius.org +# Greatly influenced by sugar/activity/namingalert.py +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.tutorius.core import * +from sugar.tutorius.actions import * +from sugar.tutorius.filters import * + +from copy import deepcopy + +class LinearCreator(object): + """ + This class is used to create a FSM from a linear sequence of orders. The + orders themselves are meant to be either an action or a transition. + """ + + def __init__(self): + self.fsm = FiniteStateMachine("Sample Tutorial") + self.current_actions = [] + self.nb_state = 0 + + def set_name(self, name): + """ + Sets the name of the generated FSM. + """ + self.fsm.name = name + + def action(self, action): + """ + Adds an action to execute in the current state. + """ + self.current_actions.append(action) + + def event(self, event_filter): + """ + Adds a transition to another state. When executing this, all the actions + previously called will be bundled in a single state, with the exit + condition of this state being the transition just added. + + Whatever the name of the next state you inserted in the event, it will + be replaced to point to the next event in the line. + """ + if len(self.current_actions) != 0: + state_name = "" + if self.nb_state == 0: + state_name = "INIT" + else: + state_name = "State" + str(self.nb_state) + # Set the next state name - there is no way the caller should have + # to deal with that. + next_state_name = "State" + str(self.nb_state+1) + event_filter.set_next_state(next_state_name) + + state = State(state_name, action_list=self.current_actions, event_filter_list=[event_filter]) + self.nb_state += 1 + self.fsm.add_state(state) + + # Clear the actions from the list + self.current_actions = [] + + def generate_fsm(self): + """ + Returns a finite state machine corresponding to the sequence of calls + that were made from this point on. + """ + # Copy the whole FSM that was generated yet + new_fsm = deepcopy(self.fsm) + + # Generate the final state + state = None + if len(self.current_actions) != 0: + state = State("State" + str(self.nb_state), action_list=self.current_actions) + # Don't increment the nb_state here - we would break the linearity + # because we might generate more stuff with this creator later. + # Since we rely on linearity for continuity when generating the + # next state's name on an event filter, we cannot increment here. + else: + state = State("State" + str(self.nb_state)) + + # Insert the state in the copy of the FSM + new_fsm.add_state(state) + + return new_fsm +
\ No newline at end of file diff --git a/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py index 7792930..de2ff1d 100644 --- a/src/sugar/tutorius/tests/coretests.py +++ b/src/sugar/tutorius/tests/coretests.py @@ -76,7 +76,7 @@ class CountAction(Action): class TriggerEventFilter(EventFilter): """ - This event filter can be triggered by simply calling its execute function. + This event filter can be triggered by simply calling its do_callback function. Used to fake events and see the effect on the FSM. """ @@ -93,6 +93,21 @@ class TriggerEventFilter(EventFilter): def _inner_cb(self, event_filter): self.toggle_on_callback = not self.toggle_on_callback +class FakeEventFilter(TriggerEventFilter): + """ + This is a fake event that is connected to the tutorial. + + The difference between this one and the TriggerEventFilter is that the + tutorial's set_state will be called on the callback. + + Do not forget to add the do_callback() after creating the object. + """ + def set_tutorial(self, tutorial): + self.tutorial = tutorial + + def _inner_cb(self, event_filter): + self.toggle_on_callback = not self.toggle_on_callback + self.tutorial.set_state(event_filter.get_next_state()) class BaseActionTests(unittest.TestCase): def test_do_unimplemented(self): act = Action() @@ -206,7 +221,261 @@ class StateTest(unittest.TestCase): assert False, "No RuntimeWarning was raised on second set_tutorial" except : pass + + def test_add_action(self): + """ + Tests on manipulating the actions inside a state. + """ + state = State("INIT") + + act1 = CountAction() + act2 = CountAction() + act3 = CountAction() + + # Try to add the actions + assert state.add_action(act1), "Could not add the first action" + assert state.add_action(act2), "Could not add the second action" + assert state.add_action(act3), "Could not add the third action" + + # Try to add a second time an action that was already inserted + assert state.add_action(act1) == False, "Not supposed to insert an action twice" + + # Fetch the associated actions + actions = state.get_action_list() + + # Make sure all the actions are present in the state + assert act1 in actions and act2 in actions and act3 in actions,\ + "The actions were not properly inserted in the state" + + # Clear the list + state.clear_actions() + + # Make sure the list of actions is empty now + assert len(state.get_action_list()) == 0, "Clearing of actions failed" + + def test_add_event_filter(self): + state = State("INIT") + + event1 = TriggerEventFilter("s") + event2 = TriggerEventFilter("t") + event3 = TriggerEventFilter("r") + + # Insert the event filters + assert state.add_event_filter(event1), "Could not add event filter 1" + assert state.add_event_filter(event2), "Could not add event filter 2" + assert state.add_event_filter(event3), "Could not add event filter 3" + + # Make sure we cannot insert an event twice + assert state.add_event_filter(event1) == False, "Could add twice the event filter" + + # Get the list of event filters + event_filters = state.get_event_filter_list() + + assert event1 in event_filters and event2 in event_filters and event3 in event_filters, \ + "The event filters were not all added inside the state" + + # Clear the list + state.clear_event_filters() + + assert len(state.get_event_filter_list()) == 0, \ + "Could not clear the event filter list properly" +class FSMTest(unittest.TestCase): + """ + This class needs to text the interface and functionality of the Finite + State Machine. + """ + + def test_sample_usage(self): + act_init = TrueWhileActiveAction() + act_second = TrueWhileActiveAction() + + event_init = FakeEventFilter("SECOND") + + content = { + "INIT": State("INIT", action_list=[act_init],event_filter_list=[event_init]), + "SECOND": State("SECOND", action_list=[act_second]) + } + + fsm = FiniteStateMachine("SampleUsage", state_dict=content) + + assert fsm is not None, "Unable to create FSM" + + tut = Tutorial("SampleUsageTutorial", fsm) + + tut.attach(None) + event_init.set_tutorial(tut) + + assert fsm.current_state.name == "INIT", "Unable to set state to initial state" + + assert act_init.active, "FSM did not call the state's action DO properly" + + # Trigger the event of the INIT state + event_init.do_callback() + + assert act_init.active == False, "FSM did not teardown INIT properly" + + assert fsm.current_state.name == "SECOND", "FSM did not switch to SECOND state" + + assert act_second.active == True, "FSM did not setup SECOND properly" + + tut.detach() + + assert act_second.active == False, "FSM did not teardown SECOND properly" + + def test_state_insert(self): + """ + This is a simple test to insert, then find a state. + """ + st1 = State("FakeState") + + fsm = FiniteStateMachine("StateInsertTest") + + fsm.add_state(st1) + + inserted_state = fsm.get_state_by_name(st1.name) + + assert inserted_state is st1, "Inserting, then fetching a state did not work" + + # Make sure we cannot insert it twice + try : + fsm.add_state(st1) + assert False, "No error raised on addition of an already present state" + except KeyError: + pass + + def test_state_find_by_name(self): + """ + Tests the interface for fetching a state by name. + - Basic functionnality + - Non-existent state + """ + + st1 = State("INIT") + + st2 = State("second") + + fsm = FiniteStateMachine("StateFindTest") + + fsm.add_state(st1) + fsm.add_state(st2) + + # Test the fetch by name + fetched_st1 = fsm.get_state_by_name(st1.name) + + assert fetched_st1 is st1, "Fetched state is not the same as the inserted one" + + fetched_st2 = fsm.get_state_by_name(st2.name) + + assert fetched_st2 is st2, "Fetched state is not the same as the inserted one" + + try: + fsm.get_state_by_name("no such state") + assert False, "Did not get a KeyError on non-existing key search" + except KeyError: + pass + except Exception: + assert False, "Did not get the right error on non-existing key search" + + def test_state_removal(self): + """ + This test removes a state from the FSM. It also verifies that the links + from other states going into the removed state are gone. + """ + st1 = State("INIT", event_filter_list=[TriggerEventFilter("second")]) + st2 = State("second", event_filter_list=[TriggerEventFilter("third")]) + st3 = State("third", event_filter_list=[TriggerEventFilter("second")]) + + fsm = FiniteStateMachine("StateRemovalTest") + + fsm.add_state(st1) + fsm.add_state(st2) + fsm.add_state(st3) + + # First tests - Removing a non-existing state and make sure we get a + # KeyError + try: + fsm.remove_state("Non-existing") + assert False, "Removing a non-existing state did not throw a KeyError" + except KeyError: + pass + except Exception: + assert False, "Removing a non-existing state dit not throw the right kind of exception" + + + # Now try removing the second state + fsm.remove_state("second") + + # Make sure it cannot be fetched + try : + fetched_state = fsm.get_state_by_name("second") + assert False, "The supposedly removed state is still present in the FSM" + except KeyError: + pass + + # Make sure that there is no link to the removed state in the rest + # of the FSM + assert "second" not in fsm.get_following_states("INIT"),\ + "The link to second from INIT still exists after removal" + + assert "second" not in fsm.get_following_states("third"),\ + "The link to second from third still exists after removal" + +class FSMExplorationTests(unittest.TestCase): + def setUp(self): + self.buildFSM() + + def buildFSM(self): + """ + Create a sample FSM to play with in the rest of the tests. + """ + st1 = State("INIT") + st1.add_action(CountAction()) + st1.add_event_filter(TriggerEventFilter("Second")) + st1.add_event_filter(TriggerEventFilter("Third")) + + st2 = State("Second") + st2.add_action(TrueWhileActiveAction()) + st2.add_event_filter(TriggerEventFilter("Third")) + st2.add_event_filter(TriggerEventFilter("Fourth")) + + st3 = State("Third") + st3.add_action(CountAction()) + st3.add_action(TrueWhileActiveAction()) + + self.fsm = FiniteStateMachine("ExplorationTestingMachine") + self.fsm.add_state(st1) + self.fsm.add_state(st2) + self.fsm.add_state(st3) + + def validate_following_states(self, in_name, out_name_list): + nextStates = self.fsm.get_following_states(in_name) + assert list(nextStates).sort() == list(out_name_list).sort(), \ + "The following states for %s are wrong : got %s"%\ + (in_name, str(nextStates)) + + def validate_previous_states(self, in_name, out_name_list): + prevStates = self.fsm.get_previous_states(in_name) + assert list(prevStates).sort() == list(out_name_list).sort(), \ + "The following states for %s are wrong : got %s"%\ + (in_name, str(prevStates)) + + def test_get_following_states(self): + self.validate_following_states("INIT", ('Second', 'Third')) + + self.validate_following_states("Second", ("Third", "Fourth")) + + self.validate_following_states("Third", ()) + + def test_get_previous_states(self): + self.validate_previous_states("INIT", ()) + + self.validate_previous_states("Second", ("INIT")) + + self.validate_previous_states("Third", ("INIT", "Second")) + + self.validate_previous_states("Fourth", ("Second")) + if __name__ == "__main__": unittest.main() diff --git a/src/sugar/tutorius/tests/linear_creatortests.py b/src/sugar/tutorius/tests/linear_creatortests.py new file mode 100644 index 0000000..b328c45 --- /dev/null +++ b/src/sugar/tutorius/tests/linear_creatortests.py @@ -0,0 +1,71 @@ +# Copyright (C) 2009, Tutorius.org +# Greatly influenced by sugar/activity/namingalert.py +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.tutorius.core import * +from sugar.tutorius.actions import * +from sugar.tutorius.filters import * +from sugar.tutorius.linear_creator import * +from coretests import TriggerEventFilter, CountAction + +import unittest + +class CreatorTests(unittest.TestCase): + + def test_simple_usage(self): + creator = LinearCreator() + fsm_name = "SimpleUsageTest" + + creator.set_name(fsm_name) + + # Generate an FSM using the steps + creator.action(CountAction()) + creator.action(CountAction()) + + creator.event(TriggerEventFilter("Not important")) + + creator.action(CountAction()) + + creator.event(TriggerEventFilter("Not good either...")) + + fsm = creator.generate_fsm() + print "------------- Generated FSM -------------------" + print str(fsm) + print "-----------------------------------------------" + # Make sure everything worked! + assert fsm.name == fsm_name, "Name was not set properly" + + init_state = fsm.get_state_by_name("INIT") + + assert len(init_state.get_action_list()) == 2, "Creator did not insert all the actions" + + assert init_state.get_event_filter_list()[0].get_next_state() == "State1" + + state1 = fsm.get_state_by_name("State1") + + assert len(state1.get_action_list()) == 1, "Creator did not insert all the actions" + + assert state1.get_event_filter_list()[0].get_next_state() == "State2" + + # Make sure we have the final state and that it's empty + state2 = fsm.get_state_by_name("State2") + + assert len(state2.get_action_list()) == 0, "Creator inserted extra actions on wrong state" + + assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state" + +if __name__ == '__main__': + unittest.main()
\ No newline at end of file |