# 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): 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()