From 79ffdf4c4c38e0064f25db06685af9156b39e679 Mon Sep 17 00:00:00 2001 From: mike Date: Wed, 18 Mar 2009 19:04:33 +0000 Subject: TutoriusV2 : Correcting constructors for States and FSMs, exploration tests --- diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 4b9e985..d0fc3cb 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 @@ -101,7 +102,7 @@ class State(object): 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. @@ -117,12 +118,12 @@ class State(object): self.name = name - self._actions = action_list + 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 @@ -188,8 +189,8 @@ class State(object): @param new_action The new action to execute when in this state @return True if added, False otherwise """ - if new_action not in self.action_list: - self.action_list.append(new_action) + if new_action not in self._actions: + self._actions.append(new_action) return True return False @@ -200,14 +201,14 @@ class State(object): """ @return A list of actions that the state will execute """ - return self.action_list + 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.action_list.clear() + self._actions.clear() def add_event_filter(self, event_filter): """ @@ -218,8 +219,8 @@ class State(object): @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_filter_list: - self.event_filter_list.append(event_filter) + if event_filter not in self._event_filters: + self._event_filters.append(event_filter) return True return False @@ -227,7 +228,7 @@ class State(object): """ @return The list of event filters associated with this state. """ - return self.event_filter_list + return self._event_filters def clear_event_filters(self): """ @@ -235,7 +236,7 @@ class State(object): was just cleared will become a sink and will be the end of the tutorial. """ - self.event_filter_list.clear() + self._event_filters.clear() class FiniteStateMachine(State): """ @@ -248,7 +249,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 @@ -270,18 +271,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 @@ -320,6 +323,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 @@ -372,7 +379,8 @@ class FiniteStateMachine(State): 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 @@ -395,10 +403,23 @@ class FiniteStateMachine(State): @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.state_dict.has_key(new_state.name): + if self._states.has_key(new_state.name): raise KeyError("There is already a state by this name in the FSM") - self.state_dict[new_state.name] = new_state + 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): """ @@ -415,18 +436,21 @@ class FiniteStateMachine(State): stored in the dictionary """ - state_to_remove = self.state_dict[state_name] + state_to_remove = self._states[state_name] # Remove the state from the states' dictionnary - for st in self.state_dict.itervalues(): + 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 - for event_filter in st.event_filter_list: + + #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_filter_list.remove(event_filter) + st._event_filters.remove(event_filter) # Remove the state from the dictionary - del self.state_dict[state_name] + del self._states[state_name] # Exploration methods - used to know more about a given state def get_following_states(self, state_name): @@ -437,12 +461,12 @@ class FiniteStateMachine(State): @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.state_dict[state_name] + state = self._states[state_name] - next_states = Set() + next_states = set() - for event_filter in state.event_filter_list: - next_states.insert(event_filter.get_next_state()) + for event_filter in state._event_filters: + next_states.add(event_filter.get_next_state()) return tuple(next_states) @@ -463,10 +487,17 @@ class FiniteStateMachine(State): states = [] # Walk through the list of states - for st in self.state_dict.itervalues(): - for event_filter in st.event_filter_list: + 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) \ No newline at end of file + 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/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py index 086c833..297a7c3 100644 --- a/src/sugar/tutorius/tests/coretests.py +++ b/src/sugar/tutorius/tests/coretests.py @@ -100,7 +100,7 @@ class FakeEventFilter(TriggerEventFilter): 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() function. + Do not forget to add the do_callback() after creating the object. """ def set_tutorial(self, tutorial): self.tutorial = tutorial @@ -251,5 +251,59 @@ class FSMTest(unittest.TestCase): assert act_second.active == False, "FSM did not teardown SECOND properly" +class FSMExplorationTests(unittest.TestCase): + 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.buildFSM() + 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.buildFSM() + 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() -- cgit v0.9.1