diff options
-rw-r--r-- | tests/tutorialtests.py | 209 | ||||
-rw-r--r-- | tutorius/tutorial.py | 467 |
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 + + |