From a188dac0527803edb46eabce04100f1c741a96f3 Mon Sep 17 00:00:00 2001 From: Simon Poirier Date: Sun, 12 Jul 2009 21:34:20 +0000 Subject: repackage of tutorius using distutils --- (limited to 'tests') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/__init__.py diff --git a/tests/actiontests.py b/tests/actiontests.py new file mode 100644 index 0000000..4e126b3 --- /dev/null +++ b/tests/actiontests.py @@ -0,0 +1,206 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm +# Copyright (C) 2009, Vincent Vinet +# +# 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 +""" +Action tests + +The behavior of the actions must be tested here. +""" + +import unittest +import gtk + +from sugar.tutorius import addon +from sugar.tutorius.actions import * +from sugar.tutorius.services import ObjectStore + +test_props = {"prop_a":8, "prop_b":3, "prop_c":"Hi"} + +class PropertyAction(Action): + prop_a = TIntProperty(test_props["prop_a"]) + prop_b = TIntProperty(test_props["prop_b"]) + prop_c = TStringProperty(test_props["prop_c"]) + def __init__(self, na): + Action.__init__(self) + +def has_function(obj, function_name): + """ + Checks whether the object has a function by that name. + """ + if hasattr(obj, function_name) and hasattr(obj.__getattribute__(function_name), "__call__"): + return True + return False + +class PropsTest(unittest.TestCase): + def test_get_properties(self): + act = PropertyAction(8) + + assert act.get_properties() == test_props.keys(), "Action does not contain property 'a'" + + for prop_name in act.get_properties(): + assert getattr(act, prop_name) == test_props[prop_name], "Wrong initial value for property %s : %s"%(prop_name,str(getattr(act, prop_name))) + +class DialogMessageTest(unittest.TestCase): + def setUp(self): + self.dial = addon.create('DialogMessage', "Message text", [200, 300]) + + def test_properties(self): + assert self.dial.message == "Message text", "Wrong start value for the message" + + assert self.dial.position == [200, 300], "Wrong start value for the position" + +class BubbleMessageTest(unittest.TestCase): + def setUp(self): + self.bubble = addon.create('BubbleMessage', message="Message text", pos=[200, 300], tailpos=[-15, -25]) + + def test_properties(self): + props = self.bubble.get_properties() + + assert "message" in props, 'No message property of BubbleMessage' + + assert "position" in props, 'No position property in BubbleMessage' + + assert "tail_pos" in props, 'No tail position property in BubbleMessage' + + +class CountAction(Action): + """ + This action counts how many times it's do and undo methods get called + """ + def __init__(self): + Action.__init__(self) + self.do_count = 0 + self.undo_count = 0 + + def do(self): + self.do_count += 1 + + def undo(self): + self.undo_count += 1 + + +class BaseActionTests(unittest.TestCase): + def test_do_unimplemented(self): + act = Action() + try: + act.do() + assert False, "do() should trigger a NotImplemented" + except NotImplementedError: + assert True, "do() should trigger a NotImplemented" + + def test_undo(self): + act = Action() + act.undo() + assert True, "undo() should never fail on the base action" + + +class OnceWrapperTests(unittest.TestCase): + def test_onceaction_toggle(self): + """ + Validate that the OnceWrapper wrapper works properly using the + CountAction + """ + act = CountAction() + wrap = OnceWrapper(act) + + assert act.do_count == 0, "do() should not have been called in __init__()" + assert act.undo_count == 0, "undo() should not have been called in __init__()" + + wrap.undo() + + assert act.undo_count == 0, "undo() should not be called if do() has not been called" + + wrap.do() + assert act.do_count == 1, "do() should have been called once" + + wrap.do() + assert act.do_count == 1, "do() should have been called only once" + + wrap.undo() + assert act.undo_count == 1, "undo() should have been called once" + + wrap.undo() + assert act.undo_count == 1, "undo() should have been called only once" + +class ChainTester(Action): + def __init__(self, witness): + Action.__init__(self) + self._witness = witness + + def do(self, **kwargs): + self._witness.append([self,"do"]) + + def undo(self): + self._witness.append([self,"undo"]) + +class ChainActionTest(unittest.TestCase): + """Tester for ChainAction""" + def test_empty(self): + """If the expected empty behavior (do nothing) changes + and starts throwing exceptions, this will flag it""" + a = ChainAction() + a.do() + a.undo() + + def test_order(self): + witness = [] + first = ChainTester(witness) + second = ChainTester(witness) + + c = ChainAction(first, second) + assert witness == [], "Actions should not be triggered on init""" + c.do() + + assert witness[0][0] is first, "First triggered action must be 'first'" + assert witness[0][1] is "do", "Action do() should be triggered" + + assert witness[1][0] is second, "second triggered action must be 'second'" + assert witness[1][1] is "do", "Action do() should be triggered" + + assert len(witness) is 2, "Two actions should give 2 do's" + + #empty the witness list + while len(witness): + rm = witness.pop() + + c.undo() + assert witness[1][0] is first, "second triggered action must be 'first'" + assert witness[1][1] is "undo", "Action undo() should be triggered" + + assert witness[0][0] is second, "first triggered action must be 'second'" + assert witness[0][1] is "undo", "Action undo() should be triggered" + + assert len(witness) is 2, "Two actions should give 2 undo's" + +class DisableWidgetActionTests(unittest.TestCase): + def test_disable(self): + btn = gtk.Button() + ObjectStore().activity = btn + btn.set_sensitive(True) + + assert btn.props.sensitive is True, "Callback should have been called" + + act = DisableWidgetAction("0") + assert btn.props.sensitive is True, "Callback should have been called again" + act.do() + assert btn.props.sensitive is False, "Callback should not have been called again" + act.undo() + assert btn.props.sensitive is True, "Callback should have been called again" + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/bundlertests.py b/tests/bundlertests.py new file mode 100644 index 0000000..8da2310 --- /dev/null +++ b/tests/bundlertests.py @@ -0,0 +1,65 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Charles-Etienne Carriere +# +# 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 +""" +Bundler tests + +This module contains all the tests for the storage mecanisms for tutorials +This mean testing savins and loading tutorial, .ini file management and +adding ressources to tutorial +""" + +import unittest +import os +import uuid + +from sugar.tutorius import bundler + +class TutorialBundlerTests(unittest.TestCase): + + def setUp(self): + + #generate a test GUID + self.test_guid = uuid.uuid1() + self.guid_path = os.path.join(bundler._get_store_root(),str(self.test_guid)) + os.mkdir(self.guid_path) + + self.ini_file = os.path.join(self.guid_path, "meta.ini") + + f = open(self.ini_file,'w') + f.write("[GENERAL_METADATA]") + f.write(os.linesep) + f.write("GUID:") + f.write(str(self.test_guid)) + f.close() + + def tearDown(self): + os.remove(self.ini_file) + os.rmdir(self.guid_path) + + def test_add_ressource(self): + bund = bundler.TutorialBundler(self.test_guid) + + temp_file = open("test.txt",'w') + temp_file.write('test') + temp_file.close() + + bund.add_resource("test.txt") + + assert os.path.exists(os.path.join(self.guid_path,"test.txt")), "add_ressource did not create the file" + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/constraintstests.py b/tests/constraintstests.py new file mode 100644 index 0000000..b7b0a47 --- /dev/null +++ b/tests/constraintstests.py @@ -0,0 +1,233 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 + +import unittest + +from sugar.tutorius.constraints import * + +class ConstraintTest(unittest.TestCase): + def test_base_class(self): + cons = Constraint() + try: + cons.validate(1) + assert False, "Base class should throw an assertion" + except NotImplementedError: + pass + +class ValueConstraintTest(unittest.TestCase): + def test_limit_set(self): + cons = ValueConstraint(12) + + assert cons.limit == 12 + +class UpperLimitConstraintTest(unittest.TestCase): + def test_empty_constraint(self): + cons = UpperLimitConstraint(None) + try: + cons.validate(20) + except UpperLimitConstraintError: + assert False, "Empty contraint should not raise an exception" + + def test_validate(self): + cons = UpperLimitConstraint(10) + + try: + cons.validate(20) + assert False, "Validation of UpperLimit(10) on 20 should raise an exception" + except UpperLimitConstraintError: + pass + + try: + cons.validate(5) + except UpperLimitConstraintError: + assert True, "Validation of UpperLimit(10) on 5 should not raise an exception" + +class LowerLimitConstraintTest(unittest.TestCase): + def test_empty_constraint(self): + cons = LowerLimitConstraint(None) + try: + cons.validate(20) + except LowerLimitConstraintError: + assert False, "Empty contraint should not raise an exception" + + def test_validate(self): + cons = LowerLimitConstraint(10) + + try: + cons.validate(5) + assert False, "Validation of LowerLimit(10) on 5 should raise an exception" + except LowerLimitConstraintError: + pass + + try: + cons.validate(20) + except LowerLimitConstraintError: + assert True, "Validation of LowerLimit(10) on 20 should not raise an exception" + +class MaxSizeConstraintTest(unittest.TestCase): + def test_empty_constraint(self): + cons = MaxSizeConstraint(None) + try: + cons.validate(20) + except MaxSizeConstraintError: + assert False, "Empty contraint should not raise an exception" + + def test_validate(self): + cons = MaxSizeConstraint(10) + + try: + cons.validate(range(0, 20)) + assert False, "Validation of MaxSizeConstraint(10) on list of length 20 should raise an exception" + except MaxSizeConstraintError: + pass + + try: + cons.validate(range(0,5)) + except MaxSizeConstraintError: + assert True, "Validation of MaxSizeConstraint(10) on list of length 5 should not raise an exception" + +class MinSizeConstraintTest(unittest.TestCase): + def test_empty_constraint(self): + cons = MinSizeConstraint(None) + try: + cons.validate(20) + except MinSizeConstraintError: + assert False, "Empty contraint should not raise an exception" + + def test_validate(self): + cons = MinSizeConstraint(10) + + try: + cons.validate(range(0, 5)) + assert False, "Validation of MinSizeConstraint(10) on list of length 20 should raise an exception" + except MinSizeConstraintError: + pass + + try: + cons.validate(range(0,20)) + except MinSizeConstraintError: + assert True, "Validation of MinSizeConstraint(10) on list of length 5 should not raise an exception" + +class ColorConstraintTest(unittest.TestCase): + def test_validate(self): + cons = ColorConstraint() + + try: + cons.validate([0, 0]) + assert False, "ColorConstraint on list of length 2 should raise an exception" + except ColorArraySizeError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([0, 0, "str"]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorTypeError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([0, "str", 0]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorTypeError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate(["str", 0, 0]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorTypeError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([1, 2, 300]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorValueError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([1, -100, 30]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorValueError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([999999, 2, 300]) + assert False, "ColorConstraint on with non-integers values should raise an exception" + except ColorValueError: + pass + except ColorConstraintError: + assert False, "ColorConstraint threw the wrong type of error" + + try: + cons.validate([12, 23, 34]) + except LowerLimitConstraintError: + assert True, "ColorConstraint on expected input should not raise an exception" + +class BooleanConstraintTest(unittest.TestCase): + def test_validate(self): + cons = BooleanConstraint() + + cons.validate(True) + cons.validate(False) + + try: + cons.validate(18) + assert False, "Setting integer on constraint should raise an error" + except BooleanConstraintError: + pass + except: + assert False, "Wrong exception type raised when setting wrong type" + +class EnumConstraintTest(unittest.TestCase): + def test_validate(self): + cons = EnumConstraint([1,2,3,7,8,9, "ex"]) + + cons.validate(8) + + cons.validate("ex") + + try: + cons.validate(4) + assert False, "There should be an exception on setting a value out of the enum" + except EnumConstraintError: + pass + except: + assert False, "Wrong exception type thrown" + +class FileConstraintTest(unittest.TestCase): + def test_validate(self): + cons = FileConstraint() + + cons.validate("run-tests.py") + + try: + cons.validate("unknown/file.py") + assert False, "Non-existing file check should throw an exception" + except FileConstraintError: + pass + +if __name__ == "__main__": + unittest.main() \ No newline at end of file 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() diff --git a/tests/filterstests.py b/tests/filterstests.py new file mode 100644 index 0000000..3e79bcc --- /dev/null +++ b/tests/filterstests.py @@ -0,0 +1,201 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet +# +# 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 +""" +Filters Tests + +This module contains all the tests that pertain to the usage of the Tutorius +Event Filters +""" + +import unittest +import time +import gobject +import gtk + +from sugar.tutorius.filters import EventFilter, TimerEvent, GtkWidgetTypeFilter +from sugar.tutorius import addon +from gtkutilstests import SignalCatcher + +class BaseEventFilterTests(unittest.TestCase): + """Test the behavior of the Base EventFilter class""" + def test_properties(self): + """Test EventFilter properties""" + e = EventFilter("NEXTSTATE") + + assert e.next_state == "NEXTSTATE", "next_state should have value used in constructor" + + e.next_state = "NEWSTATE" + + assert e.next_state == "NEWSTATE", "next_state should have been changed by setter" + + + def test_callback(self): + """Test the callback mechanism""" + e = EventFilter("Next") + s = SignalCatcher() + + #Trigger the do_callback, shouldn't do anything + e.do_callback() + + #Install the handler + e.install_handlers(s.callback) + + #Trigger the do_callback, s should receive e + e.do_callback() + assert s.data[0] is e + + s.data = None + + e.remove_handlers() + + #Trigger callback, nothing should happen again + e.do_callback() + + assert s.data is None + + + + + +class TestTimerEvent(unittest.TestCase): + """Tests for timer""" + def test_timer(self): + """Make sure timer gets called once, and only once""" + gobject.threads_init() + ctx = gobject.MainContext() + main = gobject.MainLoop(ctx) + + e = TimerEvent("Next",1) #1 second should be enough :s + s = SignalCatcher() + + e.install_handlers(s.callback) + + assert s.data is None, "Callback should not have been called yet" + + #process events + while gtk.events_pending(): + gtk.main_iteration(block=False) + while ctx.pending(): + ctx.iteration(may_block=False) + + #Wait 1.4 sec + time.sleep(1.4) + + #process events + while gtk.events_pending(): + gtk.main_iteration(block=False) + while ctx.pending(): + ctx.iteration(may_block=False) + + assert not s.data is None, "Callback should have been called" + + s.data = None + + #Wait 1.4 sec + time.sleep(1.4) + + #process events + while gtk.events_pending(): + gtk.main_iteration(block=False) + while ctx.pending(): + ctx.iteration(may_block=False) + + assert s.data is None, "Callback should not have been called again" + + def test_timer_stop(self): + """Make sure timer can be stopped""" + gobject.threads_init() + ctx = gobject.MainContext() + main = gobject.MainLoop(ctx) + + e = TimerEvent("Next",1) #1 second should be enough :s + s = SignalCatcher() + + e.install_handlers(s.callback) + + assert s.data is None, "Callback should not have been called yet" + + #process events + while gtk.events_pending(): + gtk.main_iteration(block=False) + while ctx.pending(): + ctx.iteration(may_block=False) + + assert s.data is None, "Callback should not have been called yet" + + #Wait 0.5 sec + time.sleep(0.5) + + e.remove_handlers() + + #Wait 0.5 sec + time.sleep(0.7) + + #process events + while gtk.events_pending(): + gtk.main_iteration(block=False) + while ctx.pending(): + ctx.iteration(may_block=False) + + assert s.data is None, "Callback should not have been called" + + s.data = None + + +class TestGtkWidgetEventFilter(unittest.TestCase): + """Tests for GtkWidgetEventFilter""" + def __init__(self,*args): + unittest.TestCase.__init__(self,*args) + self.top=None + self.btn1=None + + def setUp(self): + self.top = gtk.Window() + self.btn1 = gtk.Button() + self.top.add(self.btn1) + + def test_install(self): + h = addon.create('GtkWidgetEventFilter', "Next","0","whatever") + try: + h.install_handlers(None) + + assert False, "Install handlers should have failed" + except TypeError: + assert True, "Install should have failed" + + def test_button_clicks(self): + h = addon.create('GtkWidgetEventFilter', "Next","0.0","clicked") + s = SignalCatcher() + + h.install_handlers(s.callback, activity=self.top) + + assert s.data is None, "no callback to call yet" + + self.btn1.clicked() + assert not s.data is None, "callback should have been called" + s.data = None + + h.remove_handlers() + + assert s.data is None, "callback must not be called again" + + self.btn1.clicked() + + assert s.data is None, "callback must not be called again" + + + diff --git a/tests/gtkutilstests.py b/tests/gtkutilstests.py new file mode 100644 index 0000000..41634ae --- /dev/null +++ b/tests/gtkutilstests.py @@ -0,0 +1,211 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# Copyright (C) 2009, Vincent Vinet +# +# 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 +""" +Gtk Utils Tests + +This module contains all the tests that pertain to the usage of the Tutorius +gtkutils +""" + +import unittest + +import logging +import gtk, gobject +from sugar.tutorius.gtkutils import find_widget, register_signals_numbered, register_signals, get_children + +class SignalCatcher(object): + """Test class that store arguments received on it's callback method. + Useful for testing callbacks""" + def __init__(self): + """Constructor""" + self.data = None + + def callback(self, *args): + """Callback function, stores argument list in self.data""" + self.data = args + +def disconnect_handlers(hlist): + """Disconnect handles in handler list. hlist must be a list of + two-tuples (widget, handler_id)""" + for widget, handler in hlist: + try: + widget.handler_disconnect(handler) + except: + pass + +class GtkUtilsTests(unittest.TestCase): + def __init__(self,*args): + unittest.TestCase.__init__(self,*args) + self.widgets = {} + self.top = None + + def setUp(self): + #create hierarchy + self.top = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + self.top.set_name("Top") + self.widgets["top"] = {"named":"Top","numbered":"0","widget":self.top} + + hbox = gtk.HBox() + self.top.add(hbox) + hbox.show() + self.widgets["hbox0"] = {"name":"Top.GtkHBox","numbered":"0.0","widget":hbox} + + btn1 = gtk.Button() + btn1.set_name("Button1") + hbox.pack_start(btn1) + btn1.show() + self.widgets["btn1"] = {"name":"Top.GtkHBox.Button1","numbered":"0.0.0","widget":btn1} + + btn2 = gtk.Button() + btn2.set_name("Button2") + hbox.pack_start(btn2) + btn2.show() + self.widgets["btn2"] = {"name":"Top.GtkHBox.Button2","numbered":"0.0.1","widget":btn2} + + vbox = gtk.VBox() + vbox.set_name("VBox1") + hbox.pack_start(vbox) + hbox.show() + self.widgets["vbox0"] = {"name":"Top.GtkHBox.VBox1","numbered":"0.0.2","widget":vbox} + + btn3 = gtk.Button() + btn3.set_name("Button3") + vbox.pack_start(btn3) + btn3.show() + self.widgets["btn3"] = {"name":"Top.GtkHBox.VBox1.Button3","numbered":"0.0.2.0","widget":btn3} + + btn4 = gtk.Button() + vbox.pack_start(btn4) + btn4.show() + self.widgets["btn4"] = {"name":"Top.GtkHBox.VBox1.GtkButton","numbered":"0.0.2.1","widget":btn4} + + def tearDown(self): + #destroy hierarchy + self.top.destroy() + self.top = None + self.widgets = {} + + def test_named(self): + #def register_signals(target, handler, prefix=None, max_depth=None): + s=SignalCatcher() + + #Test 0 depth + handler_list = register_signals(self.top, s.callback, max_depth=0) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list) + + assert widget_list[0] == self.top, "register_signals should have gotten only the top" + + disconnect_handlers(handler_list) + + #Test 2 depth + handler_list = register_signals(self.top, s.callback, max_depth=2) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 5, "expected %d objects (got %d)" % (len(widget_list), 5) + + disconnect_handlers(handler_list) + + #Test Infinite depth + handler_list = register_signals(self.top, s.callback, max_depth=None) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 7, "expected %d objects (got %d)" % (len(widget_list), 7) + + disconnect_handlers(handler_list) + + + def test_numbered(self): + s=SignalCatcher() + #def register_signals_numbered(target, handler, prefix="0", max_depth=None): + + #Test 0 depth + handler_list = register_signals_numbered(self.top, s.callback, max_depth=0) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list) + + assert widget_list[0] == self.top, "register_signals should have gotten only the top" + + disconnect_handlers(handler_list) + + #Test 1 depth + handler_list = register_signals_numbered(self.top, s.callback, max_depth=1) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 2, "expected %d objects (got %d)" % (len(widget_list), 2) + + disconnect_handlers(handler_list) + + #Test Infinite depth + handler_list = register_signals_numbered(self.top, s.callback, max_depth=None) + + #remove duplicates in widget list + widget_list = dict.fromkeys([w for w, h in handler_list]).keys() + + assert len(widget_list) == 7, "expected %d objects (got %d)" % (len(widget_list), 7) + + disconnect_handlers(handler_list) + + + def test_find_widget(self): + #Test individual values in the defined widgets + for widget in self.widgets.values(): + f = find_widget(self.top, widget["numbered"]) + assert f is widget["widget"], "Widget %s found with path %s, expected %s" % (f, widget["numbered"], widget["widget"]) + + #Test out of index + f = find_widget(self.top, "0.99.1.2") + assert f is self.top, "Should have returned top widget" + + def test_register_args_numbered(self): + #Need to check the signal catcher and stuff... grreat + while gtk.events_pending(): + gtk.main_iteration(block=False) + + + def test_register_args_normal(self): + #Need to check the signal catcher and stuff... grreat + while gtk.events_pending(): + gtk.main_iteration(block=False) + + def test_notwidget(self): + """Test the get_children function""" + o = object() + res = get_children(o) + + assert len(res) == 0, "object has no children" + + top_children = get_children(self.top) + expected = [self.widgets["hbox0"]["widget"],] + assert top_children == expected, "expected %s for top's children, got %s" % (str(expected),str(top_children)) + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/linear_creatortests.py b/tests/linear_creatortests.py new file mode 100644 index 0000000..dcded57 --- /dev/null +++ b/tests/linear_creatortests.py @@ -0,0 +1,79 @@ +# Copyright (C) 2009, Tutorius.org +# Greatly influenced by sugar/activity/namingalert.py +# +# 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 + +from sugar.tutorius.core import * +from sugar.tutorius.actions import * +from sugar.tutorius.filters import * +from sugar.tutorius.linear_creator import * +from coretests import TriggerEventFilter +from actiontests import CountAction +import unittest + +class CreatorTests(unittest.TestCase): + + def test_simple_usage(self): + creator = LinearCreator() + fsm_name = "SimpleUsageTest" + + creator.set_name(fsm_name) + + # Generate an FSM using the steps + creator.action(CountAction()) + creator.action(CountAction()) + + creator.event(TriggerEventFilter("Not important")) + + creator.action(CountAction()) + + creator.event(TriggerEventFilter("Not good either...")) + + fsm = creator.generate_fsm() + + # Make sure everything worked! + assert fsm.name == fsm_name, "Name was not set properly" + + init_state = fsm.get_state_by_name("INIT") + + assert len(init_state.get_action_list()) == 2, "Creator did not insert all the actions" + + assert init_state.get_event_filter_list()[0].get_next_state() == "State 1" , "expected next state to be 'State 1' but got %s" % init_state.get_event_filter_list()[0].get_next_state() + + state1 = fsm.get_state_by_name("State 1") + + assert len(state1.get_action_list()) == 1, "Creator did not insert all the actions" + + assert state1.get_event_filter_list()[0].get_next_state() == "State 2" + + # Make sure we have the final state and that it's empty + state2 = fsm.get_state_by_name("State2") + + assert len(state2.get_action_list()) == 0, "Creator inserted extra actions on wrong state" + + assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state" + + creator.action(CountAction()) + + fsm = creator.generate_fsm() + + state2 = fsm.get_state_by_name("State2") + + assert len(state2.get_action_list()) == 1, "Creator did not add the action" + + assert len(state2.get_event_filter_list()) == 0, "Creator assigner events to the final state" + +if __name__ == '__main__': + unittest.main() diff --git a/tests/overlaytests.py b/tests/overlaytests.py new file mode 100644 index 0000000..783377c --- /dev/null +++ b/tests/overlaytests.py @@ -0,0 +1,119 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Simon Poirier +# +# 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 +""" +GUI Tests + +This module contains all the tests that pertain to the usage of the Tutorius +overlay mechanism used to display objects on top of the application. +""" + +import unittest + +import logging +import gtk, gobject +from sugar.tutorius.actions import Action +import sugar.tutorius.overlayer as overlayer + +class CanvasDrawable(object): + def __init__(self): + self._no_expose = False + self.exposition_count = 0 + def _set_no_expose(self, value): + self._no_expose = value + def draw_with_context(self, context): + self.exposition_count += 1 + no_expose = property(fset=_set_no_expose) + + +class OverlayerTest(unittest.TestCase): + def test_cairodrawable_iface(self): + """ + Quickly validates that all our cairo widgets have a minimal interface + implemented. + """ + drawables = [overlayer.TextBubble] + for widget in drawables: + for attr in filter(lambda s:s[0]!='_', dir(CanvasDrawable)): + assert hasattr(widget, attr), \ + "%s not implementing CanvasDrawable iface"%widget.__name__ + + + def test_drawn(self): + """ + Ensures a cairo widget draw method is called at least once in + a real gui app. + """ + win = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + + btn = gtk.Button() + btn.show() + overlay = overlayer.Overlayer(btn) + win.add(overlay) + # let's also try to draw substitute button label + lbl = overlayer.TextBubble("test!") + assert lbl.label == 'test!', \ + "label property mismatch" + btn.show() + lbl.show() + btn.add(lbl) + + lbl.no_expose = True + assert lbl.no_expose, "wrong no_expose evaluation" + lbl.no_expose = False + assert not lbl.no_expose, "wrong no_expose evaluation" + + + widg = gtk.Button('bo') + widg.show() + overlay.put(widg, 50,50) + #widget = overlayer.TextBubble("testing msg!", tailpos=(10,-20)) + #widget.exposition_count = 0 + ## override draw method + #def counter(ctx, self=widget): + # self.exposition_count += 1 + # self.real_exposer(ctx) + #widget.real_exposer = widget.draw_with_context + #widget.draw_with_context = counter + ## centering allows to test the blending with the label + #overlay.put(widget, 50, 50) + #widget.show() + #assert widget.no_expose, \ + # "Overlay should overide exposition handling of widget" + #assert not lbl.no_expose, \ + # "Non-overlayed cairo should expose as usual" + + # force widget realization + # the child is flagged to be redrawn, the overlay should redraw too. + win.set_default_size(100, 100) + win.show() + + while gtk.events_pending(): + gtk.main_iteration(block=False) + # visual validation: there should be 2 visible bubbles, one as label, + # one as overlay + import time + time.sleep(1) + # as x11 events are asynchronous, wait a bit before assuming it went + # wrong. + while gtk.events_pending(): + gtk.main_iteration(block=False) + time.sleep(10) + assert widget.exposition_count>0, "overlay widget should expose" + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/propertiestests.py b/tests/propertiestests.py new file mode 100644 index 0000000..46346c4 --- /dev/null +++ b/tests/propertiestests.py @@ -0,0 +1,402 @@ +# Copyright (C) 2009, Tutorius.org +# +# 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 + +import unittest + +from sugar.tutorius.constraints import * +from sugar.tutorius.properties import * + +# Helper function to test the wrong types on a property, given its type +def try_wrong_values(obj): + typ = type(obj).prop.type + if typ != "int": + try: + obj.prop = 3 + assert False, "Able to insert int value in property of type %s"%typ + except: + pass + + if typ != "float": + try: + obj.prop = 1.1 + assert False, "Able to insert float value in property of type %s"%typ + except: + pass + + if typ != "string": + try: + obj.prop = "Fake string" + assert False, "Able to insert string value in property of type %s"%typ + except: + pass + + if typ != "array": + try: + obj.prop = [1, 2000, 3, 400] + assert False, "Able to insert array value in property of type %s"%typ + except: + pass + + if typ != "color": + try: + obj.prop = [1,2,3] + if typ != "array": + assert False, "Able to insert color value in property of type %s"%typ + except: + pass + + if typ != "boolean": + try: + obj.prop = True + if typ != "boolean": + assert False, "Able to set boolean value in property of type %s"%typ + except: + pass + +class BasePropertyTest(unittest.TestCase): + def test_base_class(self): + class klass(TPropContainer): + prop = TutoriusProperty() + obj = klass() + + assert klass.prop.default == None, "There should not be an initial value in the base property" + + assert klass.prop.type == None, "There should be no type associated with the base property" + + assert klass.prop.get_constraints() == [], "There should be no constraints on the base property" + + obj.prop = 2 + + assert obj.prop == 2, "Unable to set a value on base class" + +class TIntPropertyTest(unittest.TestCase): + def test_int_property(self): + class klass(TPropContainer): + prop = TIntProperty(22) + obj = klass() + + assert obj.prop == 22, "Could not set value on property via constructor" + + assert klass.prop.type == "int", "Wrong type on int property : %s" % prop.type + cons = klass.prop.get_constraints() + assert len(cons) == 2, "Not enough constraints on the int property" + + obj.prop = 12 + + assert obj.prop == 12, "Could not set value" + + def test_wrong_values(self): + class klass(TPropContainer): + prop = TIntProperty(33) + obj = klass() + + # Try setting values of other types + try_wrong_values(obj) + + def test_limit_constructor(self): + class klass(TPropContainer): + prop = TIntProperty(22, 0, 30) + obj = klass() + + try: + obj.prop = -22 + assert False, "Assigning an out-of-range value should trigger LowerLimitConstraint" + except LowerLimitConstraintError: + pass + except Exception: + assert False, "Wrong exception triggered by assignation" + + try: + obj.prop = 222 + assert False, "Assigning an out-of-range value should trigger UpperLimitConstraint" + except UpperLimitConstraintError: + pass + except Exception: + assert False, "Wrong exception triggered by assignation" + + def test_failing_constructor(self): + try: + prop = TIntProperty(100, 0, 20) + assert False, "Creation of the property should fail." + except UpperLimitConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + + try: + prop = TIntProperty(-100, 0, 20) + assert False, "Creation of the property should fail." + except LowerLimitConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TFloatPropertyTest(unittest.TestCase): + def test_float_property(self): + class klass(TPropContainer): + prop = TFloatProperty(22) + obj = klass() + + assert obj.prop == 22, "Could not set value on property via constructor" + + assert klass.prop.type == "float", "Wrong type on float property : %s" % klass.prop.type + cons = klass.prop.get_constraints() + assert len(cons) == 2, "Not enough constraints on the float property" + + obj.prop = 12 + + assert obj.prop == 12, "Could not set value" + + def test_wrong_values(self): + class klass(TPropContainer): + prop = TFloatProperty(33) + obj = klass() + + # Try setting values of other types + try_wrong_values(obj) + + def test_limit_constructor(self): + class klass(TPropContainer): + prop = TFloatProperty(22.4, 0.1, 30.5223) + obj = klass() + + try: + obj.prop = -22.8 + assert False, "Assigning an out-of-range value should trigger LowerLimitConstraint" + except LowerLimitConstraintError: + pass + except Exception: + assert False, "Wrong exception triggered by assignation" + + try: + obj.prop = 222.2 + assert False, "Assigning an out-of-range value should trigger UpperLimitConstraint" + except UpperLimitConstraintError: + pass + except Exception: + assert False, "Wrong exception triggered by assignation" + + def test_failing_constructor(self): + try: + prop = TFloatProperty(100, 0, 20) + assert False, "Creation of the property should fail." + except UpperLimitConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + + try: + prop = TFloatProperty(-100, 0, 20) + assert False, "Creation of the property should fail." + except LowerLimitConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TStringPropertyTest(unittest.TestCase): + def test_basic_string(self): + class klass(TPropContainer): + prop = TStringProperty("Starter string") + obj = klass() + + assert obj.prop == "Starter string", "Could not set string value via constructor" + + assert klass.prop.type == "string", "Wrong type for string property : %s" % klass.prop.type + + def test_size_limit(self): + class klass(TPropContainer): + prop = TStringProperty("Small", 10) + obj = klass() + + try: + obj.prop = "My string is too big!" + assert False, "String should not set to longer than max size" + except MaxSizeConstraintError: + pass + except: + assert False, "Wrong exception type thrown" + + def test_wrong_values(self): + class klass(TPropContainer): + prop = TStringProperty("Beginning") + obj = klass() + + try_wrong_values(obj) + + def test_failing_constructor(self): + try: + prop = TStringProperty("This is normal", 5) + assert False, "Creation of the property should fail." + except MaxSizeConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TArrayPropertyTest(unittest.TestCase): + def test_basic_array(self): + class klass(TPropContainer): + prop = TArrayProperty([1, 2, 3, 4]) + obj = klass() + + assert obj.prop == [1,2,3,4], "Unable to set initial value via constructor" + + assert klass.prop.type == "array", "Wrong type for array : %s"%klass.prop.type + + def test_wrong_values(self): + class klass(TPropContainer): + prop = TArrayProperty([1,2,3,4,5]) + obj = klass() + + try_wrong_values(obj) + + def test_size_limits(self): + class klass(TPropContainer): + prop = TArrayProperty([1,2], None, 4) + obj = klass() + + try: + obj.prop = [1,2,4,5,6,7] + assert False, "Maximum size limit constraint was not properly applied" + except MaxSizeConstraintError: + pass + + class klass(TPropContainer): + prop = TArrayProperty([1,2,3,4], 2) + obj = klass() + + try: + obj.prop = [1] + assert False, "Minimum size limit constraint was not properly applied" + except MinSizeConstraintError: + pass + + def test_failing_constructor(self): + try: + prop = TArrayProperty([100, 0, 20], None, 2) + assert False, "Creation of the property should fail." + except MaxSizeConstraintError: + pass + try: + prop = TArrayProperty([100, 0, 20], 4, None) + assert False, "Creation of the property should fail." + except MinSizeConstraintError: + pass + +class TColorPropertyTest(unittest.TestCase): + def test_basic_color(self): + class klass(TPropContainer): + prop = TColorProperty(20, 40, 60) + obj = klass() + + assert obj.prop == [20, 40, 60], "Could not set initial value with constructor" + + assert klass.prop.type == "color", "Wrong type on color : %s"%klass.prop.type + + def test_wrong_values(self): + class klass(TPropContainer): + prop = TColorProperty(250, 250, 250) + obj = klass() + + try_wrong_values(obj) + + def test_failing_constructor(self): + try: + prop = TColorProperty(0, "str", 0) + assert False, "Creation of the property should fail." + except ColorTypeError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TBooleanPropertyTest(unittest.TestCase): + def setUp(self): + class klass(TPropContainer): + prop = TBooleanProperty(False) + self.obj = klass() + + def test_basic_boolean(self): + assert self.obj.prop == False, "Could not set initial value via constructor" + + assert self.obj.__class__.prop.type == "boolean", "Wrong type for TBooleanProperty : %s"%self.obj.__class__.prop.type + + self.obj.prop = True + + assert self.obj.prop == True, "Could not change the value via set" + + self.obj.prop = False + + assert self.obj.prop == False, "Could not change the value via set" + + def test_wrong_types(self): + try_wrong_values(self.obj) + + def test_failing_constructor(self): + try: + prop = TBooleanProperty(64) + assert False, "Creation of the property should fail with non-boolean value" + except BooleanConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TEnumPropertyTest(unittest.TestCase): + def setUp(self): + class klass(TPropContainer): + prop = TEnumProperty("hello", [1, 2, "hello", "world", True, None]) + self.obj = klass() + + def test_basic_enum(self): + assert self.obj.prop == "hello", "Could not set initial value on property" + + assert type(self.obj).prop.type == "enum", "Wrong type for TEnumProperty : %s"%type(self.obj).prop.type + + self.obj.prop = True + + assert self.obj.prop, "Could not change the value via set" + + try: + self.obj.prop = 4 + assert False, "Switched to a value outside the enum" + except EnumConstraintError: + pass + + def test_wrong_type(self): + try_wrong_values(self.obj) + +class TFilePropertyTest(unittest.TestCase): + def setUp(self): + class klass(TPropContainer): + prop = TFileProperty("propertiestests.py") + self.obj = klass() + + def test_basic_file(self): + assert self.obj.prop == "propertiestests.py", "Could not set initial value" + + assert type(self.obj).prop.type == "file", "Wrong type for TFileProperty : %s"%type(self.obj).prop.type + + self.obj.prop = "run-tests.py" + + assert self.obj.prop == "run-tests.py", "Could not change value" + + try: + self.obj.prop = "unknown/file/on/disk.gif" + assert False, "An exception should be thrown on unknown file" + except FileConstraintError: + pass + +if __name__ == "__main__": + unittest.main() + diff --git a/tests/run-tests.py b/tests/run-tests.py new file mode 100755 index 0000000..d41aa0a --- /dev/null +++ b/tests/run-tests.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# This is a dumb script to run tests on the sugar-jhbuild installed files +# The path added is the default path for the jhbuild build + +INSTALL_PATH="../../../../../../install/lib/python2.5/site-packages/" + +import os, sys +sys.path.insert(0, + os.path.abspath(INSTALL_PATH) +) + +FULL_PATH = os.path.join(INSTALL_PATH,"sugar/tutorius") +SUBDIRS = ["uam"] +GLOB_PATH = os.path.join(FULL_PATH,"*.py") +import unittest +from glob import glob +def report_files(): + ret = glob(GLOB_PATH) + for dir in SUBDIRS: + ret += glob(os.path.join(FULL_PATH,dir,"*.py")) + return ret + +import sys +if __name__=='__main__': + if "--coverage" in sys.argv: + sys.argv=[arg for arg in sys.argv if arg != "--coverage"] + import coverage + coverage.erase() + #coverage.exclude('raise NotImplementedError') + coverage.start() + + import coretests + import servicestests + import gtkutilstests + #import overlaytests # broken + import linear_creatortests + import actiontests + import uamtests + import filterstests + import constraintstests + import propertiestests + import serializertests + suite = unittest.TestSuite() + suite.addTests(unittest.findTestCases(coretests)) + suite.addTests(unittest.findTestCases(servicestests)) + suite.addTests(unittest.findTestCases(gtkutilstests)) + #suite.addTests(unittest.findTestCases(overlaytests)) # broken + suite.addTests(unittest.findTestCases(linear_creatortests)) + suite.addTests(unittest.findTestCases(actiontests)) + suite.addTests(unittest.findTestCases(uamtests)) + suite.addTests(unittest.findTestCases(filterstests)) + suite.addTests(unittest.findTestCases(constraintstests)) + suite.addTests(unittest.findTestCases(propertiestests)) + suite.addTests(unittest.findTestCases(serializertests)) + runner = unittest.TextTestRunner() + runner.run(suite) + coverage.stop() + coverage.report(report_files()) + coverage.erase() + else: + from coretests import * + from servicestests import * + from gtkutilstests import * + #from overlaytests import * # broken + from actiontests import * + from linear_creatortests import * + from uamtests import * + from filterstests import * + from constraintstests import * + from propertiestests import * + from actiontests import * + from serializertests import * + + unittest.main() diff --git a/tests/serializertests.py b/tests/serializertests.py new file mode 100644 index 0000000..6c25bae --- /dev/null +++ b/tests/serializertests.py @@ -0,0 +1,197 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Jean-Christophe Savard +# +# 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 +""" +Serialization Tests + +This module contains all the tests that pertain to the usage of the Tutorius +Serializer object. This means testing saving a tutorial dictionary to a .tml +file, loading the list of tutorials for this activity and building chosen +tutorial. +""" + +import unittest + +import os +import shutil + +from sugar.tutorius import bundler, addon +from sugar.tutorius.core import State, FiniteStateMachine +from sugar.tutorius.actions import * +from sugar.tutorius.filters import * +from sugar.tutorius.bundler import XMLSerializer, Serializer +import sugar +from uuid import uuid1 + +class SerializerInterfaceTest(unittest.TestCase): + """ + For completeness' sake. + """ + def test_save(self): + ser = Serializer() + + try: + ser.save_fsm(None) + assert False, "save_fsm() should throw an unimplemented error" + except: + pass + + def test_load(self): + ser = Serializer() + + try: + ser.load_fsm(str(uuid.uuid1())) + assert False, "load_fsm() should throw an unimplemented error" + except: + pass + +class XMLSerializerTest(unittest.TestCase): + """ + Tests the transformation of XML to FSM, then back. + """ + def setUp(self): + # Make the serializer believe the test is in a activity path + self.testpath = "/tmp/testdata/" + os.environ["SUGAR_BUNDLE_PATH"] = self.testpath + os.environ["SUGAR_PREFIX"] = self.testpath + os.environ["SUGAR_PROFILE"] = 'test' +## os.mkdir(sugar.tutorius.bundler._get_store_root()) + + # Create the sample FSM + self.fsm = FiniteStateMachine("testingMachine") + + # Add a few states + act1 = addon.create('BubbleMessage', message="Hi", pos=[300, 450]) + ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked", "Second") + act2 = addon.create('BubbleMessage', message="Second message", pos=[250, 150], tailpos=[1,2]) + + st1 = State("INIT") + st1.add_action(act1) + st1.add_event_filter(ev1) + + st2 = State("Second") + + st2.add_action(act2) + + self.fsm.add_state(st1) + self.fsm.add_state(st2) + + self.uuid = uuid1() + + # Flag to set to True if the output can be deleted after execution of + # the test + self.remove = True + + def tearDown(self): + """ + Removes the created files, if need be. + """ + if self.remove == True: + shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar",os.getenv("SUGAR_PROFILE"))) + if os.path.isdir(self.testpath): + shutil.rmtree(self.testpath) + + def test_save(self): + """ + Writes an FSM to disk, then compares the file to the expected results. + "Remove" boolean argument specify if the test data must be removed or not + """ + xml_ser = XMLSerializer() + os.makedirs(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid))) + #rpdb2.start_embedded_debugger('flakyPass') + xml_ser.save_fsm(self.fsm, bundler.TUTORIAL_FILENAME, os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid))) + + def test_save_and_load(self): + """ + Load up the written FSM and compare it with the object representation. + """ + self.test_save() + testpath = "/tmp/testdata/" + #rpdb2.start_embedded_debugger('flakyPass') + xml_ser = XMLSerializer() + + # This interface needs to be redone... It's not clean because there is + # a responsibility mixup between the XML reader and the bundler. + loaded_fsm = xml_ser.load_fsm(str(self.uuid)) + + # Compare the two FSMs + assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ + 'FSM underlying dictionary differ from original to pickled/reformed one' + assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ + 'FSM underlying dictionary differ from original to pickled/reformed one' + assert loaded_fsm._states.get("INIT").get_action_list()[0].message == \ + self.fsm._states.get("INIT").get_action_list()[0].message, \ + 'FSM underlying State underlying Action differ from original to reformed one' + assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" + + def test_all_actions(self): + """ + Inserts all the known action types in a FSM, then attempt to load it. + """ + st = State("INIT") + + act1 = addon.create('BubbleMessage', "Hi!", pos=[10,120], tailpos=[-12,30]) + act2 = addon.create('DialogMessage', "Hello again.", pos=[120,10]) + act3 = WidgetIdentifyAction() + act4 = DisableWidgetAction("0.0.0.1.0.0.0") + act5 = TypeTextAction("0.0.0.1.1.1.0.0", "New text") + act6 = ClickAction("0.0.1.0.1.1") + act7 = OnceWrapper(act1) + act8 = ChainAction([act1, act2, act3, act4]) + actions = [act1, act2, act3, act4, act5, act6, act7, act8] + + for action in actions: + st.add_action(action) + + self.fsm.remove_state("Second") + self.fsm.remove_state("INIT") + self.fsm.add_state(st) + + xml_ser = XMLSerializer() + + self.test_save() + + reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) + assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + + def test_all_filters(self): + """ + Inserts all the known action types in a FSM, then attempt to load it. + """ + st = State("INIT") + + ev1 = TimerEvent("Second", 1000) + ev2 = addon.create('GtkWidgetEventFilter', "Second", "0.0.1.1.0.0.1", "clicked") + ev3 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", text="Typed stuff") + ev4 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", strokes="acbd") + filters = [ev1, ev2, ev3, ev4] + + for filter in filters: + st.add_event_filter(filter) + + self.fsm.remove_state("INIT") + self.fsm.add_state(st) + + xml_ser = XMLSerializer() + + self.test_save() + + reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) + + assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + +if __name__ == "__main__": + unittest.main() diff --git a/tests/servicestests.py b/tests/servicestests.py new file mode 100644 index 0000000..d669012 --- /dev/null +++ b/tests/servicestests.py @@ -0,0 +1,53 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm +# Copyright (C) 2009, Vincent Vinet +# +# 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 +"""Services tests""" + +import unittest + +from sugar.tutorius.services import * + + +class ObjectStoreTests(unittest.TestCase): + def setUp(self): + self.os1 = ObjectStore() + self.os2 = ObjectStore() + + def tearDown(self): + del self.os1 + del self.os2 + ObjectStore.instance = None + + def test_singleton(self): + """ + Validate that the object store is a singleton + """ + assert self.os1 is self.os2, "Both objectstore objects should be the same" + + def test_activity(self): + """Validate the activity property""" + act = object() + self.os1.activity = act + assert self.os1.activity is self.os2.activity + + def test_tutorial(self): + """Validate the tutorial property""" + tut = object() + self.os1.tutorial = tut + assert self.os1.tutorial is self.os2.tutorial + + diff --git a/tests/skip b/tests/skip new file mode 100644 index 0000000..2061a09 --- /dev/null +++ b/tests/skip @@ -0,0 +1,2 @@ +run-tests.py +overlaytests.py diff --git a/tests/uamtests.py b/tests/uamtests.py new file mode 100644 index 0000000..b2a5901 --- /dev/null +++ b/tests/uamtests.py @@ -0,0 +1,61 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm +# Copyright (C) 2009, Vincent Vinet +# +# 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 + +import unittest + +from sugar.tutorius.uam import parse_uri, SchemeError + +PARSE_SUITE={ +#URI SCHEME HOST PARAMS PATH QUERY FRAGMENT +"tap://act.tut.org/": ["tap", "act.tut.org","", "/", "", ""], +"tap.gtk://a.t.o/0/1": ["tap.gtk","a.t.o","","/0/1","","",""], +"tap.gobject://a.t.o/Timer?timeout=5":["tap.gobject","a.t.o","","/Timer","timeout=5",""], +} + +class ParseUriTests(unittest.TestCase): + """Tests the UAM parsers""" + def test_parse_uri(self): + """Test parsing results""" + for uri, test in PARSE_SUITE.items(): + res = parse_uri(uri) + + assert res.scheme == test[0], "%s : Expected scheme %s, got %s" % (uri, test[0], res.scheme) + assert res.netloc == test[1], "%s : Expected netloc %s, got %s" % (uri, test[1], res.netloc) + assert res.params == test[2], "%s : Expected params %s, got %s" % (uri, test[2], res.params) + assert res.path == test[3], "%s : Expected path %s, got %s" % (uri, test[3], res.path) + assert res.query == test[4], "%s : Expected query %s, got %s" % (uri, test[4], res.query) + assert res.fragment == test[5], "%s : Expected fragment %s, got %s" % (uri, test[5], res.fragment) + + def test_errors(self): + """Test exceptions""" + try: + parse_uri("http://something.org/path") + assert False, "Parsing http should fail" + except SchemeError: + pass + + try: + parse_uri("tap.notarealsubscheme://something.org/path") + assert False, "Invalid Subscheme should fail" + except SchemeError: + pass + + +if __name__ == "__main__": + unittest.main() + -- cgit v0.9.1