From b1ff3d93cd809ce434f911ad004746d8c5998df5 Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 24 Nov 2009 03:08:50 +0000 Subject: LP 448319 : Adding engine tests, fixing probe tests, correcting bugs found with tests --- diff --git a/tests/enginetests.py b/tests/enginetests.py index a035c2f..ca86c9f 100644 --- a/tests/enginetests.py +++ b/tests/enginetests.py @@ -25,18 +25,74 @@ and event filters. Those are in their separate test module """ import unittest +from functools import partial +from uuid import uuid1 from sugar.tutorius.tutorial import Tutorial from sugar.tutorius.engine import TutorialRunner +import sugar.tutorius.engine as engine +from sugar.tutorius.actions import Action from sugar.tutorius.filters import EventFilter from actiontests import CountAction +class MockProbeMgrMultiAddons(object): + def __init__(self): + self.action_dict = {} + self.event_dict = {} + self.event_cb_dict = {} + + self._action_installed_cb_list = [] + self._install_error_cb_list = [] + self._event_subscribed_cb_list = [] + self._subscribe_error_cb_list = [] + + currentActivity = property(fget=lambda s:s, fset=lambda s, v: v) + + def run_install_cb(self, action_number, action): + self._action_installed_cb_list[action_number](action, str(uuid1())) + + def run_install_error_cb(self, action_number): + self._install_error_cb_list[action_number](Exception("Could not install action...")) + + def run_subscribe_cb(self, event_number): + self._event_subscribed_cb_list[event_number](str(uuid1())) + + def run_subscribe_error(self, event_number): + self._subscribe_error_cb_list[event_number](str(uuid1())) + + def install(self, action, action_installed_cb, error_cb): + action_address = str(uuid1()) + self.action_dict[action_address] = action + self._action_installed_cb_list.append(action_installed_cb) + self._install_error_cb_list.append(error_cb) + + def update(self, action_address, new_action): + self.action_dict[action_address] = new_action + + def uninstall(self, action_address): + del self.action_dict[action_address] + + def subscribe(self, event_name, event, notif_cb, subscribe_cb, error_cb): + event_address = str(uuid1()) + self.event_dict[event_name] = event_address + self.event_cb_dict[event_name] = notif_cb + self._event_subscribed_cb_list.append(subscribe_cb) + self._subscribe_error_cb_list.append(error_cb) + + def unsubscribe(self, address): + for (event_name, other_event) in self.event_dict.values(): + if event == othet_event: + del self.event_dict[event_name] + break class MockProbeMgr(object): def __init__(self): self.action = None self.event = None self.cB = None + + self._action_installed_cb = None + self._install_error_cb = None def doCB(self): self.cB(self.event) @@ -45,7 +101,8 @@ class MockProbeMgr(object): def install(self, action, action_installed_cb, error_cb): self.action = action - action_installed_cb(action, 'Action1') + self._action_installed_cb = partial(action_installed_cb, action) + self._install_error_cb = partial(error_cb, action) def update(self, action_address, newaction): self.action = newaction @@ -53,11 +110,11 @@ class MockProbeMgr(object): def uninstall(self, action_address): self.action = None - def subscribe(self, event, notif_cb, event_sub_cb, error_cb): + def subscribe(self, event_name, event, notif_cb, event_sub_cb, error_cb): self.event = event self.cB = notif_cb self.event.install_handlers(notif_cb) - # Trigger the installation callback + # Save the callbacks for this action self.event_sub_cB = event_sub_cb self._subscribe_error_cb = error_cb return str(event) @@ -67,9 +124,105 @@ class MockProbeMgr(object): class MockEvent(EventFilter): pass + +class TestRunnerStates(unittest.TestCase): + def setUp(self): + self.pM = MockProbeMgr() + self.tutorial = Tutorial("TutorialRunner") + self.state_name = self.tutorial.add_state() + self.tutorial.update_transition(Tutorial.INITIAL_TRANSITION_NAME, + None, self.state_name) + self.action = CountAction() + self.tutorial.add_action(self.state_name, self.action) + self.event = MockEvent() + self.tutorial.add_transition(self.state_name, (self.event, Tutorial.END)) + + self.runner = TutorialRunner(self.tutorial, self.pM) + + def test_setup_states(self): + assert self.runner._runner_state == engine.RUNNER_STATE_IDLE, "Idle should be the initial state for the runner" + + self.runner.start() + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Setup Actions State should be entered after start" + self.pM._action_installed_cb('action1') + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "State should be Setup Events after all actions are installed" + + self.pM.event_sub_cB('event1') + + assert self.runner._runner_state == engine.RUNNER_STATE_AWAITING_NOTIFICATIONS, "State should be Awaiting Notifications once all events are installed" + + def test_setup_actions_errors(self): + self.runner.start() + + self.pM._install_error_cb(Exception("Fake Exception")) + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Setup Events should be reached after error on action installation" + + self.pM._subscribe_error_cb(Exception("Fake Exception")) + + assert self.runner._runner_state == engine.RUNNER_STATE_AWAITING_NOTIFICATIONS, "State Awaiting Notifications should be reached after event subscribe error" + + def test_stop_in_actions(self): + self.runner.start() + + self.runner.stop() + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Stop state should not be reached" + + self.pM._action_installed_cb('action1') + + assert self.runner._runner_state == engine.RUNNER_STATE_STOPPED + + def test_stop_in_events(self): + self.runner.start() + self.pM._action_installed_cb('action1') + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Setup events state should be reached after all actions installed" + + self.runner.stop() + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Tutorial should not be stopped until all events have been confirmed" + self.pM.event_sub_cB('event1') + + assert self.runner._runner_state == engine.RUNNER_STATE_STOPPED, "Tutorial should have been stopped right after the last event was confirmed" + +class TestInstallationStates(unittest.TestCase): + def setUp(self): + self.pM = MockProbeMgrMultiAddons() + self.tutorial = Tutorial("TutorialRunner") + self.state_name = self.tutorial.add_state() + self.tutorial.update_transition(Tutorial.INITIAL_TRANSITION_NAME, + None, self.state_name) + #import rpdb2; rpdb2.start_embedded_debugger('pass') + self.action1 = CountAction() + self.tutorial.add_action(self.state_name, self.action1) + self.action2 = CountAction() + self.tutorial.add_action(self.state_name, self.action2) + + self.event = MockEvent() + self.tutorial.add_transition(self.state_name, (self.event, Tutorial.END)) + self.event2 = MockEvent() + self.tutorial.add_transition(self.state_name, (self.event2, Tutorial.INIT)) + + self.runner = TutorialRunner(self.tutorial, self.pM) + + def test_multiple_actions(self): + self.runner.start() + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Runner should be in Setup Actions state" + + self.pM.run_install_cb(1, self.action2) + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_ACTIONS, "Runner should still be in Setup Actions state after a single action confirmation callback" + + self.pM.run_install_cb(0, self.action1) + + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Runner should be in Setup Events state after all actions are installed" + self.pM.run_subscribe_cb(1) + assert self.runner._runner_state == engine.RUNNER_STATE_SETUP_EVENTS, "Runner should still be in Setup Events state when not all event installations are confirmed" - class TutorialRunnerTest(unittest.TestCase): """ This class needs to test the TutorialRunner @@ -94,13 +247,13 @@ class TutorialRunnerTest(unittest.TestCase): runner.start() self.pM.event_sub_cB('event1') - assert runner._state == state_name, "Current state is: %s"%runner._state + assert runner._state == state_name, "Current tutorial state is: %s"%runner._state assert self.pM.action == None assert self.pM.event == event event.do_callback() - assert runner._state == Tutorial.END, "Current state is: %s"%runner._state + assert runner._state == Tutorial.END, "Current tutorial state is: %s"%runner._state assert self.pM.action == None, "Current action is %s"%str(self.pM.action) assert self.pM.event == None, "Current event is %s"%str(self.pM.event) diff --git a/tests/probetests.py b/tests/probetests.py index bdb9bc9..d43da8e 100644 --- a/tests/probetests.py +++ b/tests/probetests.py @@ -85,6 +85,7 @@ class MockProbeProxy(object): @param activityName unique activity id. Must be a valid dbus bus name. """ self.MockAction = None + self.MockActionName = None self.MockActionUpdate = None self.MockEvent = None self.MockCB = None @@ -95,9 +96,10 @@ class MockProbeProxy(object): def isAlive(self): return self.MockAlive - def install(self, action, callback, block=False): + def install(self, action, action_installed_cb, error_cb): self.MockAction = action - self.MockAddressCallback_install = callback + self.MockAddressCallback_install = action_installed_cb + self.MockInstallErrorCallback = error_cb self.MockActionUpdate = None return None @@ -106,21 +108,21 @@ class MockProbeProxy(object): self.MockActionUpdate = newaction return None - def uninstall(self, action_address, block=False): + def uninstall(self, action_address): self.MockAction = None self.MockActionUpdate = None return None - def subscribe(self, event, callback, block=True): + def subscribe(self, event_name, event, notif_cb, subscribe_cb, error_cb): #Do like the current Probe - if not block: - raise RuntimeError("This function does not allow non-blocking mode yet") - + self.MockEventName = event_name self.MockEvent = event - self.MockCB = callback + self.MockCB = notif_cb + self.MockSubscribeCB = subscribe_cb + self.MockSubscriptionErrorCb = error_cb return str(id(event)) - def unsubscribe(self, address, block=True): + def unsubscribe(self, address): self.MockEventAddr = address return None @@ -348,16 +350,18 @@ class ProbeManagerTest(unittest.TestCase): ad1_address = "Address1" def callback(value): pass + def error_cb(): + pass #ErrorCase: install, update, uninstall without currentActivity #Action functions should do a warning if there is no activity - self.assertRaises(RuntimeWarning, self.probeManager.install, ad1, callback) + self.assertRaises(RuntimeWarning, self.probeManager.install, ad1_address, ad1, callback) self.assertRaises(RuntimeWarning, self.probeManager.update, ad1_address, ad1) self.assertRaises(RuntimeWarning, self.probeManager.uninstall, ad1_address) assert act1.MockAction is None, "Action should not be installed on inactive proxy" assert act2.MockAction is None, "Action should not be installed on inactive proxy" self.probeManager.currentActivity = "act1" - self.probeManager.install(ad1, callback) + self.probeManager.install(ad1, callback, error_cb) assert act1.MockAction == ad1, "Action should have been installed" assert act2.MockAction is None, "Action should not be installed on inactive proxy" @@ -379,22 +383,26 @@ class ProbeManagerTest(unittest.TestCase): act1 = self.probeManager.get_registered_probes_list("act1")[0][1] act2 = self.probeManager.get_registered_probes_list("act2")[0][1] + event_name1 = 'State0/event0' + event_name2 = 'State0/event1' ad1 = MockAddon() ad2 = MockAddon() ad2.i, ad2.s = (2, "test2") cb1 = lambda *args: None + install_cb1 = lambda *args:None + error_cb1 = lambda *args:None cb2 = lambda *args: None #ErrorCase: unsubscribe and subscribe without current activity #Event functions should do a warning if there is no activity - self.assertRaises(RuntimeWarning, self.probeManager.subscribe, ad1, cb1) + self.assertRaises(RuntimeWarning, self.probeManager.subscribe, event_name1, ad1, cb1, install_cb1, error_cb1) self.assertRaises(RuntimeWarning, self.probeManager.unsubscribe, None) assert act1.MockEvent is None, "No event should be on act1" assert act2.MockEvent is None, "No event should be on act2" self.probeManager.currentActivity = "act1" - self.probeManager.subscribe(ad1, cb1) + self.probeManager.subscribe(event_name1, ad1, cb1, install_cb1, error_cb1) assert act1.MockEvent == ad1, "Event should have been installed" assert act1.MockCB == cb1, "Callback should have been set" assert act2.MockEvent is None, "No event should be on act2" @@ -431,50 +439,58 @@ class ProbeProxyTest(unittest.TestCase): #Check if the installed action is the good one address = "Addr1" - def callback(value): + def action_installed_cb(value): + pass + def error_cb(value): pass #Set the return value of probe install self.mockObj.MockRet["install"] = address - self.probeProxy.install(action, callback, block=True) + self.probeProxy.install(action, action_installed_cb, error_cb) assert pickle.loads(self.mockObj.MockCall["install"]["args"][0]) == action, "1 argument, the action" + self.mockObj.MockCall["install"]["kwargs"]["reply_handler"](address) #ErrorCase: Update should fail on noninstalled actions - self.assertRaises(RuntimeWarning, self.probeProxy.update, action2_address, action2, block=True) + self.assertRaises(RuntimeWarning, self.probeProxy.update, action2_address, action2) #Test the update - self.probeProxy.update(address, action2, block=True) + self.probeProxy.update(address, action2) args = self.mockObj.MockCall["update"]["args"] assert args[0] == address, "arg 1 should be the action address" assert pickle.loads(args[1]) == action2._props, "arg2 should be the new action properties" #ErrorCase: Uninstall on not installed action (silent fail) #Test the uninstall - self.probeProxy.uninstall(action2_address, block=True) + self.probeProxy.uninstall(action2_address) assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed" - self.probeProxy.uninstall(address, block=True) + self.probeProxy.uninstall(address) assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address" def test_events(self): event = MockAddon() event.i, event.s = 5, "event" + event_address = 'event1' event2 = MockAddon() event2.i, event2.s = 10, "event2" def callback(event): global message_box message_box = event + subs_cb = lambda *args : None + error_cb = lambda *args : None #Check if the installed event is the good one address = "Addr1" #Set the return value of probe subscribe self.mockObj.MockRet["subscribe"] = address - self.probeProxy.subscribe(event, callback, block=True) + self.probeProxy.subscribe('State0/event0', event, callback, subs_cb, error_cb) + self.probeProxy._ProbeProxy__update_event('State0/event0', event, callback, subs_cb, event_address) assert pickle.loads(self.mockObj.MockCall["subscribe"]["args"][0]) == event, "1 argument, the event" #Call the callback with the event global message_box + #import rpdb2; rpdb2.start_embedded_debugger('pass') self.mockObj.MockCB["eventOccured"]["handler_function"](pickle.dumps(event)) assert message_box == event, "callback should have been called with event" message_box = None @@ -487,11 +503,11 @@ class ProbeProxyTest(unittest.TestCase): #ErrorCase: unsubcribe for non subscribed event #Test the unsubscribe - self.probeProxy.unsubscribe("otheraddress", block=True) + self.probeProxy.unsubscribe("otheraddress") assert not "unsubscribe" in self.mockObj.MockCall, "Unsubscribe should not be called if event is not subscribeed" - self.probeProxy.unsubscribe(address, block=True) - assert self.mockObj.MockCall["unsubscribe"]["args"][0] == address, "1 argument, the event address" + self.probeProxy.unsubscribe(event_address) + assert self.mockObj.MockCall["unsubscribe"]["args"][0] == event_address, "1 argument, the event address" #ErrorCase: eventOccured triggered by uninstalled event #Test the callback with unregistered event diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index e58dd03..02352af 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -361,7 +361,7 @@ class ProbeProxy: del self._actions[this_action] break - def __update_event(self, event, callback, event_subscribed_cb, address): + def __update_event(self, event_name, event, callback, event_subscribed_cb, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) # Since multiple callbacks could be associated to the same # event signature, we will store multiple callbacks @@ -410,7 +410,7 @@ class ProbeProxy: else: LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address) - def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + def subscribe(self, event_name, event, notification_cb, event_subscribed_cb, error_cb): """ Register an event listener @param event Event to listen for @@ -427,7 +427,7 @@ class ProbeProxy: # for event types and sources, we will need to revise the lookup # mecanism for which callback function to call self._probe.subscribe(pickle.dumps(event), - reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb), + reply_handler=save_args(self.__update_event, event_name, event, notification_cb, event_subscribed_cb), error_handler=save_args(error_cb, event)) def unsubscribe(self, address, block=True): @@ -528,7 +528,7 @@ class ProbeManager(object): else: raise RuntimeWarning("No activity attached") - def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + def subscribe(self, event_name, event, notification_cb, event_subscribed_cb, error_cb): """ Register an event listener @param event Event to listen for @@ -540,7 +540,7 @@ class ProbeManager(object): @return address identifier used for unsubscribing """ if self.currentActivity: - return self._first_proxy(self.currentActivity).subscribe(event, notification_cb,\ + return self._first_proxy(self.currentActivity).subscribe(event_name, event, notification_cb,\ event_subscribed_cb, error_cb) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/engine.py b/tutorius/engine.py index b8fe988..be0b935 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -37,6 +37,7 @@ RUNNER_STATE_SETUP_EVENTS = 2 RUNNER_STATE_AWAITING_NOTIFICATIONS = 3 RUNNER_STATE_UNINSTALLING_ACTIONS = 4 RUNNER_STATE_UNSUBSCRIBING_EVENTS = 5 +RUNNER_STATE_STOPPED = 6 LOGGER = logging.getLogger("sugar.tutorius.engine") @@ -58,6 +59,9 @@ class TutorialRunner(object): # The message queue is a heap, so only heap operations should be done # on it like heappush, heappop, etc... + # The stocked messages are actually a list of parameters that should be + # passed to the appropriate function. E.g. When raising an event notification, + # it saves the (next_state, event) in the message. self._message_queue = [] #State @@ -92,9 +96,8 @@ class TutorialRunner(object): def _execute_stop(self): self.setCurrentActivity() #Temp Hack until activity in events/actions - self.enterState(self._tutorial.END) - self._teardownState() self._state = None + self._runner_state = RUNNER_STATE_STOPPED def _handleEvent(self, next_state, event): # Look if we are actually receiving notifications @@ -114,7 +117,10 @@ class TutorialRunner(object): if self._state is None: #No state, no teardown return - + self._remove_installed_actions() + self._remove_subscribed_events() + + def _remove_installed_actions(self): #Clear the current actions for (action_name, action_address) in self._installed_actions.items(): LOGGER.debug("TutorialRunner :: Uninstalling action %s with address %s"%(action_name, action_address)) @@ -123,6 +129,7 @@ class TutorialRunner(object): self._installed_actions.clear() self._installation_errors.clear() + def _remove_subscribed_events(self): #Clear the EventFilters for (event_name, event) in self._subscribed_events.items(): self._pM.unsubscribe(event) @@ -137,7 +144,7 @@ class TutorialRunner(object): def __install_error(self, action_name, action, exception): # TODO : Fix this as it doesn't warn the user about the problem or anything - LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action) + str(exception))) + LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception))) self._installation_errors[action_name] = exception self.__verify_action_install_state() @@ -147,7 +154,7 @@ class TutorialRunner(object): install_complete = True for (this_action_name, this_action) in self._actions.items(): if not this_action_name in self._installed_actions.keys() and \ - not this_action in self._installation_errors.keys(): + not this_action_name in self._installation_errors.keys(): # There's at least one uninstalled action, so we still wait install_complete = False break @@ -166,8 +173,8 @@ class TutorialRunner(object): def __subscribe_error(self, event_name, exception): # TODO : Do correct error handling here - LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : "%(event_name, str(exception))) - self._subscribed_error[event_name] = exception + LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) + self._subscription_errors[event_name] = exception # Verify if we just completed the subscription of all the events for this state self.__verify_event_install_state() @@ -178,8 +185,8 @@ class TutorialRunner(object): # Check to see if we completed all the event subscriptions subscribe_complete = True for (this_event_name, (this_event, next_state)) in transitions.items(): - if not this_event in self._subscribed_events.keys() and \ - not this_event in self._subscription_errors.keys(): + if not this_event_name in self._subscribed_events.keys() and \ + not this_event_name in self._subscription_errors.keys(): subscribe_complete = False break @@ -206,16 +213,26 @@ class TutorialRunner(object): return # Send all the event registration - for (event, next_state) in transitions.values(): - self._pM.subscribe(event, + for (event_name, (event, next_state)) in transitions.items(): + self._pM.subscribe(event_name, event, save_args(self._handleEvent, next_state), - save_args(self.__event_subscribed, event), - save_args(self.__subscribe_error, event)) + save_args(self.__event_subscribed, event_name), + save_args(self.__subscribe_error, event_name)) def __all_events_subscribed(self): self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS self.__process_pending_messages() + def __uninstall_actions(self): + self._runner_state = RUNNER_STATE_UNINSTALLING_ACTIONS + self._remove_installed_actions() + self._execute_stop() + + def __unsubscribe_events(self): + self._runner_state = RUNNER_STATE_UNSUBSCRIBING_EVENTS + self._remove_subscribed_events() + self.__uninstall_actions() + def __process_pending_messages(self): while len(self._message_queue) != 0: (priority, message) = heappop(self._message_queue) @@ -224,13 +241,13 @@ class TutorialRunner(object): LOGGER.debug("TutorialRunner :: Stop message taken from message queue") # We can safely ignore the rest of the events self._message_queue = [] - self._execute_stop() + #self._execute_stop() # Start removing the installed addons - #if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS: - # # Start uninstalling the events - # self.__unsubscribe_events() - #if self._runner_state == RUNNER_STATE_SETUP_EVENTS: - # self.__uninstall_actions() + if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS: + # Start uninstalling the events + self.__unsubscribe_events() + if self._runner_state == RUNNER_STATE_SETUP_EVENTS: + self.__uninstall_actions() elif priority == EVENT_NOTIFICATION_MSG_PRIORITY: LOGGER.debug("TutorialRunner :: Handling stored event notification for next_state %s"%message[0]) self._handle_event(*message) diff --git a/tutorius/translator.py b/tutorius/translator.py index bfa1746..2265ebd 100644 --- a/tutorius/translator.py +++ b/tutorius/translator.py @@ -154,8 +154,8 @@ class ResourceTranslator(object): def detach(self, activity_id): self._probe_manager.detach(activity_id) - def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): - return self._probe_manager.subscribe(event, notification_cb, event_subscribed_cb, error_cb) + def subscribe(self, event_name, event, notification_cb, event_subscribed_cb, error_cb): + return self._probe_manager.subscribe(event_name, event, notification_cb, event_subscribed_cb, error_cb) def unsubscribe(self, address): return self._probe_manager.unsubscribe(address) -- cgit v0.9.1