From 0c3f127c86af818d260966d2292b199757087157 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Sat, 11 Jul 2009 21:39:46 +0000 Subject: repackage --- (limited to 'tests/coretests.py') diff --git a/tests/coretests.py b/tests/coretests.py new file mode 100644 index 0000000..eadea01 --- /dev/null +++ b/tests/coretests.py @@ -0,0 +1,597 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm +# +# 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 logging +from sugar.tutorius.actions import Action, OnceWrapper, ClickAction, TypeTextAction +from sugar.tutorius.core import * +from sugar.tutorius.filters import * + + +from actiontests import CountAction + +# Helper classes to help testing +class SimpleTutorial(Tutorial): + """ + Fake tutorial + """ + def __init__(self, start_name="INIT"): + #Tutorial.__init__(self, "Simple Tutorial", None) + self.current_state_name = start_name + self.activity = "TODO : This should be an activity" + + def set_state(self, name): + self.current_state_name = name + +class TutorialWithFSM(Tutorial): + """ + Fake tutorial, but associated with a FSM. + """ + def __init__(self, start_name="INIT", fsm=None): + Tutorial.__init__(self, start_name, fsm) + self.activity = activity.Activity() + +class TrueWhileActiveAction(Action): + """ + This action's active member is set to True after a do and to False after + an undo. + + Used to verify that a State correctly triggers the do and undo actions. + """ + def __init__(self): + Action.__init__(self) + self.active = False + + def do(self): + self.active = True + + def undo(self): + self.active = False + +class ClickableWidget(): + """ + This class fakes a widget with a clicked() method + """ + def __init__(self): + self.click_count = 0 + + def clicked(self): + self.click_count += 1 + +class FakeTextEntry(): + """ + This class fakes a widget with an insert_text() method + """ + def __init__(self): + self.text_lines = [] + self.last_entered_line = "" + self.displayed_text = "" + + def insert_text(self, text, index): + self.last_entered_line = text + self.text_lines.append(text) + self.displayed_text = self.displayed_text[0:index] + text + self.displayed_text[index+1:] + +class FakeParentWidget(): + """ + This class fakes a widet container, it implements the get_children() method + """ + def __init__(self): + self._children = [] + + def add_child(self, child): + self._children.append(child) + + def get_children(self): + return self._children + + + + +class TriggerEventFilter(EventFilter): + """ + This event filter can be triggered by simply calling its do_callback function. + + Used to fake events and see the effect on the FSM. + """ + def __init__(self, next_state): + EventFilter.__init__(self, next_state) + self.toggle_on_callback = False + + def install_handlers(self, callback, **kwargs): + """ + Forsakes the incoming callback function and just set the inner one. + """ + self._callback = self._inner_cb + + 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 ClickActionTests(unittest.TestCase): + """ + Test class for click action + """ + def test_do_action(self): + activity = FakeParentWidget() + widget = ClickableWidget() + activity.add_child(widget) + ObjectStore().activity = activity + + action = ClickAction("0.0") + + assert widget == ObjectStore().activity.get_children()[0],\ + "The clickable widget isn't reachable from the object store \ + the test cannot pass" + + action.do() + + assert widget.click_count == 1, "clicked() should have been called by do()" + + action.do() + + assert widget.click_count == 2, "clicked() should have been called by do()" + + def test_undo(self): + activity = FakeParentWidget() + widget = ClickableWidget() + activity.add_child(widget) + ObjectStore().activity = activity + + action = ClickAction("0.0") + + assert widget == ObjectStore().activity.get_children()[0],\ + "The clickable widget isn't reachable from the object store \ + the test cannot pass" + + action.undo() + + #There is no undo for this action so the test should not fail + assert True + + + +class TypeTextActionTests(unittest.TestCase): + """ + Test class for type text action + """ + def test_do_action(self): + activity = FakeParentWidget() + widget = FakeTextEntry() + activity.add_child(widget) + ObjectStore().activity = activity + + test_text = "This is text" + + + action = TypeTextAction("0.0", test_text) + + assert widget == ObjectStore().activity.get_children()[0],\ + "The clickable widget isn't reachable from the object store \ + the test cannot pass" + + action.do() + + assert widget.last_entered_line == test_text, "insert_text() should have been called by do()" + + action.do() + + assert widget.last_entered_line == test_text, "insert_text() should have been called by do()" + assert len(widget.text_lines) == 2, "insert_text() should have been called twice" + + def test_undo(self): + activity = FakeParentWidget() + widget = FakeTextEntry() + activity.add_child(widget) + ObjectStore().activity = activity + + test_text = "This is text" + + + action = TypeTextAction("0.0", test_text) + + assert widget == ObjectStore().activity.get_children()[0],\ + "The clickable widget isn't reachable from the object store \ + the test cannot pass" + + action.undo() + + #There is no undo for this action so the test should not fail + assert True + +# State testing class +class StateTest(unittest.TestCase): + """ + This class has to test the State interface as well as the expected + functionality. + """ + + def test_action_toggle(self): + """ + Validate that the actions are properly done on setup and undone on + teardown. + + Pretty awesome. + """ + act = TrueWhileActiveAction() + + state = State("action_test", action_list=[act]) + + assert act.active == False, "Action is not initialized properly" + + state.setup() + + assert act.active == True, "Action was not triggered properly" + + state.teardown() + + assert act.active == False, "Action was not undone properly" + + def test_event_filter(self): + """ + Tests the fact that the event filters are correctly installed on setup + and uninstalled on teardown. + """ + event_filter = TriggerEventFilter("second_state") + + state = State("event_test", event_filter_list=[event_filter]) + state.set_tutorial(SimpleTutorial()) + + assert event_filter.toggle_on_callback == False, "Wrong init of event_filter" + assert event_filter._callback == None, "Event filter has a registered callback before installing handlers" + + state.setup() + + assert event_filter._callback != None, "Event filter did not register callback!" + + # 'Trigger' the event - This is more like a EventFilter test. + event_filter.do_callback() + + assert event_filter.toggle_on_callback == True, "Event filter did not execute callback" + + state.teardown() + + assert event_filter._callback == None, "Event filter did not remove callback properly" + + def test_warning_set_tutorial_twice(self): + """ + Calls set_tutorial twice and expects a warning on the second. + """ + state = State("start_state") + tut = SimpleTutorial("First") + tut2 = SimpleTutorial("Second") + + state.set_tutorial(tut) + + try: + state.set_tutorial(tut2) + 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" + + def test_set_same_state(self): + fsm = FiniteStateMachine("Set same state") + + st1 = State("INIT") + st1.add_action(CountAction()) + + fsm.add_state(st1) + + tut = SimpleTutorial() + + fsm.set_tutorial(tut) + + fsm.set_state("INIT") + + assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \ + "The action was not triggered on 'INIT'" + + fsm.set_state("INIT") + + do_count = fsm.get_state_by_name("INIT").get_action_list()[0].do_count + assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \ + "The action was triggered a second time, do_count = %d"%do_count + + undo_count = fsm.get_state_by_name("INIT").get_action_list()[0].undo_count + assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0,\ + "The action has been undone unappropriately, undo_count = %d"%undo_count + +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() -- cgit v0.9.1