From e784a2f0b92ecaf46a77ddca94b31fcc86e0cbae Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 17 Apr 2009 04:36:31 +0000 Subject: Merge branch 'jc_support' into mike Conflicts: source/external/source/sugar-toolkit/src/sugar/tutorius/tests/coretests.py source/external/source/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py --- diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py index 21e38f6..a5188fd 100644 --- a/src/sugar/activity/activity.py +++ b/src/sugar/activity/activity.py @@ -126,11 +126,22 @@ class ActivityToolbar(gtk.Toolbar): if hasattr(self._activity,"get_tutorials") and hasattr(self._activity.get_tutorials,"__call__"): self.tutorials = ToolComboBox(label_text=_('Tutorials:')) self.tutorials.combo.connect('changed', self.__tutorial_changed_cb) - tutorials = self._activity.get_tutorials() + # Get tutorial list by file + logging.debug("************************************ before creating serialize") +## serialize = TutoSerializer() +## logging.debug("************************************ before calling load_tuto_list()") +## +## #tutorials = self._activity.get_tutorials() +## if getattr(self._activity,"_tutorials",None) is None: +## tutorials = serialize.load_tuto_list() + self._current_tutorial = None + tutorials = None if tutorials: for key, tutorial in tutorials.items(): - self.tutorials.combo.append_item(key, tutorial.name) + # self.tutorials.combo.append_item(key, _(tutorial.name)) + self.tutorials.combo.append_item(key, _(tutorial)) + self.insert(self.tutorials, -1) self.tutorials.show() @@ -196,16 +207,34 @@ class ActivityToolbar(gtk.Toolbar): """ Callback for tutorial combobox item change """ - model = combo.get_model() - it = combo.get_active_iter() - (key, ) = model.get(it, 0) - tutorial = self._activity.get_tutorials().get(key,None) - if not tutorial is None: + logging.debug("************ function __tutorial_changed_cb called") + serialize = TutoSerializer() + + if self._current_tutorial: + self._current_tutorial.detach() + + model = self.tutorials.combo.get_model() + it = self.tutorials.combo.get_active_iter() + (key,) = model.get(it, 0) + + #Load and build chosen tutorial from Pickle file + logging.debug("****************** before tuto build") +## tutorials = self._activity.get_tutorials() + tuto = serialize.build_tutorial(key) + self._activity._tutorials = tuto + logging.debug("****************** after tuto build") +## tutorial = self._activity.get_tutorials().get(key,None) + tutorial = tuto.get(key, None) + + if not getattr(self._activity,"_tutorials",None) is None: if not self._current_tutorial is None: self._current_tutorial.detach() + self._current_tutorial = tutorial + logging.debug(" *************** try to attach tuto") self._current_tutorial.attach(self._activity) + def __keep_clicked_cb(self, button): self._activity.copy() diff --git a/src/sugar/tutorius/Makefile.am b/src/sugar/tutorius/Makefile.am index 02f832b..8805314 100644 --- a/src/sugar/tutorius/Makefile.am +++ b/src/sugar/tutorius/Makefile.am @@ -1,5 +1,3 @@ -SUBDIRS = uam - sugardir = $(pythondir)/sugar/tutorius sugar_PYTHON = \ __init__.py \ @@ -11,6 +9,5 @@ sugar_PYTHON = \ services.py \ overlayer.py \ editor.py \ - linear_creator.py \ - constraints.py \ - properties.py + linear_creator.py\ + bundler.py diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py index fc364e0..d81e3c2 100644 --- a/src/sugar/tutorius/actions.py +++ b/src/sugar/tutorius/actions.py @@ -222,3 +222,56 @@ class DisableWidgetAction(Action): """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 585ba88..a28d6ef 100644 --- a/src/sugar/tutorius/bundler.py +++ b/src/sugar/tutorius/bundler.py @@ -21,33 +21,89 @@ This module contains all the data handling class of Tutorius """ import logging -import linecache import os +import uuid +import xml.dom.minidom +import xml.dom.ext from sugar.tutorius import gtkutils, overlayer from sugar.tutorius.core import Tutorial, State, FiniteStateMachine -from sugar.tutorius.actions import DialogMessage, OnceWrapper, BubbleMessage -from sugar.tutorius.filters import GtkWidgetEventFilter, TimerEvent +import sugar.tutorius.actions +import sugar.tutorius.filters +from ConfigParser import SafeConfigParser + +def __get_store_root(): + return os.path.join(os.getenv("SUGAR_PREFIX"),"share","tutorius","data") +def __get_bundle_root(): + return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data") + +INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES" +INI_METADATA_SECTION = "GENERAL_METADATA" +INI_GUID_PROPERTY = "GUID" +INI_NAME_PROPERTY = "NAME" +INI_XML_FSM_PROPERTY = "FSM_FILENAME" class TutorialStore: - def list_avaible_tutorials(self, activity_name): + def list_avaible_tutorials(self, activity_name, activity_vers): """ Recuperate the list of all tutorials present on disk for a given activity. """ - NotImplementedError + + store_root = __get_store_root() + bundle_root = __get_bundle_root() + + logging.debug("*********** Path of store_root : " + store_root) + + # Create /data/tutorius if no exists + if not os.path.exists(store_root): + os.mkdir(store_root) + logging.debug("************* Creating %s folder" % store_root) + + tutoGuidName = {} + + # iterate in each GUID subfolder + for dir in os.listdir(store_root): + # iterate for each ".ini" file in the activity store_root folder + for file_name in os.listdir(store_root + "/" + dir): + + if file_name.endswith(".ini"): + logging.debug("************** .ini file found : " + file_name) + # Filter for just .ini files who metadata ACTIVITY_NAME + # match 'activity_name' given in argument. + config = SafeConfigParser() + config.read(file_name) + # Get all activity tuples (Activity_Name: Activity_Version) + file_activity_tuples = config.items(INI_ACTIVITY_SECTION) + + for i in range(0, len(file_activity_tuples) - 1): + + if file_activity_tuples[i][0] == activity_name and \ + int(file_activity_tuples[i][1]) == activity_vers: + # Add this tutorial guid and name in the dictionary + file_activity_guid = config.get(INI_METADATA_SECTION, + INI_GUID_PROPERTY) + file_activity_name = config.get(INI_METADATA_SECTION, + INI_NAME_PROPERTY) + tutoGuidName[file_activity_name] = file_activity_guid + + return tutoGuidName class Serializer: """ - Class that provide serializing and deserializing of the FSM - used in the tutorials to/from disk. + Interface that provide serializing and deserializing of the FSM + used in the tutorials to/from disk. Must be inherited. """ - def save_fsm(self,fsm): + def save_fsm(self,fsm, guid = None): """ - Save fsm to disk. + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are overwritten. If the GUId is not found, an + exception occur. If no GUID is provided, FSM is written in a new file + in the store root. """ NotImplementedError @@ -60,43 +116,381 @@ class Serializer: class XMLSerializer(Serializer): """ Class that provide serializing and deserializing of the FSM - used in the tutorials to/from a xml file. + used in the tutorials to/from a .xml file. Inherit from Serializer """ - def save_fsm(self,fsm): + def create_state_dict_node(self, state_dict, doc): """ - Save fsm to .xml file + Create and return a xml Node from a State dictionnary. """ - NotImplementedError + statesList = doc.createElement("States") + for state_name, state in state_dict.items(): + stateNode = statesList.appendChild("State") + stateNode.setAttribute("State:Name", state_name) + stateNode = stateNode.appendChild(create_action_list_node(state.action_list, doc)) + stateNode = stateNode.appendChild(create_event_filters_node(state.event_filters, doc)) + return statesList + + def create_action_list_node(self, action_list, doc): + """ + Create and return a xml Node from a Action list. + """ + actionsList = doc.createElement("Actions") + for action in action_list: + actionNode = actionsList.appendChild("Action") + if type(action) is DialogMessage: + actionNode.setAttribute("Action:Class", type(action)) + actionNode.setAttribute("Action:Message", action.message) + actionNode.setAttribute("Action:Position", action.position) + elif type(action) is BubbleMessage: + actionNode.setAttribute("Action:Class", str(type(action))) + actionNode.setAttribute("Action:Message", action.message) + actionNode.setAttribute("Action:Position", action.position) + actionNode.setAttribute("Action:Tail_pos", action.tail_pos) + # TODO : elif for each type of action + elif type(action) is WidgetIdentifyAction: + actionNode.setAttribute("Action:Class", str(type(action))) + # TODO + elif type(action) is ChainAction: + # TODO + actionNode.setAttribute("Action:Class", str(type(action))) + elif type(action) is DisableWidgetAction: + # TODO + actionNode.setAttribute("Action:Class", str(type(action))) + elif type(action) is TypeTextAction: + # TODO + actionNode.setAttribute("Action:Class", str(type(action))) + elif type(action) is ClickAction: + # TODO + actionNode.setAttribute("Action:Class", str(type(action))) + + return actionsList + + def create_event_filters_node(self, event_filters, doc): + """ + Create and return a xml Node from a event filters. + """ + eventFiltersList = doc.createElement("EventFiltersList") + for event_f in event_filters: + eventFilterNode = eventFiltersList.appendChild("EventFilter") + # TODO : elif for each type of event filters + if type(event_f) is TimerEvent: + # TODO + eventFilterNode.setAttribute("EventFilter:Class", str(type(event_f))) + elif type(event_f) is GtkWidgetEventFilter: + # TODO + eventFilterNode.setAttribute("EventFilter:Class", str(type(event_f))) + elif type(event_f) is GtkWidgetTypeFilter: + # TODO + eventFilterNode.setAttribute("EventFilter:Class", str(type(event_f))) + + return eventFiltersList + def save_fsm(self, fsm, xml_filename, path): + """ + Save fsm to disk, in the xml file specified by "xml_filename", in the + "path" folder. If the specified file doesn't exist, it will be created. + """ + doc = xml.dom.minidom.Document() + fsm_element = doc.createElement("FSM") + doc.appendChild(fsm_element) + fsm_element.setAttribute("fsm:Name", fsm.name) + fsm_element.setAttribute("fsm:StartStateName", fsm.start_state_name) + fsm_element = fsm_element.appendChild(create_state_dict_node(fsm.state_dict, doc)) + fsm_element = fsm_element.appendChild(create_action_list_node(fsm.action_list, doc)) + + file_object = open(path + "/" + xml_filename, "w") + xml.dom.ext.PrettyPrint(doc, file_object) + file_object.close() + + + def _find_tutorial_dir_with_guid(self, guid): + """ + Finds the tutorial with the associated GUID. If it is found, return + the path to the tutorial's directory. If it doesn't exist, raise an + IOError. + + A note : if there are two tutorials with this GUID in the folders, + they will both be inspected and the one with the highest version + number will be returned. If they have the same version number, the one + from the global store will be returned. + + @param guid The GUID of the tutorial that is to be loaded. + """ + # Attempt to find the tutorial's directory in the global directory + global_dir = os.path.join(__get_store_root(), guid) + # Then in the activty's bundle path + activity_dir = os.path.join(__get_bundle_root(), guid) + + # If they both exist + if os.path.isdir(global_filename) and os.path.isdir(activity_dir): + # Inspect both metadata files + global_meta = os.path.join(global_dir, "meta.ini") + activity_meta = os.path.join(activity_dir, "meta.ini") + + # Open both config files + global_parser = SafeConfigParser() + global_parser.read(global_meta) + + activity_parser = SafeConfigParser() + activity_parser.read(activity_meta) + + # Get the version number for each tutorial + global_version = global_parser.get(INI_METADATA_SECTION, "version") + activity_version = activity_parser.get(INI_METADATA_SECTION, "version") + + # If the global version is higher or equal, we'll take it + if global_version >= activity_version: + return global_dir + else: + return activity_dir + + # Do we just have the global directory? + if os.path.isdir(global_dir): + return global_dir + + # Or just the activity's bundle directory? + if os.path.isdir(activity_dir): + return activity_dir + + # Error : none of these directories contain the tutorial + raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID"%guid) + + def _load_xml_properties(self, properties_elem): + """ + Changes a list of properties into fully instanciated properties. + + @param properties_elem An XML element reprensenting a list of + properties + """ + return [] + + def _load_xml_event_filters(self, filters_elem): + """ + Loads up a list of Event Filters. + + @param filters_elem An XML Element representing a list of event filters + """ + return [] + + def _load_xml_actions(self, actions_elem): + """ + Transforms an Actions element into a list of instanciated Action. + + @param actions_elem An XML Element representing a list of Actions + """ + return [] + + def _load_xml_states(self, states_elem): + """ + Takes in a States element and fleshes out a complete list of State + objects. + + @param states_elem An XML Element that represents a list of States + """ + return [] + + def _load_xml_fsm(self, fsm_elem): + """ + Takes in an XML element representing an FSM and returns the fully + crafted FSM. + + @param fsm_elem The XML element that describes a FSM + """ + # Load the FSM's name and start state's name + fsm_name = fsm_elem.getAttribute("fsm:Name") + + fsm_start_state_name = None + try: + fsm_start_state_name = fsm_elem.getAttribute("fsm:StartStateName") + except: + pass + + fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name) + + # Load the states + states = self._load_xml_states(fsm_elem.getElementsByName("States")) + for state in states: + fsm.add_state(state) + + # Load the actions on this FSM + actions = self._load_xml_actions(fsm_elem.getElementsByName("Actions")) + for action in actions: + fsm.add_action(action) + + # Load the event filters + events = self._load_xml_event_filters(fsm_elem.getElementsByName("EventFiltersList")) + for event in events: + fsm.add_event_filter(event) + + return fsm + def load_fsm(self, guid): """ - Load fsm from xml file who .ini file guid match argument guid. + Load fsm from xml file whose .ini file guid match argument guid. """ - NotImplementedError + # Fetch the directory (if any) + tutorial_dir = self._find_tutorial_dir_with_guid(guid) + + # Open the XML file + tutorial_file = os.path.join(tutorial_dir, "fsm.xml") + + xml_dom = xml.dom.minidom.parse(tutorial_file) + fsm_elem = xml_dom.getElementsByTagName("fsm")[0] + return self._load_xml_fsm(fsm_elem) + + class TutorialBundler: """ This class provide the various data handling methods useable by the tutorial editor. """ - def write_metadata_file(data): + def __init__(self,generated_guid = None): """ - Write metadata to a .ini file. + TODO. + Tutorial_bundler constructor. If a GUID is given in the parameter, the + Tutorial_bundler object will be associated with it. If no GUID is given, + a new GUID will be generated, """ - NotImplementedError - - def write_fsm(fsm): + + self.Guid = generated_guid or uuid.uuid1() + + #Look for the file in the path if a uid is supplied + if generated_guid: + #General store + store_path = os.path.join(__get_store_root(), generated_guid, INI_FILENAME) + if os.path.isfile(store_path): + self.Path = os.path.dirname(store_path) + else: + #Bundle store + bundle_path = os.path.join(__get_bundle_root(), generated_guid, INI_FILENAME) + if os.path.isfile(bundle_path): + self.Path = os.path.dirname(bundle_path) + else: + raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid) + + else: + #Create the folder, any failure will go through to the caller for now + store_path = os.path.join(__get_store_root(), generated_guid) + os.mkdir(store_path) + self.Path = store_path + + + def __SetGuid(self, value): + self.__guid = value + + def __GetGuid(self): + return self.__guid + + def __DelGuid(self): + del self.__guid + + def __SetPath(self, value): + self.__path = value + + def __GetPath(self): + return self.__path + + def __DelPath(self): + del self.__path + + Guid = property(fget=__SetGuid, + fset=__GetGuid, + fdel=__DelGuid, + doc="The guid associated with the Tutoria_Bundler") + + Path = property(fget=__SetPath, + fset=__GetPath, + fdel=__DelPath, + doc="The path associated with the Tutoria_Bundler") + + + def write_metadata_file(self, data): """ - Write fsm to disk. + Write metadata to a property file. If a GUID is provided, TutorialBundler + will try to find and overwrite the existing property file who contain the + given GUID, and will raise an exception if it cannot find it. """ NotImplementedError - def add_resources(typename, file): + def get_tutorial_path(self): + """ + Return the path of the .ini file associated with the guiven guid set in + the Guid property of the Tutorial_Bundler. If the guid is present in + more than one path, the store_root is given priority. + """ + + store_root = __get_store_root() + bundle_root = __get_bundle_root() + + config = SafeConfigParser() + path = None + + logging.debug("************ Path of store_root folder of activity : " \ + + store_root) + + # iterate in each GUID subfolder + for dir in os.listdir(store_root): + + # iterate for each .ini file in the store_root folder + + for file_name in os.listdir(store_root + "/" + dir): + if file_name.endswith(".ini"): + logging.debug("******************* Found .ini file : " \ + + file_name) + config.read(file_name) + if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == self.Guid: + xml_filename = config.get(INI_METADATA_SECTION, + INI_XML_FSM_PROPERTY) + + path = os.path.join(store_root, self.Guid) + return path + + logging.debug("************ Path of bundle_root folder of activity : " \ + + bundle_root) + + + # iterate in each GUID subfolder + for dir in os.listdir(bundle_root): + + # iterate for each .ini file in the bundle_root folder + for file_name in os.listdir(bundle_root + "/" + dir): + if file_name.endswith(".ini"): + logging.debug("******************* Found .ini file : " \ + + file_name) + config.read(file_name) + if config.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) == guid: + path = os.path.join(bundle_root, self.Guid) + return path + + if path is None: + logging.debug("**************** Error : GUID not found") + raise KeyError + + def write_fsm(self, fsm, guid=None): + + """ + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are created or overwritten. If the GUID is not + found, an exception occur. + """ + + config = SafeConfigParser() + + if guid is not None: + path = get_tutorial_path() + "/meta.ini" + config.read(path) + xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) + save_fsm(fsm, xml_filename, store_root) + + + def add_resources(self, typename, file): """ Add ressources to metadata. """ - NotImplementedError \ No newline at end of file + raise NotImplementedError("add_resources not implemented") diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py index 4d53684..602947f 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/services.py b/src/sugar/tutorius/services.py index 467eca0..9ed2e50 100644 --- a/src/sugar/tutorius/services.py +++ b/src/sugar/tutorius/services.py @@ -66,3 +66,4 @@ class ObjectStore(object): tutorial = property(fset=set_tutorial,fget=get_tutorial,doc="tutorial") __doc__ = __ObjectStore.__doc__ + diff --git a/src/sugar/tutorius/temp.py b/src/sugar/tutorius/temp.py new file mode 100644 index 0000000..6c7fb85 --- /dev/null +++ b/src/sugar/tutorius/temp.py @@ -0,0 +1 @@ +Ex GUID : 79521158-22b4-11de-8cfa-000c293a027a \ No newline at end of file diff --git a/src/sugar/tutorius/tests/bundlertests.py b/src/sugar/tutorius/tests/bundlertests.py new file mode 100644 index 0000000..8da2310 --- /dev/null +++ b/src/sugar/tutorius/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/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py index 1d864f9..c27846d 100644 --- a/src/sugar/tutorius/tests/coretests.py +++ b/src/sugar/tutorius/tests/coretests.py @@ -29,8 +29,7 @@ and event filters. Those are in their separate test module import unittest import logging -import sugar.activity -from sugar.tutorius.actions import Action, OnceWrapper +from sugar.tutorius.actions import Action, OnceWrapper, ClickAction, TypeTextAction from sugar.tutorius.core import * from sugar.tutorius.filters import * @@ -73,6 +72,44 @@ 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 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 + @@ -112,7 +149,96 @@ class FakeEventFilter(TriggerEventFilter): 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): diff --git a/src/sugar/tutorius/tests/run-tests.py b/src/sugar/tutorius/tests/run-tests.py index 87edd57..1fc534e 100755 --- a/src/sugar/tutorius/tests/run-tests.py +++ b/src/sugar/tutorius/tests/run-tests.py @@ -14,7 +14,6 @@ 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: @@ -40,6 +39,7 @@ if __name__=='__main__': import filterstests import constraintstests import propertiestests + import serializertests suite = unittest.TestSuite() suite.addTests(unittest.findTestCases(coretests)) suite.addTests(unittest.findTestCases(servicestests)) @@ -51,10 +51,10 @@ if __name__=='__main__': 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() @@ -70,5 +70,6 @@ if __name__=='__main__': from constraintstests import * from propertiestests import * from actiontests import * + from serializertests import * unittest.main() diff --git a/src/sugar/tutorius/tests/serializertests.py b/src/sugar/tutorius/tests/serializertests.py new file mode 100644 index 0000000..a53e196 --- /dev/null +++ b/src/sugar/tutorius/tests/serializertests.py @@ -0,0 +1,122 @@ +# 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 logging +import linecache +import os +import cPickle as pickle + +from sugar.tutorius import gtkutils, overlayer +from sugar.tutorius.core import Tutorial, State, FiniteStateMachine +from sugar.tutorius.actions import DialogMessage, OnceWrapper, BubbleMessage +from sugar.tutorius.filters import GtkWidgetEventFilter, TimerEvent + + +# Helper classes to help testing + + + + +class SerializerTest(unittest.TestCase): + """ + This class has to test the Serializer methods as well as the expected + functionality. + """ + + # Voiding test as it is meant to be used with the pickle serializer, + # that was deprecated +## def test_pickle_integrity(self): +## """ +## Validates content is uncorrupted trough a pickle file save/load. +## """ +## +## # Sample valid FSM dict +## sampleDict = { +## "INIT":State("INIT", +## action_list=[ +## OnceWrapper(BubbleMessage(message="Welcome to the text editor tutorial!\n\n Click on the canvas and type a letter.", pos=[100,100], tailpos=[-10,-20])), +## ], +## event_filter_list=[ +## GtkWidgetEventFilter("TEXT","0.0.0.1.0.0.0","key-press-event"), +## TimerEvent("LOST",15), +## ], +## ), +## "LOST":State("LOST", +## action_list=[BubbleMessage("Click in the canvas and type on your keyboard", [400, 400]),], +## event_filter_list=[ +## GtkWidgetEventFilter("TEXT","0.0.0.1.0.0.0","key-press-event"), +## TimerEvent("INIT",5), +## ], +## ), +## "TEXT":State("TEXT", +## action_list=[OnceWrapper(BubbleMessage(" You can type more letters if you want!\n\n" + +## "To proceed to the next step, select your text.\n\n Click and drag over the text!", [200,150])),], +## event_filter_list=[ +## GtkWidgetEventFilter("SELECTED","0.0.0.1.0.0","text-selected"), +## ], +## ), +## } +## +## testpath = "/tmp/testdata/" +## +## # Create testdata/ folder if no exists +## if not os.path.exists(testpath): +## os.mkdir(testpath) +## +## serialize = TutoSerializer() +## +## # Make the class believe the test is in a activity path +## os.environ["SUGAR_ACTIVITY_ROOT"] = testpath +## +## fsm = FiniteStateMachine("Test", state_dict=sampleDict) +## +## serialize.save_tutorial("Test", "Test", fsm, "serializeTest") +## +## fileDict = serialize.load_tuto_list() +## +## for filekey, tutorial in fileDict.items(): +## if filekey == "Test": +## reformedTuto = serialize.build_tutorial(filekey) +## +## reformedfsm = reformedTuto.get("Test").state_machine +## +## #Tests +## assert reformedfsm._states.get("INIT").name == fsm._states.get("INIT").name, \ +## 'FSM underlying dictionary differ from original to pickled/reformed one' +## assert reformedfsm._states.get("LOST").name == fsm._states.get("LOST").name, \ +## 'FSM underlying dictionary differ from original to pickled/reformed one' +## assert reformedfsm._states.get("TEXT").name == fsm._states.get("TEXT").name, \ +## 'FSM underlying dictionary differ from original to pickled/reformed one' +## +## +## os.remove(testpath + "serializeTest.tml") +## os.rmdir(testpath) +## os.rmdir("/tmp") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file -- cgit v0.9.1