Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/sugar/tutorius/Makefile.am3
-rw-r--r--src/sugar/tutorius/core.py231
-rw-r--r--src/sugar/tutorius/filters.py7
-rw-r--r--src/sugar/tutorius/gtkutils.py6
-rw-r--r--src/sugar/tutorius/linear_creator.py97
-rw-r--r--src/sugar/tutorius/tests/coretests.py271
-rw-r--r--src/sugar/tutorius/tests/linear_creatortests.py71
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