# 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 from copy import deepcopy import logging from sugar.tutorius.actions import * from sugar.tutorius.addon import * from sugar.tutorius.core import * from sugar.tutorius.filters import * from actiontests import CountAction, FakeEventFilter # 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 TutorialTest(unittest.TestCase): """Tests the tutorial functions that are not covered elsewhere.""" def test_detach(self): class Activity(object): name = "this" activity1 = Activity() activity2 = Activity() fsm = FiniteStateMachine("Sample example") tutorial = Tutorial("Test tutorial", fsm) assert tutorial.activity == None, "There is a default activity in the tutorial" tutorial.attach(activity1) assert tutorial.activity == activity1, "Activity should have been associated to this tutorial" tutorial.attach(activity2) assert tutorial.activity == activity2, "Activity should have been changed to activity2" 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 # 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 = addon.create('TriggerEventFilter') state = State("event_test", event_filter_list=[(event_filter, "second_state")]) 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" # 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 = addon.create('TriggerEventFilter') event2 = addon.create('TriggerEventFilter') # Insert the event filters assert state.add_event_filter(event1, "s"), "Could not add event filter 1" # Make sure we cannot insert an event twice assert state.add_event_filter(event1, "s") == False, "Could add twice the event filter" assert state.add_event_filter(event2, "t") == False, "Could add event filter 2" # Get the list of event filters event_filters = map(lambda x: x[0],state.get_event_filter_list()) #even if we added only the event 1, they are equivalent assert event1 in event_filters and event2 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" def test_eq_simple(self): """ Two empty states with the same name must be identical """ st1 = State("Identical") st2 = State("Identical") assert st1 == st2, "Empty states with the same name should be identical" def test_eq(self): """ Test whether two states share the same set of actions and event filters. """ st1 = State("Identical") st2 = State("Identical") non_state = object() act1 = addon.create("BubbleMessage", message="Hi", position=[132,450]) act2 = addon.create("BubbleMessage", message="Hi", position=[132,450]) event1 = addon.create("GtkWidgetEventFilter", "0.0.0.1.1.2.3.1", "clicked") act3 = addon.create("DialogMessage", message="Hello again.", position=[200, 400]) # Build the first state st1.add_action(act1) st1.add_action(act3) st1.add_event_filter(event1, "nextState") # Build the second state st2.add_action(act2) st2.add_action(act3) st2.add_event_filter(event1, "nextState") # Make sure that they are identical for now assert st1 == st2, "States should be considered as identical" assert st2 == st1, "States should be considered as identical" # Modify the second bubble message action act2.message = "New message" # Since one action changed in the second state, this should indicate that the states # are not identical anymore assert not (st1 == st2), "Action was changed and states should be different" assert not (st2 == st1), "Action was changed and states should be different" # Make sure that trying to find identity with something else than a State object fails properly assert not (st1 == non_state), "Passing a non-State object should fail for identity" st2.name = "Not identical anymore" assert not(st1 == st2), "Different state names should give different states" st2.name = "Identical" st3 = deepcopy(st1) st3.add_action(addon.create("BubbleMessage", "Hi!", [128,264])) assert not (st1 == st3), "States having a different number of actions should be different" st4 = deepcopy(st1) st4.add_event_filter(addon.create("GtkWidgetEventFilter", "0.0.1.1.2.2.3", "clicked"), "next_state") assert not (st1 == st4), "States having a different number of events should be different" st5 = deepcopy(st1) st5._event_filters = [] st5.add_event_filter(addon.create("GtkWidgetEventFilter", "0.1.2.3.4.1.2", "pressed"), "other_state") assert not (st1 == st5), "States having the same number of event filters" \ + " but those being different should be different" 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() content = { "INIT": State("INIT", action_list=[act_init],event_filter_list=[(event_init,"SECOND")]), "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=[(addon.create('TriggerEventFilter'), "second")]) st2 = State("second", event_filter_list=[(addon.create('TriggerEventFilter'), "third")]) st3 = State("third", event_filter_list=[(addon.create('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 def test_setup(self): fsm = FiniteStateMachine("New state machine") try: fsm.setup() assert False, "fsm should throw an exception when trying to setup and not bound to a tutorial" except UnboundLocalError: pass def test_setup_actions(self): tut = SimpleTutorial() states_dict = {"INIT": State("INIT")} fsm = FiniteStateMachine("New FSM", state_dict=states_dict) act = CountAction() fsm.add_action(act) fsm.set_tutorial(tut) fsm.setup() # Let's also test the current state name assert fsm.get_current_state_name() == "INIT", "Initial state should be INIT" assert act.do_count == 1, "Action should have been called during setup" fsm._fsm_has_finished = True fsm.teardown() assert act.undo_count == 1, "Action should have been undone" def test_string_rep(self): fsm = FiniteStateMachine("Testing machine") st1 = State("INIT") st2 = State("Other State") st3 = State("Final State") st1.add_action(addon.create("BubbleMessage", "Hi!", [132,312])) fsm.add_state(st1) fsm.add_state(st2) fsm.add_state(st3) assert str(fsm) == "INIT, Final State, Other State, " def test_eq_(self): fsm = FiniteStateMachine("Identity test") non_fsm_object = object() assert not (fsm == non_fsm_object), "Testing with non FSM object should not give identity" # Compare FSMs act1 = CountAction() fsm.add_action(act1) fsm2 = deepcopy(fsm) assert fsm == fsm2 act2 = CountAction() fsm2.add_action(act2) assert not(fsm == fsm2), \ "FSMs having a different number of actions should be different" fsm3 = FiniteStateMachine("Identity test") act3 = addon.create("BubbleMessage", "Hi!", [123,312]) fsm3.add_action(act3) assert not(fsm3 == fsm), \ "Actions having the same number of actions but different ones should be different" st1 = State("INIT") st2 = State("OtherState") fsm.add_state(st1) fsm.add_state(st2) fsm4 = deepcopy(fsm) assert fsm == fsm4 st3 = State("Last State") fsm4.add_state(st3) assert not (fsm == fsm4), "FSMs having a different number of states should not be identical" fsm4.remove_state("OtherState") assert not (fsm == fsm4), "FSMs having different states should be different" fsm4.remove_state("Last State") st5 = State("OtherState") st5.add_action(CountAction()) fsm4.add_state(st5) assert not(fsm == fsm4), "FSMs having states with same name but different content should be different" 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(addon.create('TriggerEventFilter'), "Second") st1.add_event_filter(addon.create('TriggerEventFilter'), "Third") st2 = State("Second") st2.add_action(TrueWhileActiveAction()) st2.add_event_filter(addon.create('TriggerEventFilter'), "Third") st2.add_event_filter(addon.create('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()