Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tests/tutorialtests.py209
-rw-r--r--tutorius/tutorial.py467
2 files changed, 676 insertions, 0 deletions
diff --git a/tests/tutorialtests.py b/tests/tutorialtests.py
new file mode 100644
index 0000000..864c452
--- /dev/null
+++ b/tests/tutorialtests.py
@@ -0,0 +1,209 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com>
+#
+# 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
+"""
+Core Tests
+
+This module contains all the tests that pertain to the usage of the Tutorius
+Core. This means that the Event Filters, the Finite State Machine and all the
+related elements and interfaces are tested here.
+
+Usage of actions and event filters is tested, but not the concrete actions
+and event filters. Those are in their separate test module
+
+"""
+
+import unittest
+
+import copy
+import logging
+from sugar.tutorius.tutorial import *
+
+# The following tests are organized around 4 classes:
+#
+# Black box tests:
+# Those tests should limit themselves to exercise the
+# interface of the object so everything should be tested
+# only through the interface the object offers. This will
+# ease test maintenance since we anticipate most changes
+# will be about the implementation of an object and not
+# its interface.
+#
+# Tests definitions are written assuming the previous tests
+# did complete correctly so the number of things to assert
+# is minimal.
+#
+# Basic interface cases:
+# Test the interface of the object for trivial cases
+# just to assert that the functionality this object
+# offers really works
+#
+# Limit cases:
+# Test edge cases that cover more obscure usage
+# scenarios but that should be valid nonetheless
+#
+# Error cases:
+# Test wrong inputs to make sure that the object is hard
+# to misuse and do generate proper errors
+#
+# White box tests:
+# Those should be used only for really important algorithms
+# to make sure they behave correctly in every cases, otherwise
+# the tests will break each time we change something in the
+# implementation
+
+class StateTest(unittest.TestCase):
+ """Test basic functionalities of states used by tutorials"""
+
+ def setUp(self):
+ self.state = State("State1")
+
+ def tearDown(self):
+ pass
+
+ ######################### Basic interface cases #########################
+
+ #### Action
+ def test_add_dummy_action(self):
+ action_name = self.state.add_action("action1")
+ assert len(self.state.get_action_dict()) == 1
+ assert self.state.get_action_dict().has_key(action_name)
+ assert self.state.get_action_dict()[action_name] == "action1"
+
+ def test_add_generate_unique_action_names(self):
+ action_name1 = self.state.add_action("action1")
+ action_name2 = self.state.add_action("action2")
+ assert action_name1 != action_name2
+
+ def test_update_dummy_action(self):
+ action_name = self.state.add_action("action1")
+ self.state.update_action(action_name, "action2")
+ assert len(self.state.get_action_dict()) == 1
+ assert self.state.get_action_dict().has_key(action_name)
+ assert self.state.get_action_dict()[action_name] == "action2"
+
+ def test_delete_dummy_action(self):
+ action_name = self.state.add_action("action1")
+ assert len(self.state.get_action_dict()) == 1
+ assert self.state.get_action_dict().has_key(action_name)
+ assert self.state.get_action_dict()[action_name] == "action1"
+
+ self.state.delete_action(action_name)
+ assert len(self.state.get_action_dict()) == 0
+
+ def test_delete_all_dummy_actions(self):
+ action_name = self.state.add_action("action1")
+ assert len(self.state.get_action_dict()) == 1
+ assert self.state.get_action_dict().has_key(action_name)
+ assert self.state.get_action_dict()[action_name] == "action1"
+
+ self.state.delete_actions()
+ assert len(self.state.get_action_dict()) == 0
+
+ #### Transition
+ def test_add_dummy_transition(self):
+ transition_name = self.state.add_transition("transition1")
+ assert len(self.state.get_transition_dict()) == 1
+ assert self.state.get_transition_dict().has_key(transition_name)
+ assert self.state.get_transition_dict()[transition_name] == "transition1"
+
+ def test_add_generate_unique_transition_names(self):
+ transition_name1 = self.state.add_transition("transition1")
+ transition_name2 = self.state.add_transition("transition2")
+ assert transition_name1 != transition_name2
+
+ def test_update_dummy_transition(self):
+ transition_name = self.state.add_transition("transition1")
+ self.state.update_transition(transition_name, "transition2")
+ assert len(self.state.get_transition_dict()) == 1
+ assert self.state.get_transition_dict().has_key(transition_name)
+ assert self.state.get_transition_dict()[transition_name] == "transition2"
+
+ def test_delete_dummy_transition(self):
+ transition_name = self.state.add_transition("transition1")
+ assert len(self.state.get_transition_dict()) == 1
+ assert self.state.get_transition_dict().has_key(transition_name)
+ assert self.state.get_transition_dict()[transition_name] == "transition1"
+
+ self.state.delete_transition(transition_name)
+ assert len(self.state.get_transition_dict()) == 0
+
+ def test_delete_all_dummy_transitions(self):
+ transition_name = self.state.add_transition("transition1")
+ assert len(self.state.get_transition_dict()) == 1
+ assert self.state.get_transition_dict().has_key(transition_name)
+ assert self.state.get_transition_dict()[transition_name] == "transition1"
+
+ self.state.delete_transitions()
+ assert len(self.state.get_transition_dict()) == 0
+
+
+
+ ######################### Limit cases ###################################
+ #### Action
+
+ #### Transition
+
+ ######################### Error cases ###################################
+ #### Action
+ def test_update_unknown_action(self):
+ name_error = None
+ try:
+ self.state.update_action("unknown_name", "action")
+ except NameError, e:
+ name_error = e
+
+ assert name_error
+
+
+ def test_delete_unknown_action(self):
+ name_error = None
+ try:
+ self.state.delete_action("unknown_name")
+ except NameError, e:
+ name_error = e
+
+ assert name_error
+
+ #### Transition
+ def test_add_existing_transition(self):
+ self.state.add_transition("transition")
+ transition_exists_error = None
+ try:
+ self.state.add_transition("transition")
+ except TransitionAlreadyExists, e:
+ transition_exists_error = e
+
+ assert transition_exists_error
+
+
+class TutorialTest(unittest.TestCase):
+ """Test basic functionalities of tutorials"""
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ ######################### Basic interface cases #########################
+
+ ######################### Limit cases ###################################
+
+ ######################### Error cases ###################################
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py
new file mode 100644
index 0000000..a4da092
--- /dev/null
+++ b/tutorius/tutorial.py
@@ -0,0 +1,467 @@
+
+class Tutorial(object):
+ """ This class replaces the previous Tutorial class and
+ allows manipulation of the abstract representation
+ of a tutorial as a state machine
+ """
+
+ _INIT = "INIT"
+ _END = "END"
+
+ def __init__(self, name, state_dict=None):
+ """
+ The constructor for the Tutorial. By default, the tutorial contains
+ only an initial state and an end state.
+ The initial state doesn't contain any action or transition.
+ The end state doesn't contain any action either.
+
+ If state_dict is provided, a valid initial state and an end state
+ must be provided.
+
+ @param name The name of the tutorial
+ @param state_dict optional, a valid dictionary of states
+ """
+ self.name = name
+
+ # We will use an adjacency list representation through the
+ # usage of state objects because our graph representation
+ # is really sparse and mostly linear, for a brief
+ # example of graph programming in python see:
+ # http://www.python.org/doc/essays/graphs
+ self._state_dict = state_dict or \
+ {Tutorial._INIT:State(name=Tutorial._INIT),\
+ Tutorial._END:State(name=Tutorial._END)}
+
+ # Minimally check for the presence of an INIT and an END
+ # state
+ if not self._state_dict.has_key(Tutorial._INIT):
+ raise Exception("No INIT state found in state_dict")
+
+ if not self._state_dict.has_key(Tutorial._END):
+ raise Exception("No END state found in state_dict")
+
+ self.validate()
+
+ # Initialize variables for generating unique names
+ # TODO: We should take the max number from the
+ # existing state names
+ self._state_name_nb = 0
+
+
+ def add_state(self, action_list=[], transition_list=[]):
+ """
+ Add a new state to the state machine. The state is
+ initialized with the action list and transition list
+ and a new unique name is returned for this state.
+
+ The action is added using add_action.
+
+ The transitions is added using add_transition.
+
+ @param action_list The list of valid actions for this state
+ @param transition_list The list of valid transitions
+ @return string unique name for this state
+ """
+ name = self._generate_unique_state_name()
+
+ for action in action_list:
+ self._validate_action(action)
+
+ for transition in transition_list:
+ self._validate_transition(transition)
+
+ state = State(name, action_list, transition_list)
+
+ if self._state_dict.has_key(name):
+ raise Exception("Name: " + name + " already exists, could not\
+ add a new state.")
+
+ self._state_dict[name] = state
+
+ return name
+
+
+ def add_action(self, state_name, action):
+ """
+ Add an action to a specific state. A unique name for this
+ tutorial is generated to refer precisely to this action
+ and is returned.
+
+ The action is validated.
+
+ @param state_name The name of the state to add an action to
+ @param action The action to be added
+ @return unique name for this action
+ """
+ return "State/Action"
+
+ def add_transition(self, state_name, transition):
+ """
+ Add a transition to a specific state. A unique name for this
+ tutorial is generated to refer precisely to this transition
+ and is returned. Inserting a duplicate transition will raise
+ an exception.
+
+ The transition is validated.
+
+ @param state_name The name of the state to add a transition to
+ @param transition The transition to be added
+ @return unique name for this action
+ @raise TransitionAlreadyExists
+ """
+ return "State/Transition"
+
+ def update_action(self, action_name, action):
+ """
+ Update the properties of a specific action with a copy of the
+ properties of the action passed in.
+
+ The action is validated.
+
+ @param action_name The name of the action to update
+ @param action An action with the properties to copy from
+ @return action_name if the update was successful, False otherwise
+ """
+ return action_name
+
+ def update_transition(self, transition_name, transition):
+ """
+ Update the properties of a specific transition with a copy of the
+ properties of the transition passed in.
+
+ The transition is validated.
+
+ @param transition_name The name of the transition to update
+ @param transition An transition with the properties to copy from
+ @return transition_name if the update was successful, False otherwise
+ """
+ return transition_name
+
+ def delete_action(self, action_name):
+ """
+ Delete the action identified by action_name.
+
+ @param action_name The name of the action to be deleted
+ @return the action that has been deleted
+ """
+ return None
+
+ def delete_transition(self, transition_name):
+ """
+ Delete the transition identified by transition_name.
+
+ @param transition_name The name of the transition to be deleted
+ @return the transition that has been deleted
+ """
+ return None
+
+ def delete_state(self, state_name):
+ """
+ Delete the state, delete all the actions and transitions
+ in this state, update the transitions from the state that
+ pointed to this one to the next state and remove all the
+ unreachable states recursively.
+
+ @param state_name The name of the state to remove
+ """
+ pass
+
+ def get_actions(self, state_name):
+ """
+ @param state_name The name of the state to list actions from
+ @return A list of actions for state_name
+ """
+ pass
+
+ def get_events(self, state_name):
+ """
+ @param state_name The name of the state to list actions from
+ @return A list of events for state_name
+ """
+ pass
+
+ 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
+ """
+ pass
+
+
+ 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.
+ @raise KeyError When there is no state by this name in the FSM
+ """
+ pass
+
+ def _validate_action(self, action):
+ """
+ Validate that an action conforms to what we expect,
+ throws an exception otherwise.
+
+ @param action The action to validate
+ @except InvalidAction if the action fails to conform to what we expect
+ """
+ pass
+
+ def _validate_action_name(self, action_name):
+ """
+ Check if action_name exists.
+
+ @param action_name The name to check
+ @except UnknownName if the name is not present in the Tutorial
+ """
+ pass
+
+ def _validate_transition(self, transition):
+ """
+ Validate that a transition conforms to what we expect,
+ throws an exception otherwise.
+
+ @param transition The transition to validate
+ @except InvalidTransition if the transition fails to conform to what we expect
+ """
+ pass
+
+ def _validate_transition_name(self, transition_name):
+ """
+ Check if transition_name exists.
+
+ @param transition_name The name to check
+ @except UnknownName if the name is not present in the Tutorial
+ """
+ pass
+
+ def validate(self):
+ """
+ Validate the state machine for a serie of properties:
+ 1. No unreachable states
+ 2. No dead end state (except END)
+ 3. No branching in the main path
+ 4. No loop in the main path
+ 5. ...
+
+ Throw an exception for the first condition that is not met.
+ """
+ pass
+
+ def _generate_unique_state_name(self):
+ name = "State" + str(self._state_name_nb)
+ self._state_name_nb += 1
+ return name
+
+ def __str__(self):
+ """
+ Return a string representation of the tutorial
+ """
+ return ""
+
+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 transitions to lead
+ to next states.
+
+ This class is not meant to be used explicitly as no validation is done on
+ inputs, the validation should be done by the containing class.
+ """
+
+ def __init__(self, name="", action_list=[], transition_list=[]):
+ """
+ Initializes the content of the state, such as loading the actions
+ that are required and building the correct transitions.
+
+ @param action_list The list of actions to execute when entering this
+ state
+ @param transition_list A list of tuples of the form
+ (event, next_state_name), that explains the outgoing links for
+ this state
+ """
+ object.__init__(self)
+
+ self.name = name
+
+ self._actions = {}
+ for action in action_list:
+ self._actions[self._generate_unique_action_name(action)] = action
+
+ self._transitions = {}
+ for transition in transition_list:
+ self._transitions[self._generate_unique_transition_name(transition)] = transition
+
+ # Initialize internal variables for name generation
+ self.action_name_nb = 0
+ self.transition_name_nb = 0
+
+ # Action manipulations
+ def add_action(self, new_action):
+ """
+ Adds an action to the state
+
+ @param new_action The action to add
+ @return a unique name for this action
+ """
+ action_name = self._generate_unique_action_name(new_action)
+ self._actions[action_name] = new_action
+ return action_name
+
+ def delete_action(self, action_name):
+ """
+ Delete the action with the name action_name
+
+ @param action_name The name of the action to delete
+ @return The action deleted
+ @raise NameError if action_name doesn't exist
+ """
+ if self._actions.has_key(action_name):
+ return self._actions.pop(action_name)
+ else:
+ raise NameError("Tutorial.State: action '" + action_name + "' is not defined")
+
+ def update_action(self, action_name, new_action):
+ """
+ Replace the action with action_name by new_action
+
+ @param action_name The name of the action to replace
+ @param new_action The action that will replace the old one
+ @return The replaced action
+ @raise NameError if action_name doesn't exist
+ """
+ # TODO: For now let's just replace the action with a new one,
+ # we should check to see if we need a replace or an update
+ # semantic for this update method
+ if self._actions.has_key(action_name):
+ old_action = self._actions.pop(action_name)
+ self._actions[action_name] = new_action
+ return old_action
+ else:
+ raise NameError("Tutorial.State: action '" + action_name + "' is not defined")
+
+ def get_action_dict(self):
+ """
+ @return A dictionary of actions that the state will execute
+ """
+ return self._actions
+
+ def delete_actions(self):
+ """
+ Removes all the action associated with this state. A cleared state will
+ not do anything when entered or exited.
+ """
+ self._actions = {}
+
+ # Transition manipulations
+ def add_transition(self, new_transition):
+ """
+ Adds a transition from this state to another state.
+
+ The same transition may not be added twice.
+
+ @param transition The new transition.
+ @return A unique name for the transition
+ @raise TransitionAlreadyExists if an equivalent transition exists
+ """
+ for transition in self._transitions.itervalues():
+ if transition == new_transition:
+ raise TransitionAlreadyExists(str(transition))
+
+ transition_name = self._generate_unique_transition_name(new_transition)
+ self._transitions[transition_name] = new_transition
+ return transition_name
+
+ def update_transition(self, transition_name, new_transition):
+ """
+ Replace the transition with transition_name by new_transition
+
+ @param transition_name The name of the transition to replace
+ @param new_transition The transition that will replace the old one
+ @return The replaced transition
+ @raise NameError if transition_name doesn't exist
+ """
+ # TODO: For now let's just replace the transition with a new one,
+ # we should check to see if we need a replace or an update
+ # semantic for this update method
+ if self._transitions.has_key(transition_name):
+ old_transition = self._transitions.pop(transition_name)
+ self._transitions[transition_name] = new_transition
+ return old_transition
+ else:
+ raise NameError("Tutorial.State: transition '" + transition_name + "' is not defined")
+
+ def delete_transition(self, transition_name):
+ """
+ Delete the transition with the name transition_name
+
+ @param transition_name The name of the transition to delete
+ @return The transition deleted
+ @raise NameError if transition_name doesn't exist
+ """
+ if self._transitions.has_key(transition_name):
+ return self._transitions.pop(transition_name)
+ else:
+ raise NameError("Tutorial.State: transition '" + transition_name + "' is not defined")
+
+ def get_transition_dict(self):
+ """
+ @return The dictionary of transitions associated with this state.
+ """
+ return self._transitions
+
+ def delete_transitions(self):
+ """
+ Delete all the transitions associated with this state.
+ """
+ self._transitions = {}
+
+ def _generate_unique_action_name(self, action):
+ """
+ Returns a unique name for the action in this state,
+ the actual content of the name should not be relied upon
+ for correct behavior
+
+ @param action The action to generate a name for
+ @return A name garanteed to be unique within this state
+ """
+ #TODO use the action class name to generate a name
+ # to make it easier to debug and know what we are
+ # manipulating
+ name = "action" + str(self.action_name_nb)
+ self.action_name_nb += 1
+ return name
+
+ def _generate_unique_transition_name(self, transition):
+ """
+ Returns a unique name for the transition in this state,
+ the actual content of the name should not be relied upon
+ for correct behavior
+
+ @param transition The transition to generate a name for
+ @return A name garanteed to be unique within this state
+ """
+ #TODO use the event class name from the transition to
+ # generate a name to make it easier to debug and know
+ # what we are manipulating
+ name = "transition" + str(self.transition_name_nb)
+ self.transition_name_nb += 1
+ return name
+
+
+
+################## Error Handling and Exceptions ##############################
+
+class TransitionAlreadyExists(Exception):
+ """
+ Raised when a duplicate transition is added to a state
+ """
+ pass
+
+