diff options
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | src/sugar/tutorius/actions.py | 138 | ||||
-rw-r--r-- | src/sugar/tutorius/bundler.py | 2 | ||||
-rw-r--r-- | src/sugar/tutorius/constraints.py | 189 | ||||
-rw-r--r-- | src/sugar/tutorius/core.py | 20 | ||||
-rw-r--r-- | src/sugar/tutorius/editor.py | 257 | ||||
-rw-r--r-- | src/sugar/tutorius/filters.py | 73 | ||||
-rw-r--r-- | src/sugar/tutorius/gtkutils.py | 95 | ||||
-rw-r--r-- | src/sugar/tutorius/properties.py | 204 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/actiontests.py | 173 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/constraintstests.py | 211 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/coretests.py | 174 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/filterstests.py | 200 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/gtkutilstests.py | 91 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/linear_creatortests.py | 16 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/propertiestests.py | 348 | ||||
-rw-r--r-- | src/sugar/tutorius/tests/uamtests.py | 61 | ||||
-rw-r--r-- | src/sugar/tutorius/uam/Makefile.am | 5 | ||||
-rw-r--r-- | src/sugar/tutorius/uam/__init__.py | 88 | ||||
-rw-r--r-- | src/sugar/tutorius/uam/gobjectparser.py | 27 | ||||
-rw-r--r-- | src/sugar/tutorius/uam/gtkparser.py | 44 |
21 files changed, 2283 insertions, 134 deletions
diff --git a/configure.ac b/configure.ac index 2f473cb..66e139b 100644 --- a/configure.ac +++ b/configure.ac @@ -42,6 +42,7 @@ src/sugar/bundle/Makefile src/sugar/graphics/Makefile src/sugar/presence/Makefile src/sugar/tutorius/Makefile +src/sugar/tutorius/uam/Makefile src/sugar/datastore/Makefile po/Makefile.in ]) diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py index 12de298..ad91fb4 100644 --- a/src/sugar/tutorius/actions.py +++ b/src/sugar/tutorius/actions.py @@ -42,6 +42,13 @@ class Action(object): """ pass #Should raise NotImplemented? + def get_properties(self): + if not hasattr(self, "_props") or self._props is None: + self._props = [] + for i in dir(self.__class__): + if type(getattr(self.__class__,i)) is property: + self._props.append(i) + return self._props class OnceWrapper(object): """ @@ -71,6 +78,9 @@ class OnceWrapper(object): if self._need_undo: self._action.undo() self._need_undo = False + + def get_properties(self): + return self._action.get_properties() class DialogMessage(Action): """ @@ -82,9 +92,25 @@ class DialogMessage(Action): def __init__(self, message, pos=[0,0]): super(DialogMessage, self).__init__() self._message = message - self.position = pos + self._position = pos self._dialog = None + def set_message(self, msg): + self._message = msg + + def get_message(self): + return self._message + + message = property(fget=get_message, fset=set_message) + + def set_pos(self, x, y): + self._position = [x, y] + + def get_pos(self): + return self._position + + position = property(fget=get_pos, fset=set_pos) + def do(self): """ Show the dialog @@ -115,14 +141,31 @@ class BubbleMessage(Action): def __init__(self, message, pos=[0,0], speaker=None, tailpos=None): Action.__init__(self) self._message = message - self.position = pos + self._position = pos self.overlay = None self._bubble = None self._speaker = None self._tailpos = tailpos + def set_message(self, msg): + self._message = msg + def get_message(self): + return self._message + message = property(fget=get_message, fset=set_message, doc="Message displayed to the user") + def set_pos(self, x, y): + self._position = [x, y] + def get_pos(self): + return self.position + position = property(fget=get_pos, fset=set_pos, doc="Position in [x, y] on the screen") + + def set_tail_pos(self, x, y): + self._tailpos = [x, y] + def get_tail_pos(self): + return self._tailpos + tail_pos = property(fget=get_tail_pos, fset=set_tail_pos, doc="Position the tail of the bubble must point to") + def do(self): """ Show the dialog @@ -135,7 +178,7 @@ class BubbleMessage(Action): # draw the overlay. if not self._bubble: - x, y = self.position + x, y = self._position # TODO: tails are relative to tailpos. They should be relative to # the speaking widget. Same of the bubble position. self._bubble = overlayer.TextBubble(text=self._message, @@ -170,4 +213,93 @@ class WidgetIdentifyAction(Action): if self._dialog: self._dialog.destroy() +class ChainAction(Action): + """Utility class to allow executing actions in a specific order""" + def __init__(self, *actions): + """ChainAction(action1, ... ) builds a chain of actions""" + self._actions = actions + + def do(self,**kwargs): + """do() each action in the chain""" + for act in self._actions: + act.do(**kwargs) + + def undo(self): + """undo() each action in the chain, starting with the last""" + for act in reversed(self._actions): + act.undo() + +class DisableWidgetAction(Action): + def __init__(self, target): + """Constructor + @param target target treeish + """ + Action.__init__(self) + self._target = target + self._widget = None + def do(self): + """Action do""" + os = ObjectStore() + if os.activity: + self._widget = gtkutils.find_widget(os.activity, self._target) + if self._widget: + self._widget.set_sensitive(False) + + def undo(self): + """Action undo""" + if self._widget: + self._widget.set_sensitive(True) + +class TypeTextAction(Action): + """ + Simulate a user typing text in a widget + Work on any widget that implements a insert_text method + + @param widget The treehish representation of the widget + @param text the text that is typed + """ + def __init__(self, widget, text): + Action.__init__(self) + + self._widget = widget + self._text = text + + def do(self, **kwargs): + """ + Type the text + """ + widget = gtkutils.find_widget(ObjectStore().activity, self._widget) + if hasattr(widget, "insert_text"): + widget.insert_text(self._text, -1) + + def undo(self): + """ + no undo + """ + pass + +class ClickAction(Action): + """ + Action that simulate a click on a widget + Work on any widget that implements a clicked() method + + @param widget The threehish representation of the widget + """ + def __init__(self, widget): + Action.__init__(self) + self._widget = widget + + def do(self): + """ + click the widget + """ + widget = gtkutils.find_widget(ObjectStore().activity, self._widget) + if hasattr(widget, "clicked"): + widget.clicked() + + def undo(self): + """ + No undo + """ + pass diff --git a/src/sugar/tutorius/bundler.py b/src/sugar/tutorius/bundler.py index d089c35..8712c86 100644 --- a/src/sugar/tutorius/bundler.py +++ b/src/sugar/tutorius/bundler.py @@ -363,4 +363,4 @@ class TutorialBundler: return False else: logger.debug("file is a directory :"+file) - return False
\ No newline at end of file + return False diff --git a/src/sugar/tutorius/constraints.py b/src/sugar/tutorius/constraints.py new file mode 100644 index 0000000..a666ecb --- /dev/null +++ b/src/sugar/tutorius/constraints.py @@ -0,0 +1,189 @@ +# 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 +""" +Constraints + +Defines a set of constraints with their related errors. These constraints are +made to be used inside TutoriusProperties in order to limit the values that +they might take. They can also be used to enforce a particular format or type +for some properties. +""" + +# For the File Constraint +import os + +class Constraint(): + """ + Basic block for defining constraints on a TutoriusProperty. Every class + inheriting from Constraint will have a validate function that will be + executed when the property's value is to be changed. + """ + def validate(self, value): + """ + This function receives the value that is proposed as a new value for + the property. It needs to raise an Error in the case where the value + does not respect this constraint. + """ + raise NotImplementedError("Unable to validate a base Constraint") + +class ValueConstraint(Constraint): + """ + A value constraint contains a _limit member that can be used in a children + class as a basic value. See UpperLimitConstraint for an exemple. + """ + def __init__(self, limit): + self.limit = limit + +class UpperLimitConstraintError(Exception): + pass + +class UpperLimitConstraint(ValueConstraint): + def validate(self, value): + """ + Evaluates whether the given value is smaller than the limit. + + @raise UpperLimitConstraintError When the value is strictly higher than + the limit. + """ + if self.limit is not None: + if self.limit >= value: + return + raise UpperLimitConstraintError() + return + +class LowerLimitConstraintError(Exception): + pass + +class LowerLimitConstraint(ValueConstraint): + def validate(self, value): + """ + If the value is lower than the limit, this function raises an error. + + @raise LowerLimitConstraintError When the value is strictly smaller + than the limit. + """ + if self.limit is not None: + if value >= self.limit: + return + raise LowerLimitConstraintError() + return + +class SizeConstraintError(Exception): + pass + +class SizeConstraint(ValueConstraint): + def validate(self, value): + """ + Evaluate whether a given object is smaller than the given size when + run through len(). Great for string, lists and the like. ;) + + @raise SizeConstraintError If the length of the value is strictly + bigger than the limit. + """ + if self.limit is not None: + if self.limit > len(value): + return + raise SizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit)) + return + +class ColorConstraintError(Exception): + pass + +class ColorArraySizeError(ColorConstraintError): + pass + +class ColorTypeError(ColorConstraintError): + pass + +class ColorValueError(ColorConstraintError): + pass + +class ColorConstraint(Constraint): + """ + Validates that the value is an array of size 3 with three numbers between + 0 and 255 (inclusively) in it. + + """ + def validate(self, value): + if len(value) != 3: + raise ColorArraySizeError("The value is not an array of size 3") + + if not (type(value[0]) == type(22) and type(value[1]) == type(22) and type(value[2]) == type(22)): + raise ColorTypeError("Not all the elements of the array are integers") + + if value[0] > 255 or value[0] <0: + raise ColorValueError("Red value is not between 0 and 255") + + if value[1] > 255 or value[1] <0: + raise ColorValueError("Green value is not between 0 and 255") + + if value[2] > 255 or value[2] <0: + raise ColorValueError("Blue value is not between 0 and 255") + + return + +class BooleanConstraintError(Exception): + pass + +class BooleanConstraint(Constraint): + """ + Validates that the value is either True or False. + """ + def validate(self, value): + if value == True or value == False: + return + raise BooleanConstraintError("Value is not True or False") + +class EnumConstraintError(Exception): + pass + +class EnumConstraint(Constraint): + """ + Validates that the value is part of a set of well-defined values. + """ + def __init__(self, accepted_values): + """ + Creates the constraint and stores the list of accepted values. + + @param correct_values A list that contains all the values that will + be declared as satisfying the constraint + """ + self._accepted_values = accepted_values + + def validate(self, value): + """ + Ensures that the value that is passed is part of the list of accepted + values. + """ + if not value in self._accepted_values: + raise EnumConstraintError("Value is not part of the enumeration") + return + +class FileConstraintError(Exception): + pass + +class FileConstraint(Constraint): + """ + Ensures that the string given corresponds to an existing file on disk. + """ + def validate(self, value): + # TODO : Decide on the architecture for file retrieval on disk + # Relative paths? From where? Support macros? + # + if not os.path.isfile(value): + raise FileConstraintError("Non-existing file : %s"%value) + return +
\ No newline at end of file diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 901820f..f290f1e 100644 --- a/src/sugar/tutorius/core.py +++ b/src/sugar/tutorius/core.py @@ -24,6 +24,7 @@ This module contains the core classes for tutorius import gtk import logging import copy +import os from sugar.tutorius.dialog import TutoriusDialog from sugar.tutorius.gtkutils import find_widget @@ -36,12 +37,13 @@ class Tutorial (object): Tutorial Class, used to run through the FSM. """ - def __init__(self, name, fsm): + def __init__(self, name, fsm,filename= None): """ Creates an unattached tutorial. """ object.__init__(self) self.name = name + self.activity_init_state_filename = filename self.state_machine = fsm self.state_machine.set_tutorial(self) @@ -64,6 +66,7 @@ class Tutorial (object): self.activity = activity ObjectStore().activity = activity ObjectStore().tutorial = self + self._prepare_activity() self.state_machine.set_state("INIT") def detach(self): @@ -97,6 +100,21 @@ class Tutorial (object): #Swith to the next state pointed by the eventfilter self.set_state(eventfilter.get_next_state()) + + def _prepare_activity(self): + """ + Prepare the activity for the tutorial by loading the saved state and + emitting gtk signals + """ + #Load the saved activity if any + if self.activity_init_state_filename is not None: + #For now the file will be saved in the data folder + #of the activity root directory + filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\ + self.activity_init_state_filename + if os.path.exists(filename): + self.activity.read_file(filename) + class State(object): """ diff --git a/src/sugar/tutorius/editor.py b/src/sugar/tutorius/editor.py index 1a1eb61..42cc718 100644 --- a/src/sugar/tutorius/editor.py +++ b/src/sugar/tutorius/editor.py @@ -14,17 +14,22 @@ # 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 +""" Tutorial Editor Module +""" import gtk import gobject -import hippo -import gconf +#import hippo +#import gconf from gettext import gettext as _ +from sugar.tutorius.gtkutils import register_signals_numbered, get_children + class WidgetIdentifier(gtk.Window): """ - Tool that allows identifying widgets + Tool that allows identifying widgets. + """ __gtype_name__ = 'TutoriusWidgetIdentifier' @@ -32,7 +37,19 @@ class WidgetIdentifier(gtk.Window): gtk.Window.__init__(self) self._activity = activity - self._handlers = [] + self._handlers = {} + # dict of signals to register on the widgets. + # key : signal name + # value : initial checkbox status + signals = { + "focus":True, + "button-press-event":True, + "enter-notify-event":False, + "leave-notify-event":False, + "key-press-event":True, + "text-selected":True, + "clicked":True, + } self.set_decorated(False) self.set_resizable(False) @@ -47,63 +64,249 @@ class WidgetIdentifier(gtk.Window): self._expander.show() - vbox = gtk.VBox() - self._expander.add(vbox) - vbox.show() - + nbk = gtk.Notebook() + self._expander.add(nbk) + nbk.show() + ############################### + # Event log viewer page + ############################### self.logview = gtk.TextView() self.logview.set_editable(False) self.logview.set_cursor_visible(False) self.logview.set_wrap_mode(gtk.WRAP_NONE) self._textbuffer = self.logview.get_buffer() - sw = gtk.ScrolledWindow() - sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - sw.add(self.logview) + swd = gtk.ScrolledWindow() + swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd.add(self.logview) self.logview.show() - vbox.pack_start(sw) - sw.show() + nbk.append_page(swd, gtk.Label(_("Log"))) + swd.show() + + ############################### + # Filters page + ############################### + filters = gtk.Table( (len(signals)+1)/2, 2) + + xpos, ypos = 0, 0 + for key, active in signals.items(): + cbtn = gtk.CheckButton(label=key) + filters.attach(cbtn, xpos, xpos+1, ypos, ypos+1) + cbtn.show() + cbtn.set_active(active) + if active: + self._handlers[key] = register_signals_numbered( \ + self._activity, self._handle_events, events=(key,)) + else: + self._handlers[key] = [] + + cbtn.connect("toggled", self.__filter_toggle_cb, key) + + #Follow lines then columns + xpos, ypos = (xpos+1)%2, ypos+(xpos%2) + + nbk.append_page(filters, gtk.Label(_("Events"))) + filters.show() + + ############################### + # Explorer Page + ############################### + tree = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + explorer = gtk.TreeView(tree) + + pathrendr = gtk.CellRendererText() + pathrendr.set_properties(background="#ffffff", foreground="#000000") + pathcol = gtk.TreeViewColumn(_("Path"), pathrendr, text=0, background=0, foreground=0) + explorer.append_column(pathcol) + + typerendr = gtk.CellRendererText() + typerendr.set_properties(background="#ffffff", foreground="#000000") + typecol = gtk.TreeViewColumn(_("Widget"), typerendr, text=1, background=1, foreground=1) + explorer.append_column(typecol) + + self.__populate_treestore( + tree, #tree + tree.append(None, ["0",self._activity.get_name()]), #parent + self._activity, #widget + "0" #path + ) + + explorer.set_expander_column(typecol) + + swd2 = gtk.ScrolledWindow() + swd2.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd2.add(explorer) + explorer.show() + nbk.append_page(swd2, gtk.Label(_("Explorer"))) + swd2.show() - from sugar.tutorius.gtkutils import register_signals_numbered - self._handlers = register_signals_numbered(self._activity, self._handle_events) + ############################### + # GObject Explorer Page + ############################### + tree2 = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + explorer2 = gtk.TreeView(tree2) - def __expander_cb(self, *args): + pathrendr2 = gtk.CellRendererText() + pathrendr2.set_properties(background="#ffffff", foreground="#000000") + pathcol2 = gtk.TreeViewColumn(_("Path"), pathrendr2, text=0, background=0, foreground=0) + explorer2.append_column(pathcol2) + + typerendr2 = gtk.CellRendererText() + typerendr2.set_properties(background="#ffffff", foreground="#000000") + typecol2 = gtk.TreeViewColumn(_("Widget"), typerendr2, text=1, background=1, foreground=1) + explorer2.append_column(typecol2) + + self.__populate_gobject_treestore( + tree2, #tree + tree2.append(None, ["activity",self._activity.get_name()]), #parent + self._activity, #widget + "activity" #path + ) + + explorer2.set_expander_column(typecol2) + + swd3 = gtk.ScrolledWindow() + swd3.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + swd3.add(explorer2) + explorer2.show() + nbk.append_page(swd3, gtk.Label(_("GObject Explorer"))) + swd3.show() + + def __populate_treestore(self, tree, parent, widget, path): + """Populates the treestore with the widget's children recursively + @param tree gtk.TreeStore to populate + @param parent gtk.TreeIter to append to + @param widget gtk.Widget to check for children + @param path treeish of the widget + """ + #DEBUG: show parameters in log window gehehe + #self._handle_events((path,str(type(widget)))) + children = get_children(widget) + for i in xrange(len(children)): + childpath = ".".join([path, str(i)]) + child = children[i] + self.__populate_treestore( + tree, #tree + tree.append(parent, [childpath, child.get_name()]), #parent + child, #widget + childpath #path + ) + + + def __populate_gobject_treestore(self, tree, parent, widget, path, listed=None): + """Populates the treestore with the widget's children recursively + @param tree gtk.TreeStore to populate + @param parent gtk.TreeIter to append to + @param widget gtk.Widget to check for children + @param path treeish of the widget + """ + listed = listed or [] + if widget in listed: + return + listed.append(widget) + #DEBUG: show parameters in log window gehehe + #self._handle_events((path,str(type(widget)))) + #Add a child node + children = tree.append(parent, ["","children"]) + for i in dir(widget): + #Add if a gobject + try: + child = getattr(widget, i) + except: + continue + if isinstance(child,gobject.GObject): + childpath = ".".join([path, i]) + child = getattr(widget, i) + self.__populate_gobject_treestore( + tree, #tree + tree.append(children, [childpath, i]), #parent + child, #widget + path + "." + i, #path, + listed + ) + widgets = tree.append(parent, ["","widgets"]) + wchildren = get_children(widget) + for i in xrange(len(wchildren)): + childpath = ".".join([path, str(i)]) + child = wchildren[i] + self.__populate_gobject_treestore( + tree, #tree + tree.append(widgets, [childpath, (hasattr(child,"get_name") and child.get_name()) or i]), #parent + child, #widget + childpath, #path, + listed + ) + + #Add signals and attributes nodes + signals = tree.append(parent, ["","signals"]) + for signame in gobject.signal_list_names(widget): + tree.append(signals, ["",signame]) + + attributes = tree.append(parent, ["","properties"]) + for prop in gobject.list_properties(widget): + tree.append(attributes, ["",prop]) + + def __filter_toggle_cb(self, btn, eventname): + """Callback for signal name checkbuttons' toggling""" + #Disconnect existing handlers on key + self.__disconnect_handlers(eventname) + if btn.get_active(): + #if checked, reconnect + self._handlers[eventname] = register_signals_numbered( \ + self._activity, self._handle_events, events=(eventname,)) + + + def __expander_cb(self, *args): + """Callback for the window expander toggling""" if self._expander.get_expanded(): self.__move_expanded() else: self.__move_collapsed() def __move_expanded(self): - width = 400 + """Move the window to it's expanded position""" + width = 500 height = 300 - ww = gtk.gdk.screen_width() - wh = gtk.gdk.screen_height() + swidth = gtk.gdk.screen_width() + sheight = gtk.gdk.screen_height() self.set_size_request(width, height) - self.move((ww-width)/2, wh-height) + self.move((swidth-width)/2, sheight-height) def __move_collapsed(self): + """Move the window to it's collapsed position""" width = 150 height = 40 - ww = gtk.gdk.screen_width() - wh = gtk.gdk.screen_height() + swidth = gtk.gdk.screen_width() + sheight = gtk.gdk.screen_height() self.set_size_request(width, height) - self.move((ww-width)/2, wh-height) + self.move((swidth-width)/2, sheight-height) def __realize_cb(self, widget): + """Callback for realize""" self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG) self.window.set_accept_focus(True) self.__move_expanded() def _disconnect_handlers(self): - for widget, handlerid in self._handlers: - widget.handler_disconnect(handlerid) - self._handlers = [] + """ Disconnect all event handlers """ + for key in self._handlers: + self.__disconnect_handlers(key) + + def __disconnect_handlers(self, key): + """ Disconnect event handlers associated to signal name "key" """ + if self._handlers.has_key(key): + for widget, handlerid in self._handlers[key]: + widget.handler_disconnect(handlerid) + del self._handlers[key] - def _handle_events(self,*args): + def _handle_events(self, *args): + """ Event handler for subscribed widget events. + Accepts variable length argument list. Last must be + a two-tuple containing (event name, widget name) """ sig, name = args[-1] text = "\r\n".join( (["%s event received from %s" % (sig, name)] + diff --git a/src/sugar/tutorius/filters.py b/src/sugar/tutorius/filters.py index a69055a..594ad6a 100644 --- a/src/sugar/tutorius/filters.py +++ b/src/sugar/tutorius/filters.py @@ -16,9 +16,14 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import gobject - +import gtk +import logging +logger = logging.getLogger("filters") from sugar.tutorius.gtkutils import find_widget +from sugar.tutorius.services import ObjectStore + + class EventFilter(object): """ Base class for an event filter @@ -168,4 +173,68 @@ class GtkWidgetEventFilter(EventFilter): self._widget.handler_disconnect(self._handler_id) self._handler_id=None - +class GtkWidgetTypeFilter(EventFilter): + """ + Event Filter that listens for keystrokes on a widget + """ + def __init__(self, next_state, object_id, text=None, strokes=None): + """Constructor + @param next_state default EventFilter param, passed on to EventFilter + @param object_id object tree-ish identifier + @param text resulting text expected + @param strokes list of strokes expected + + At least one of text or strokes must be supplied + """ + super(GtkWidgetTypeFilter, self).__init__(next_state) + self._object_id = object_id + self._text = text + self._captext = "" + self._strokes = strokes + self._capstrokes = [] + self._widget = None + self._handler_id = None + + def install_handlers(self, callback, **kwargs): + """install handlers + @param callback default EventFilter callback arg + """ + super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs) + logger.debug("~~~GtkWidgetTypeFilter install") + activity = ObjectStore().activity + if activity is None: + logger.error("No activity") + raise RuntimeWarning("no activity in the objectstore") + + self._widget = find_widget(activity, self._object_id) + if self._widget: + self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb) + logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) ) + + def remove_handlers(self): + """remove handlers""" + super(GtkWidgetTypeFilter, self).remove_handlers() + #if an event was connected, disconnect it + if self._handler_id: + self._widget.handler_disconnect(self._handler_id) + self._handler_id=None + + def __keypress_cb(self, widget, event, *args): + """keypress callback""" + logger.debug("~~~keypressed!") + key = event.keyval + keystr = event.string + logger.debug("~~~Got key: " + str(key) + ":"+ keystr) + self._capstrokes += [key] + #TODO Treat other stuff, such as arrows + if key == gtk.keysyms.BackSpace: + self._captext = self._captext[:-1] + else: + self._captext = self._captext + keystr + + logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext)) + if not self._strokes is None and self._strokes in self._capstrokes: + self.do_callback() + if not self._text is None and self._text in self._captext: + self.do_callback() + diff --git a/src/sugar/tutorius/gtkutils.py b/src/sugar/tutorius/gtkutils.py index 1870dc4..a745b9d 100644 --- a/src/sugar/tutorius/gtkutils.py +++ b/src/sugar/tutorius/gtkutils.py @@ -19,11 +19,6 @@ Utility classes and functions that are gtk related """ import gtk -def activity(activity=None, singleton=[]): - if activity: - singleton.append(activity) - return singleton[0] - def find_widget(base, target_fqdn): """Find a widget by digging into a parent widget's children tree @@ -47,13 +42,13 @@ def find_widget(base, target_fqdn): while len(path) > 0: try: - obj = obj.get_children()[int(path.pop(0))] + obj = get_children(obj)[int(path.pop(0))] except: break return obj -EVENTS = [ +EVENTS = ( "focus", "button-press-event", "enter-notify-event", @@ -61,9 +56,9 @@ EVENTS = [ "key-press-event", "text-selected", "clicked", -] +) -IGNORED_WIDGETS = [ +IGNORED_WIDGETS = ( "GtkVBox", "GtkHBox", "GtkAlignment", @@ -71,9 +66,9 @@ IGNORED_WIDGETS = [ "GtkButton", "GtkToolItem", "GtkToolbar", -] +) -def register_signals_numbered(target, handler, prefix="0", max_depth=None): +def register_signals_numbered(target, handler, prefix="0", max_depth=None, events=None): """ Recursive function to register event handlers on an target and it's children. The event handler is called with an extra @@ -96,25 +91,24 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None): @returns list of (object, handler_id) """ ret = [] + evts = events or EVENTS #Gtk Containers have a get_children() function - if hasattr(target, "get_children") and \ - hasattr(target.get_children, "__call__"): - children = target.get_children() - for i in range(len(children)): - child = children[i] - if max_depth is None or max_depth > 0: - #Recurse with a prefix on all children - pre = ".".join( \ - [p for p in (prefix, str(i)) if not p is None] - ) - if max_depth is None: - dep = None - else: - dep = max_depth - 1 - ret+=register_signals_numbered(child, handler, pre, dep) + children = get_children(target) + for i in range(len(children)): + child = children[i] + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, str(i)) if not p is None] + ) + if max_depth is None: + dep = None + else: + dep = max_depth - 1 + ret+=register_signals_numbered(child, handler, pre, dep, evts) #register events on the target if a widget XXX necessary to check this? if isinstance(target, gtk.Widget): - for sig in EVENTS: + for sig in evts: try: ret.append( \ (target, target.connect(sig, handler, (sig, prefix) ))\ @@ -124,7 +118,7 @@ def register_signals_numbered(target, handler, prefix="0", max_depth=None): return ret -def register_signals(target, handler, prefix=None, max_depth=None): +def register_signals(target, handler, prefix=None, max_depth=None, events=None): """ Recursive function to register event handlers on an target and it's children. The event handler is called with an extra @@ -148,28 +142,27 @@ def register_signals(target, handler, prefix=None, max_depth=None): @returns list of (object, handler_id) """ ret = [] + evts = events or EVENTS #Gtk Containers have a get_children() function - if hasattr(target, "get_children") and \ - hasattr(target.get_children, "__call__"): - for child in target.get_children(): - if max_depth is None or max_depth > 0: - #Recurse with a prefix on all children - pre = ".".join( \ - [p for p in (prefix, target.get_name()) \ - if not (p is None or p in IGNORED_WIDGETS)] \ - ) - if max_depth is None: - dep = None - else: - dep = max_depth - 1 - ret += register_signals(child, handler, pre, dep) + for child in get_children(target): + if max_depth is None or max_depth > 0: + #Recurse with a prefix on all children + pre = ".".join( \ + [p for p in (prefix, target.get_name()) \ + if not (p is None or p in IGNORED_WIDGETS)] \ + ) + if max_depth is None: + dep = None + else: + dep = max_depth - 1 + ret += register_signals(child, handler, pre, dep, evts) name = ".".join( \ [p for p in (prefix, target.get_name()) \ if not (p is None or p in IGNORED_WIDGETS)] \ ) #register events on the target if a widget XXX necessary to check this? if isinstance(target, gtk.Widget): - for sig in EVENTS: + for sig in evts: try: ret.append( \ (target, target.connect(sig, handler, (sig, name) )) \ @@ -179,3 +172,19 @@ def register_signals(target, handler, prefix=None, max_depth=None): return ret +def get_children(widget): + """Lists widget's children""" + #widgets with multiple children + try: + return widget.get_children() + except (AttributeError,TypeError): + pass + + #widgets with a single child + try: + return [widget.get_child(),] + except (AttributeError,TypeError): + pass + + #otherwise return empty list + return [] diff --git a/src/sugar/tutorius/properties.py b/src/sugar/tutorius/properties.py new file mode 100644 index 0000000..5be7e1c --- /dev/null +++ b/src/sugar/tutorius/properties.py @@ -0,0 +1,204 @@ +# 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 + +from sugar.tutorius.constraints import * + +class TutoriusProperty(): + """ + The base class for all actions' properties. The interface is the following : + + set() : attempts to change the value (may throw an exception if constraints + are not respected + + value : the value of the property + + type : the type of the property + + get_contraints() : the constraints inserted on this property. They define + what is acceptable or not as values. + """ + def __init__(self): + self._type = None + self._constraints = None + self._value = None + + def set(self, value): + """ + Attempts to set the value of the property. If the value does not respect + the constraints on the property, this method will raise an exception. + + The exception should be of the type related to the constraint that + failed. E.g. When a int is to be set with a value that + """ + for constraint_name in self.get_constraints(): + constraint = getattr(self, constraint_name) + constraint.validate(value) + self._value = value + return True + + def get(self): + return self._value + + value = property(fget=get) + + def get_constraints(self): + """ + Returns the list of constraints associated to this property. + """ + if self._constraints is None: + self._constraints = [] + for i in dir(self): + t = getattr(self,i) + if isinstance(t, Constraint): + self._constraints.append(i) + return self._constraints + + def get_type(self): + return self._type + + type = property(fget=get_type) + +class TIntProperty(TutoriusProperty): + """ + Represents an integer. Can have an upper value limit and/or a lower value + limit. + """ + + def __init__(self, value, lower_limit=None, upper_limit=None): + TutoriusProperty.__init__(self) + self._type = "int" + self.upper_limit = UpperLimitConstraint(upper_limit) + self.lower_limit = LowerLimitConstraint(lower_limit) + + self.set(value) + +class TFloatProperty(TutoriusProperty): + """ + Represents a floting point number. Can have an upper value limit and/or + a lower value limit. + """ + def __init__(self, value, lower_limit=None, upper_limit=None): + TutoriusProperty.__init__(self) + self._type = "float" + + self.upper_limit = UpperLimitConstraint(upper_limit) + self.lower_limit = LowerLimitConstraint(lower_limit) + + self.set(value) + +class TStringProperty(TutoriusProperty): + """ + Represents a string. Can have a maximum size limit. + """ + def __init__(self, value, size_limit=None): + TutoriusProperty.__init__(self) + self._type = "string" + self.size_limit = SizeConstraint(size_limit) + + self.set(value) + +class TArrayProperty(TutoriusProperty): + """ + Represents an array of properties. Can have a maximum number of element + limit, but there are no constraints on the content of the array. + """ + def __init__(self, value, size_limit=None): + TutoriusProperty.__init__(self) + self._type = "array" + self.size_limit = SizeConstraint(size_limit) + + self.set(value) + +class TColorProperty(TutoriusProperty): + """ + Represents a RGB color with 3 8-bit integer values. + + The value of the property is the array [R, G, B] + """ + def __init__(self, red=None, green=None, blue=None): + TutoriusProperty.__init__(self) + self._type = "color" + + self.color_constraint = ColorConstraint() + + self._red = red or 0 + self._green = green or 0 + self._blue = blue or 0 + + self.set([self._red, self._green, self._blue]) + +class TFileProperty(TutoriusProperty): + """ + Represents a path to a file on the disk. + """ + def __init__(self, path): + """ + Defines the path to an existing file on disk file. + + For now, the path may be relative or absolute, as long as it exists on + the local machine. + TODO : Make sure that we have a file scheme that supports distribution + on other computers (LP 355197) + """ + TutoriusProperty.__init__(self) + + self._type = "file" + + self.file_constraint = FileConstraint() + + self.set(path) + +class TEnumProperty(TutoriusProperty): + """ + Represents a value in a given enumeration. This means that the value will + always be one in the enumeration and nothing else. + + """ + def __init__(self, value, accepted_values): + """ + Creates the enumeration property. + + @param value The initial value of the enum. Must be part of + accepted_values + @param accepted_values A list of values that the property can take + """ + TutoriusProperty.__init__(self) + + self._type = "enum" + + self.enum_constraint = EnumConstraint(accepted_values) + + self.set(value) + +class TBooleanProperty(TutoriusProperty): + """ + Represents a True of False value. + """ + def __init__(self, value=False): + TutoriusProperty.__init__(self) + + self._type = "boolean" + + self.boolean_constraint = BooleanConstraint() + + self.set(value) + +class TUAMProperty(TutoriusProperty): + """ + Represents a widget of the interface by storing its UAM. + """ + # TODO : Pending UAM check-in (LP 355199) + pass diff --git a/src/sugar/tutorius/tests/actiontests.py b/src/sugar/tutorius/tests/actiontests.py new file mode 100644 index 0000000..ab9cdba --- /dev/null +++ b/src/sugar/tutorius/tests/actiontests.py @@ -0,0 +1,173 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com> +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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.actions import * +from sugar.tutorius.services import ObjectStore + +class PropertyAction(Action): + def __init__(self, na): + self._a = na + + def set_a(self, na): + self._a = na + + def get_a(self): + return self._a + + a = property(fget=get_a, fset=set_a) + +class PropsTest(unittest.TestCase): + + def test_get_properties(self): + prop = PropertyAction(8) + + assert prop.get_properties() == ['a'], "Action does not contain property 'a'" + +class CountAction(Action): + """ + This action counts how many times it's do and undo methods get called + """ + def __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): + 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/src/sugar/tutorius/tests/constraintstests.py b/src/sugar/tutorius/tests/constraintstests.py new file mode 100644 index 0000000..407cc24 --- /dev/null +++ b/src/sugar/tutorius/tests/constraintstests.py @@ -0,0 +1,211 @@ +# 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 SizeConstraintTest(unittest.TestCase): + def test_empty_constraint(self): + cons = SizeConstraint(None) + try: + cons.validate(20) + except SizeConstraintError: + assert False, "Empty contraint should not raise an exception" + + def test_validate(self): + cons = SizeConstraint(10) + + try: + cons.validate(range(0, 20)) + assert False, "Validation of SizeLimit(10) on list of length 20 should raise an exception" + except SizeConstraintError: + pass + + try: + cons.validate(range(0,5)) + except SizeConstraintError: + assert True, "Validation of SizeLimit(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/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py index ec730f2..5f91a64 100644 --- a/src/sugar/tutorius/tests/coretests.py +++ b/src/sugar/tutorius/tests/coretests.py @@ -18,18 +18,24 @@ 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 +Core. This means that the the Finite State Machine, States 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 +from sugar.tutorius.actions import Action, ClickAction, TypeTextAction from sugar.tutorius.core import * from sugar.tutorius.filters import * + +from actiontests import CountAction + # Helper classes to help testing class SimpleTutorial(Tutorial): """ @@ -58,21 +64,46 @@ class TrueWhileActiveAction(Action): 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 CountAction(Action): +class FakeParentWidget(): """ - This action counts how many times it's do and undo methods get called + This class fakes a widet container, it implements the get_children() method """ def __init__(self): - self.do_count = 0 - self.undo_count = 0 + self._children = [] + + def add_child(self, child): + self._children.append(child) + + def get_children(self): + return self._children + - def do(self): - self.do_count += 1 - def undo(self): - self.undo_count += 1 class TriggerEventFilter(EventFilter): """ @@ -110,49 +141,96 @@ class FakeEventFilter(TriggerEventFilter): self.tutorial.set_state(event_filter.get_next_state()) -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" +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): - 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" + 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 + + - wrap.do() - assert act.do_count == 1, "do() should have been called only once" +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 - wrap.undo() - assert act.undo_count == 1, "undo() should have been called once" + 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" - wrap.undo() - assert act.undo_count == 1, "undo() should have been called only once" + 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): diff --git a/src/sugar/tutorius/tests/filterstests.py b/src/sugar/tutorius/tests/filterstests.py new file mode 100644 index 0000000..8ee6cc8 --- /dev/null +++ b/src/sugar/tutorius/tests/filterstests.py @@ -0,0 +1,200 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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, GtkWidgetEventFilter, GtkWidgetTypeFilter +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 = 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 = 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/src/sugar/tutorius/tests/gtkutilstests.py b/src/sugar/tutorius/tests/gtkutilstests.py index fb9a20b..41634ae 100644 --- a/src/sugar/tutorius/tests/gtkutilstests.py +++ b/src/sugar/tutorius/tests/gtkutilstests.py @@ -26,16 +26,22 @@ import unittest import logging import gtk, gobject -from sugar.tutorius.gtkutils import find_widget, register_signals_numbered, register_signals +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): - self.data = [] + """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) @@ -97,6 +103,8 @@ class GtkUtilsTests(unittest.TestCase): 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 @@ -104,12 +112,36 @@ class GtkUtilsTests(unittest.TestCase): assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list) - while gtk.events_pending(): - gtk.main_iteration(block=False) + 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 @@ -117,15 +149,62 @@ class GtkUtilsTests(unittest.TestCase): assert len(widget_list) == 1, "register_signals should not have recursed (%d objects registered)" % len(widget_list) - while gtk.events_pending(): - gtk.main_iteration(block=False) + 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/src/sugar/tutorius/tests/linear_creatortests.py b/src/sugar/tutorius/tests/linear_creatortests.py index 3bc06f9..f9ffbe7 100644 --- a/src/sugar/tutorius/tests/linear_creatortests.py +++ b/src/sugar/tutorius/tests/linear_creatortests.py @@ -19,8 +19,8 @@ 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, CountAction - +from coretests import TriggerEventFilter +from actiontests import CountAction import unittest class CreatorTests(unittest.TestCase): @@ -64,6 +64,16 @@ class CreatorTests(unittest.TestCase): 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()
\ No newline at end of file + unittest.main() diff --git a/src/sugar/tutorius/tests/propertiestests.py b/src/sugar/tutorius/tests/propertiestests.py new file mode 100644 index 0000000..52a9a75 --- /dev/null +++ b/src/sugar/tutorius/tests/propertiestests.py @@ -0,0 +1,348 @@ +# 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(prop): + if prop.type != "int": + try: + prop.set(3) + assert False, "Able to insert int value in property of type %s"%prop.type + except: + pass + + if prop.type != "float": + try: + prop.set(1.1) + assert False, "Able to insert float value in property of type %s"%prop.type + except: + pass + + if prop.type != "string": + try: + prop.set("Fake string") + assert False, "Able to insert string value in property of type %s"%prop.type + except: + pass + + if prop.type != "array": + try: + prop.set([1, 2000, 3, 400]) + assert False, "Able to insert array value in property of type %s"%prop.type + except: + pass + + if prop.type != "color": + try: + prop.set([1,2,3]) + if prop.type != "array": + assert False, "Able to insert color value in property of type %s"%prop.type + except: + pass + + if prop.type != "boolean": + try: + prop.set(True) + if prop.type != "boolean": + assert False, "Able to set boolean value in property of type %s"%prop.type + except: + pass + +class BasePropertyTest(unittest.TestCase): + def test_base_class(self): + prop = TutoriusProperty() + + assert prop.value == None, "There should not be an initial value in the base property" + + assert prop.type == None, "There should be no type associated with the base property" + + assert prop.get_constraints() == [], "There should be no constraints on the base property" + + prop.set(2) + + assert prop.value == 2, "Unable to set a value on base class" + +class TIntPropertyTest(unittest.TestCase): + def test_int_property(self): + prop = TIntProperty(22) + + assert prop.value == 22, "Could not set value on property via constructor" + + assert prop.type == "int", "Wrong type on int property : %s" % prop.type + cons = prop.get_constraints() + assert len(cons) == 2, "Not enough constraints on the int property" + + prop.set(12) + + assert prop.value == 12, "Could not set value" + + def test_wrong_values(self): + prop = TIntProperty(33) + + # Try setting values of other types + try_wrong_values(prop) + + def test_limit_constructor(self): + prop = TIntProperty(22, 0, 30) + + try: + prop.set(-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: + prop.set(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): + prop = TFloatProperty(22) + + assert prop.value == 22, "Could not set value on property via constructor" + + assert prop.type == "float", "Wrong type on float property : %s" % prop.type + cons = prop.get_constraints() + assert len(cons) == 2, "Not enough constraints on the float property" + + prop.set(12) + + assert prop.value == 12, "Could not set value" + + def test_wrong_values(self): + prop = TFloatProperty(33) + # Try setting values of other types + try_wrong_values(prop) + + def test_limit_constructor(self): + prop = TFloatProperty(22.4, 0.1, 30.5223) + + try: + prop.set(-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: + prop.set(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): + prop = TStringProperty("Starter string") + + assert prop.value == "Starter string", "Could not set string value via constructor" + + assert prop.type == "string", "Wrong type for string property : %s" % prop.type + + def test_size_limit(self): + prop = TStringProperty("Small", 10) + + try: + prop.set("My string is too big!") + assert False, "String should not set to longer than max size" + except SizeConstraintError: + pass + except: + assert False, "Wrong exception type thrown" + + def test_wrong_values(self): + prop = TStringProperty("Beginning") + + try_wrong_values(prop) + + def test_failing_constructor(self): + try: + prop = TStringProperty("This is normal", 5) + assert False, "Creation of the property should fail." + except SizeConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TArrayPropertyTest(unittest.TestCase): + def test_basic_array(self): + prop = TArrayProperty([1, 2, 3, 4]) + + assert prop.value == [1,2,3,4], "Unable to set initial value via constructor" + + assert prop.type == "array", "Wrong type for array : %s"%prop.type + + def test_wrong_values(self): + prop = TArrayProperty([1,2,3,4,5]) + + try_wrong_values(prop) + + def test_size_limit(self): + prop = TArrayProperty([1,2], 4) + + try: + prop.set([1,2,4,5,6,7]) + assert False, "Size limit constraint was not properly applied" + except SizeConstraintError: + pass + except: + assert False, "Wrong type of exception thrown" + + + def test_failing_constructor(self): + try: + prop = TArrayProperty([100, 0, 20], 2) + assert False, "Creation of the property should fail." + except SizeConstraintError: + pass + except: + assert False, "Wrong exception type on failed constructor" + +class TColorPropertyTest(unittest.TestCase): + def test_basic_color(self): + prop = TColorProperty(20, 40, 60) + + assert prop.value == [20, 40, 60], "Could not set initial value with constructor" + + assert prop.type == "color", "Wrong type on color : %s"%prop.type + + def test_wrong_values(self): + prop = TColorProperty(250, 250, 250) + + try_wrong_values(prop) + + 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): + self.prop = TBooleanProperty(False) + + def test_basic_boolean(self): + assert self.prop.value == False, "Could not set initial value via constructor" + + self.prop.set(True) + + assert self.prop.value == True, "Could not change the value via set" + + self.prop.set(False) + + assert self.prop.value == False, "Could not change the value via set" + + def test_wrong_types(self): + try_wrong_values(self.prop) + + 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): + self.prop = TEnumProperty("hello", [1, 2, "hello", "world", True, None]) + + def test_basic_enum(self): + assert self.prop.value == "hello", "Could not set initial value on property" + + self.prop.set(True) + + assert self.prop.value, "Could not change the value via set" + + try: + self.prop.set(4) + assert False, "Switched to a value outside the enum" + except EnumConstraintError: + pass + + def test_wrong_type(self): + try_wrong_values(self.prop) + +class TFilePropertyTest(unittest.TestCase): + def setUp(self): + self.prop = TFileProperty("propertiestests.py") + + def test_basic_file(self): + assert self.prop.value == "propertiestests.py", "Could not set initial value" + + self.prop.set("run-tests.py") + + assert self.prop.value == "run-tests.py", "Could not change value" + + try: + self.prop.set("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/src/sugar/tutorius/tests/uamtests.py b/src/sugar/tutorius/tests/uamtests.py new file mode 100644 index 0000000..b2a5901 --- /dev/null +++ b/src/sugar/tutorius/tests/uamtests.py @@ -0,0 +1,61 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Michael Janelle-Montcalm <michael.jmontcalm@gmail.com> +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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() + diff --git a/src/sugar/tutorius/uam/Makefile.am b/src/sugar/tutorius/uam/Makefile.am new file mode 100644 index 0000000..219291e --- /dev/null +++ b/src/sugar/tutorius/uam/Makefile.am @@ -0,0 +1,5 @@ +sugardir = $(pythondir)/sugar/tutorius/uam +sugar_PYTHON = \ + gobjectparser.py \ + gtkparser.py \ + __init__.py diff --git a/src/sugar/tutorius/uam/__init__.py b/src/sugar/tutorius/uam/__init__.py new file mode 100644 index 0000000..7cf5671 --- /dev/null +++ b/src/sugar/tutorius/uam/__init__.py @@ -0,0 +1,88 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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 +""" +Universal Addressing Mechanism module + +Allows addressing Events, signals, widgets, etc for supported platforms +""" + +from urllib2 import urlparse + +import gtkparser +import gobjectparser + + +SCHEME="tap" #Tutorius Adressing Protocol + +__parsers = { + gtkparser.SCHEME:gtkparser.parse_gtk, + gobjectparser.SCHEME:gobjectparser.parse_gobject, +} + +def __add_to_urlparse(name): + #Add to uses_netloc + if not name in urlparse.uses_netloc: + urlparse.uses_netloc.append(name) + + #Add to uses_relative + if not name in urlparse.uses_relative: + urlparse.uses_relative.append(name) + +# #Add to uses_params +# if not name in urlparse.uses_params: +# urlparse.uses_params.append(name) + + #Add to uses_query + if not name in urlparse.uses_query: + urlparse.uses_query.append(name) + + #Add to uses_frament + if not name in urlparse.uses_fragment: + urlparse.uses_fragment.append(name) + + +#Add schemes to urlparse +__add_to_urlparse(SCHEME) + +for subscheme in [".".join([SCHEME,s]) for s in __parsers]: + __add_to_urlparse(subscheme) + + +class SchemeError(Exception): + def __init__(self, message): + Exception.__init__(self, message) + self.message = message + + +def parse_uri(uri): + res = urlparse.urlparse(uri) + + scheme = res.scheme.split(".")[0] + subscheme = ".".join(res.scheme.split(".")[1:]) + if not scheme == SCHEME: + raise SchemeError("Scheme %s not supported" % scheme) + + if subscheme != "" and not subscheme in __parsers: + raise SchemeError("SubScheme %s not supported" % subscheme) + + if subscheme: + return __parsers[subscheme](res) + + return res + + + diff --git a/src/sugar/tutorius/uam/gobjectparser.py b/src/sugar/tutorius/uam/gobjectparser.py new file mode 100644 index 0000000..c1fba3d --- /dev/null +++ b/src/sugar/tutorius/uam/gobjectparser.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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 +""" +UAM Parser for gobject subscheme + +To be completed +""" + +SCHEME="gobject" + +def parse_gobject(parsed_uri): + """Do nothing for now""" + return parsed_uri diff --git a/src/sugar/tutorius/uam/gtkparser.py b/src/sugar/tutorius/uam/gtkparser.py new file mode 100644 index 0000000..ede2f03 --- /dev/null +++ b/src/sugar/tutorius/uam/gtkparser.py @@ -0,0 +1,44 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> +# +# 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 +""" +UAM Parser for gtk subscheme + +Allows addressing Gtk Events, signals, widgets + +The gtk subscheme for tutorius is + +<scheme>://<activity>/<path>[?<params>#<ptype>] + +where: + +<scheme> is the uam.SCHEME + "." + SCHEME + +<activity> is the activity's dns identifier, such as battleship.tutorius.org + +<path> is the Hierarchical path to the widget, where 0 is the activity, such as /0/0/1/0/1/0 + +<params> can be used to specify additionnal parameters required for an event handler or action, such as event=clicked + +<ptype> must be used with params to specify which action or eventfilter to use, such as "DialogMessage" + +""" + +SCHEME="gtk" + +def parse_gtk(parsed_uri): + """Do nothing for now""" + return parsed_uri |