Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-05-05 05:44:27 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-05-05 05:44:27 (GMT)
commit6fa568daae3291c7a876cd903f04079a12945dcb (patch)
treefaa112d6dee15fa9d6dc340762e7a45878842bee
parentd66690142b643401429bfff84d42acac002e1980 (diff)
parentfb49b482f5bd478bada50cd1ab25a876806eff31 (diff)
Merge branch 'mike'
Conflicts: source/external/source/sugar-toolkit/src/sugar/tutorius/actions.py source/external/source/sugar-toolkit/src/sugar/tutorius/tests/actiontests.py source/external/source/sugar-toolkit/src/sugar/tutorius/tests/run-tests.py
-rw-r--r--src/sugar/activity/activity.py43
-rw-r--r--src/sugar/tutorius/Makefile.am3
-rw-r--r--src/sugar/tutorius/actions.py6
-rw-r--r--src/sugar/tutorius/bundler.py590
-rw-r--r--src/sugar/tutorius/constraints.py26
-rw-r--r--src/sugar/tutorius/core.py14
-rw-r--r--src/sugar/tutorius/properties.py10
-rw-r--r--src/sugar/tutorius/services.py1
-rw-r--r--src/sugar/tutorius/temp.py1
-rw-r--r--src/sugar/tutorius/tests/actiontests.py22
-rw-r--r--src/sugar/tutorius/tests/bundlertests.py65
-rw-r--r--src/sugar/tutorius/tests/constraintstests.py38
-rw-r--r--src/sugar/tutorius/tests/coretests.py41
-rw-r--r--src/sugar/tutorius/tests/propertiestests.py34
-rwxr-xr-xsrc/sugar/tutorius/tests/run-tests.py6
-rw-r--r--src/sugar/tutorius/tests/serializertests.py196
16 files changed, 1021 insertions, 75 deletions
diff --git a/src/sugar/activity/activity.py b/src/sugar/activity/activity.py
index 21e38f6..3e2d3d4 100644
--- a/src/sugar/activity/activity.py
+++ b/src/sugar/activity/activity.py
@@ -77,6 +77,7 @@ from sugar.datastore import datastore
from sugar.session import XSMPClient
from sugar import wm
from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.tutoserialize import TutoSerializer
_ = lambda msg: gettext.dgettext('sugar-toolkit', msg)
@@ -126,11 +127,21 @@ 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
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..7223c60 100644
--- a/src/sugar/tutorius/Makefile.am
+++ b/src/sugar/tutorius/Makefile.am
@@ -13,4 +13,5 @@ sugar_PYTHON = \
editor.py \
linear_creator.py \
constraints.py \
- properties.py
+ properties.py \
+ bundler.py
diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py
index 2d630be..ff7f427 100644
--- a/src/sugar/tutorius/actions.py
+++ b/src/sugar/tutorius/actions.py
@@ -131,13 +131,13 @@ class BubbleMessage(Action):
@param tailpos The position of the tail of the bubble; useful to point to
specific elements of the interface
"""
- def __init__(self, message, pos=[0,0], speaker=None, tailpos=None):
+ def __init__(self, message, pos=None, speaker=None, tailpos=None):
Action.__init__(self)
self.message = TStringProperty(message)
# Create the position as an array of fixed-size 2
- self.position = TArrayProperty(pos, 2, 2)
+ self.position = TArrayProperty(pos or [0,0], 2, 2)
# Do the same for the tail position
- self.tail_pos = TArrayProperty(tailpos, 2, 2)
+ self.tail_pos = TArrayProperty(tailpos or [0,0], 2, 2)
self.overlay = None
self._bubble = None
diff --git a/src/sugar/tutorius/bundler.py b/src/sugar/tutorius/bundler.py
index 585ba88..f9a3911 100644
--- a/src/sugar/tutorius/bundler.py
+++ b/src/sugar/tutorius/bundler.py
@@ -21,33 +21,88 @@ This module contains all the data handling class of Tutorius
"""
import logging
-import linecache
import os
+import uuid
+import xml.dom.minidom
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
+from sugar.tutorius.filters import *
+from sugar.tutorius.actions import *
+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 +115,532 @@ 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 = doc.createElement("State")
+ statesList.appendChild(stateNode)
+ stateNode.setAttribute("Name", state_name)
+ actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc))
+ eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc))
+ return statesList
+
+ def _create_action_node(self, action, doc):
+ """
+ Takes a single action and transforms it into a xml node.
+
+ @param action A single action
+ @param doc The XML document root (used to create nodes only
+ @return A XML Node object with the Action tag name
+ """
+ actionNode = doc.createElement("Action")
+
+ # Write down just the name of the Action class as the Class
+ # property --
+ # Using .__class__ since type() doesn't have the same behavior
+ # with class derivating from object and class that don't
+ actionNode.setAttribute("Class", str(action.__class__))
+
+ if type(action) is DialogMessage:
+ actionNode.setAttribute("Message", action.message.value)
+ actionNode.setAttribute("PositionX", str(action.position.value[0]))
+ actionNode.setAttribute("PositionY", str(action.position.value[1]))
+ elif type(action) is BubbleMessage:
+ actionNode.setAttribute("Message", action.message.value)
+ actionNode.setAttribute("PositionX", str(action.position.value[0]))
+ actionNode.setAttribute("PositionY", str(action.position.value[1]))
+ actionNode.setAttribute("Tail_posX", str(action.tail_pos.value[0]))
+ actionNode.setAttribute("Tail_posY", str(action.tail_pos.value[1]))
+ # TO ADD : elif for each type of action
+ elif type(action) is WidgetIdentifyAction:
+ # Nothing else to save
+ pass
+ elif type(action) is ChainAction:
+ # Recusively write the contained actions - in the correct order
+ subActionsNode = self._create_action_list_node(action._actions, doc)
+ actionNode.appendChild(subActionsNode)
+ elif type(action) is DisableWidgetAction:
+ # Remember the target
+ actionNode.setAttribute("Target", action._target)
+ elif type(action) is TypeTextAction:
+ # Save the text and the widget
+ actionNode.setAttribute("Widget", action._widget)
+ actionNode.setAttribute("Text", action._text)
+ elif type(action) is ClickAction:
+ # Save the widget to click
+ actionNode.setAttribute("Widget", action._widget)
+ elif type(action) is OnceWrapper:
+ # Encapsulate the action in a OnceWrapper
+ subActionNode = self._create_action_node(action._action, doc)
+ actionNode.appendChild(subActionNode)
+
+ return actionNode
+
+ def _create_action_list_node(self, action_list, doc):
+ """
+ Create and return a xml Node from a Action list.
+
+ @param action_list A list of actions
+ @param doc The XML document root (used to create new nodes only)
+ @return A XML Node object with the Actions tag name and a serie of
+ Action children
+ """
+ actionsList = doc.createElement("Actions")
+ for action in action_list:
+ # Create the action node
+ actionNode = self._create_action_node(action, doc)
+ # Append it to the list
+ actionsList.appendChild(actionNode)
+
+ 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 = doc.createElement("EventFilter")
+ eventFiltersList.appendChild(eventFilterNode)
+
+ # Write down just the name of the Action class as the Class
+ # property --
+ # using .__class__ since type() doesn't have the same behavior
+ # with class derivating from object and class that don't
+ eventFilterNode.setAttribute("Class", str(event_f.__class__))
+
+ # Write the name of the next state
+ eventFilterNode.setAttribute("NextState", event_f.next_state)
+
+ if type(event_f) is TimerEvent:
+ eventFilterNode.setAttribute("Timeout_s", str(event_f._timeout))
+
+ elif type(event_f) is GtkWidgetEventFilter:
+ eventFilterNode.setAttribute("EventName", event_f._event_name)
+ eventFilterNode.setAttribute("ObjectId", event_f._object_id)
+
+ elif type(event_f) is GtkWidgetTypeFilter:
+ eventFilterNode.setAttribute("ObjectId", event_f._object_id)
+ if event_f._strokes is not None:
+ eventFilterNode.setAttribute("Strokes", event_f._strokes)
+ if event_f._text is not None:
+ eventFilterNode.setAttribute("Text", event_f._text)
+
+ 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("Name", fsm.name)
+ fsm_element.setAttribute("StartStateName", fsm.start_state_name)
+ statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc))
+
+ fsm_actions_node = self._create_action_list_node(fsm.actions, doc)
+ fsm_actions_node.tagName = "FSMActions"
+ actionsList = fsm_element.appendChild(fsm_actions_node)
+
+ file_object = open(os.path.join(path, xml_filename), "w")
+ file_object.write(doc.toprettyxml())
+ 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_dir) 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 %s"%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
+ """
+ reformed_event_filters_list = []
+ event_filter_element_list = filters_elem.getElementsByTagName("EventFilter")
+ new_event_filter = None
+
+ for event_filter in event_filter_element_list:
+ # Load the name of the next state for this filter
+ next_state = event_filter.getAttribute("NextState")
+
+ if event_filter.getAttribute("Class") == str(TimerEvent):
+ timeout = int(event_filter.getAttribute("Timeout_s"))
+ new_event_filter = TimerEvent(next_state, timeout)
+
+ elif event_filter.getAttribute("Class") == str(GtkWidgetEventFilter):
+ # Get the event name and the object's ID
+ event_name = event_filter.getAttribute("EventName")
+ object_id = event_filter.getAttribute("ObjectId")
+
+ new_event_filter = GtkWidgetEventFilter(next_state, object_id, event_name)
+ elif event_filter.getAttribute("Class") == str(GtkWidgetTypeFilter):
+ # Get the widget to write in and the text
+ object_id = event_filter.getAttribute("ObjectId")
+ if event_filter.hasAttribute("Text"):
+ text = event_filter.getAttribute("Text")
+ new_event_filter = GtkWidgetTypeFilter(next_state, object_id, text=text)
+ elif event_filter.hasAttribute("Strokes"):
+ strokes = event_filter.getAttribute("Strokes")
+ new_event_filter = GtkWidgetTypeFilter(next_state, object_id, strokes=strokes)
+
+ if new_event_filter is not None:
+ reformed_event_filters_list.append(new_event_filter)
+
+ return reformed_event_filters_list
+
+ def _load_xml_action(self, action):
+ """
+ Loads a single action from an Xml Action node.
+
+ @param action The Action XML Node to transform
+ object
+ @return The Action object of the correct type according to the XML
+ description
+ """
+ # TO ADD: an elif for each type of action
+ if action.getAttribute("Class") == str(DialogMessage):
+ message = action.getAttribute("Message")
+ positionX = int(action.getAttribute("PositionX"))
+ positionY = int(action.getAttribute("PositionY"))
+ position = [positionX, positionY]
+ return DialogMessage(message,position)
+ elif action.getAttribute("Class") == str(BubbleMessage):
+ message = action.getAttribute("Message")
+ positionX = int(action.getAttribute("PositionX"))
+ positionY = int(action.getAttribute("PositionY"))
+ position = [positionX, positionY]
+ tail_posX = action.getAttribute("Tail_posX")
+ tail_posY = action.getAttribute("Tail_posY")
+ tail_pos = [tail_posX, tail_posY]
+ return BubbleMessage(message,position,None,tail_pos)
+ elif action.getAttribute("Class") == str(WidgetIdentifyAction):
+ return WidgetIdentifyAction()
+ elif action.getAttribute("Class") == str(ChainAction):
+ # Load the subactions
+ subActionsList = self._load_xml_actions(action.getElementsByTagName("Actions")[0])
+ return ChainAction(subActionsList)
+ elif action.getAttribute("Class") == str(DisableWidgetAction):
+ # Get the target
+ targetName = action.getAttribute("Target")
+ return DisableWidgetAction(targetName)
+ elif action.getAttribute("Class") == str(TypeTextAction):
+ # Get the widget and the text to type
+ widget = action.getAttribute("Widget")
+ text = action.getAttribute("Text")
+
+ return TypeTextAction(widget, text)
+ elif action.getAttribute("Class") == str(ClickAction):
+ # Load the widget to click
+ widget = action.getAttribute("Widget")
+
+ return ClickAction(widget)
+
+ 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
+ """
+ reformed_actions_list = []
+ actions_element_list = actions_elem.getElementsByTagName("Action")
+
+ for action in actions_element_list:
+ new_action = self._load_xml_action(action)
+
+ reformed_actions_list.append(new_action)
+
+ return reformed_actions_list
+
+ 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
+ """
+ reformed_state_list = []
+ # item(0) because there is always only one <States> tag in the xml file
+ # so states_elem should always contain only one element
+ states_element_list = states_elem.item(0).getElementsByTagName("State")
+
+ for state in states_element_list:
+ stateName = state.getAttribute("Name")
+ # Using item 0 in the list because there is always only one
+ # Actions and EventFilterList element per State node.
+ actions_list = self._load_xml_actions(state.getElementsByTagName("Actions")[0])
+ event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0])
+ reformed_state_list.append(State(stateName, actions_list, event_filters_list))
+
+ return reformed_state_list
+
+ 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("Name")
+
+ fsm_start_state_name = None
+ try:
+ fsm_start_state_name = fsm_elem.getAttribute("StartStateName")
+ except:
+ pass
+
+ fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name)
+
+ # Load the states
+ states = self._load_xml_states(fsm_elem.getElementsByTagName("States"))
+ for state in states:
+ fsm.add_state(state)
+
+ # Load the actions on this FSM
+ actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0])
+ for action in actions:
+ fsm.add_action(action)
+
+ # Load the event filters
+ events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
+ 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.
+ 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:
+ serializer = XMLSerializer()
+ path = get_tutorial_path() + "/meta.ini"
+ config.read(path)
+ xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY)
+ serializer.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/constraints.py b/src/sugar/tutorius/constraints.py
index a666ecb..0e09664 100644
--- a/src/sugar/tutorius/constraints.py
+++ b/src/sugar/tutorius/constraints.py
@@ -81,10 +81,10 @@ class LowerLimitConstraint(ValueConstraint):
raise LowerLimitConstraintError()
return
-class SizeConstraintError(Exception):
+class MaxSizeConstraintError(Exception):
pass
-class SizeConstraint(ValueConstraint):
+class MaxSizeConstraint(ValueConstraint):
def validate(self, value):
"""
Evaluate whether a given object is smaller than the given size when
@@ -94,9 +94,27 @@ class SizeConstraint(ValueConstraint):
bigger than the limit.
"""
if self.limit is not None:
- if self.limit > len(value):
+ if self.limit >= len(value):
return
- raise SizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit))
+ raise MaxSizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit))
+ return
+
+class MinSizeConstraintError(Exception):
+ pass
+
+class MinSizeConstraint(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 MinSizeConstraintError("Setter : trying to set value of length %d while limit is %d"%(len(value), self.limit))
return
class ColorConstraintError(Exception):
diff --git a/src/sugar/tutorius/core.py b/src/sugar/tutorius/core.py
index f290f1e..602947f 100644
--- a/src/sugar/tutorius/core.py
+++ b/src/sugar/tutorius/core.py
@@ -295,11 +295,8 @@ class FiniteStateMachine(State):
self._states = state_dict or {}
self.start_state_name = start_state_name
- # If we have a filled input dictionary
- if len(self._states) > 0:
- self.current_state = self._states[self.start_state_name]
- else:
- self.current_state = None
+ # Set the current state to None - we are not executing anything yet
+ self.current_state = None
# Register the actions for the FSM - They will be processed at the
# FSM level, meaning that when the FSM will start, it will first
@@ -375,6 +372,13 @@ class FiniteStateMachine(State):
# state by that name - we must ignore this state change request as
# it will be done elsewhere in the hierarchy (or it's just bogus).
return
+
+ if self.current_state != None:
+ if new_state_name == self.current_state.name:
+ # If we already are in this state, we do not need to change
+ # anything in the current state - By design, a state may not point
+ # to itself
+ return
new_state = self._states[new_state_name]
diff --git a/src/sugar/tutorius/properties.py b/src/sugar/tutorius/properties.py
index 52993b8..a0bfa03 100644
--- a/src/sugar/tutorius/properties.py
+++ b/src/sugar/tutorius/properties.py
@@ -106,7 +106,7 @@ class TStringProperty(TutoriusProperty):
def __init__(self, value, size_limit=None):
TutoriusProperty.__init__(self)
self._type = "string"
- self.size_limit = SizeConstraint(size_limit)
+ self.size_limit = MaxSizeConstraint(size_limit)
self.set(value)
@@ -115,11 +115,11 @@ 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):
+ def __init__(self, value, min_size_limit=None, max_size_limit=None):
TutoriusProperty.__init__(self)
self._type = "array"
- self.size_limit = SizeConstraint(size_limit)
-
+ self.max_size_limit = MaxSizeConstraint(max_size_limit)
+ self.min_size_limit = MinSizeConstraint(min_size_limit)
self.set(value)
class TColorProperty(TutoriusProperty):
@@ -185,7 +185,7 @@ class TEnumProperty(TutoriusProperty):
class TBooleanProperty(TutoriusProperty):
"""
- Represents a True of False value.
+ Represents a True or False value.
"""
def __init__(self, value=False):
TutoriusProperty.__init__(self)
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/actiontests.py b/src/sugar/tutorius/tests/actiontests.py
index bce753e..d244547 100644
--- a/src/sugar/tutorius/tests/actiontests.py
+++ b/src/sugar/tutorius/tests/actiontests.py
@@ -52,6 +52,28 @@ class PropsTest(unittest.TestCase):
for prop_name in act.get_properties():
assert act.properties[prop_name].value == test_props[prop_name], "Wrong initial value for property %s : %s"%(prop_name,str(act.properties[prop_name]))
+
+class DialogMessageTest(unittest.TestCase):
+ def setUp(self):
+ self.dial = DialogMessage("Message text", [200, 300])
+
+ def test_properties(self):
+ assert self.dial.message.value == "Message text", "Wrong start value for the message"
+
+ assert self.dial.position.value == [200, 300], "Wrong start value for the position"
+
+class BubbleMessageTest(unittest.TestCase):
+ def setUp(self):
+ self.bubble = BubbleMessage(message="Message text", pos=[200, 300], tailpos=[-15, -25])
+
+ def test_properties(self):
+ props = self.bubble.get_properties()
+
+ assert "message" in props, 'No message property of BubbleMessage'
+
+ assert "position" in props, 'No position property in BubbleMessage'
+
+ assert "tail_pos" in props, 'No tail position property in BubbleMessage'
class CountAction(Action):
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 <iso.swiffer@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
+"""
+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/constraintstests.py b/src/sugar/tutorius/tests/constraintstests.py
index 407cc24..b7b0a47 100644
--- a/src/sugar/tutorius/tests/constraintstests.py
+++ b/src/sugar/tutorius/tests/constraintstests.py
@@ -77,28 +77,50 @@ class LowerLimitConstraintTest(unittest.TestCase):
except LowerLimitConstraintError:
assert True, "Validation of LowerLimit(10) on 20 should not raise an exception"
-class SizeConstraintTest(unittest.TestCase):
+class MaxSizeConstraintTest(unittest.TestCase):
def test_empty_constraint(self):
- cons = SizeConstraint(None)
+ cons = MaxSizeConstraint(None)
try:
cons.validate(20)
- except SizeConstraintError:
+ except MaxSizeConstraintError:
assert False, "Empty contraint should not raise an exception"
def test_validate(self):
- cons = SizeConstraint(10)
+ cons = MaxSizeConstraint(10)
try:
cons.validate(range(0, 20))
- assert False, "Validation of SizeLimit(10) on list of length 20 should raise an exception"
- except SizeConstraintError:
+ assert False, "Validation of MaxSizeConstraint(10) on list of length 20 should raise an exception"
+ except MaxSizeConstraintError:
pass
try:
cons.validate(range(0,5))
- except SizeConstraintError:
- assert True, "Validation of SizeLimit(10) on list of length 5 should not raise an exception"
+ except MaxSizeConstraintError:
+ assert True, "Validation of MaxSizeConstraint(10) on list of length 5 should not raise an exception"
+class MinSizeConstraintTest(unittest.TestCase):
+ def test_empty_constraint(self):
+ cons = MinSizeConstraint(None)
+ try:
+ cons.validate(20)
+ except MinSizeConstraintError:
+ assert False, "Empty contraint should not raise an exception"
+
+ def test_validate(self):
+ cons = MinSizeConstraint(10)
+
+ try:
+ cons.validate(range(0, 5))
+ assert False, "Validation of MinSizeConstraint(10) on list of length 20 should raise an exception"
+ except MinSizeConstraintError:
+ pass
+
+ try:
+ cons.validate(range(0,20))
+ except MinSizeConstraintError:
+ assert True, "Validation of MinSizeConstraint(10) on list of length 5 should not raise an exception"
+
class ColorConstraintTest(unittest.TestCase):
def test_validate(self):
cons = ColorConstraint()
diff --git a/src/sugar/tutorius/tests/coretests.py b/src/sugar/tutorius/tests/coretests.py
index 5f91a64..c27846d 100644
--- a/src/sugar/tutorius/tests/coretests.py
+++ b/src/sugar/tutorius/tests/coretests.py
@@ -18,7 +18,7 @@
Core Tests
This module contains all the tests that pertain to the usage of the Tutorius
-Core. This means that the the Finite State Machine, States and all the
+Core. This means that the Event Filters, the Finite State Machine and all the
related elements and interfaces are tested here.
Usage of actions and event filters is tested, but not the concrete actions
@@ -29,7 +29,7 @@ and event filters. Those are in their separate test module
import unittest
import logging
-from sugar.tutorius.actions import Action, ClickAction, TypeTextAction
+from sugar.tutorius.actions import Action, OnceWrapper, ClickAction, TypeTextAction
from sugar.tutorius.core import *
from sugar.tutorius.filters import *
@@ -49,6 +49,14 @@ class SimpleTutorial(Tutorial):
def set_state(self, name):
self.current_state_name = name
+class TutorialWithFSM(Tutorial):
+ """
+ Fake tutorial, but associated with a FSM.
+ """
+ def __init__(self, start_name="INIT", fsm=None):
+ Tutorial.__init__(self, start_name, fsm)
+ self.activity = activity.Activity()
+
class TrueWhileActiveAction(Action):
"""
This action's active member is set to True after a do and to False after
@@ -483,7 +491,6 @@ class FSMTest(unittest.TestCase):
except Exception:
assert False, "Removing a non-existing state dit not throw the right kind of exception"
-
# Now try removing the second state
fsm.remove_state("second")
@@ -502,6 +509,33 @@ class FSMTest(unittest.TestCase):
assert "second" not in fsm.get_following_states("third"),\
"The link to second from third still exists after removal"
+ def test_set_same_state(self):
+ fsm = FiniteStateMachine("Set same state")
+
+ st1 = State("INIT")
+ st1.add_action(CountAction())
+
+ fsm.add_state(st1)
+
+ tut = SimpleTutorial()
+
+ fsm.set_tutorial(tut)
+
+ fsm.set_state("INIT")
+
+ assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \
+ "The action was not triggered on 'INIT'"
+
+ fsm.set_state("INIT")
+
+ do_count = fsm.get_state_by_name("INIT").get_action_list()[0].do_count
+ assert fsm.get_state_by_name("INIT").get_action_list()[0].do_count == 1, \
+ "The action was triggered a second time, do_count = %d"%do_count
+
+ undo_count = fsm.get_state_by_name("INIT").get_action_list()[0].undo_count
+ assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0,\
+ "The action has been undone unappropriately, undo_count = %d"%undo_count
+
class FSMExplorationTests(unittest.TestCase):
def setUp(self):
self.buildFSM()
@@ -556,6 +590,7 @@ class FSMExplorationTests(unittest.TestCase):
self.validate_previous_states("Third", ("INIT", "Second"))
self.validate_previous_states("Fourth", ("Second"))
+
if __name__ == "__main__":
unittest.main()
diff --git a/src/sugar/tutorius/tests/propertiestests.py b/src/sugar/tutorius/tests/propertiestests.py
index 4f4caac..45ba264 100644
--- a/src/sugar/tutorius/tests/propertiestests.py
+++ b/src/sugar/tutorius/tests/propertiestests.py
@@ -204,7 +204,7 @@ class TStringPropertyTest(unittest.TestCase):
try:
prop.set("My string is too big!")
assert False, "String should not set to longer than max size"
- except SizeConstraintError:
+ except MaxSizeConstraintError:
pass
except:
assert False, "Wrong exception type thrown"
@@ -218,7 +218,7 @@ class TStringPropertyTest(unittest.TestCase):
try:
prop = TStringProperty("This is normal", 5)
assert False, "Creation of the property should fail."
- except SizeConstraintError:
+ except MaxSizeConstraintError:
pass
except:
assert False, "Wrong exception type on failed constructor"
@@ -236,26 +236,34 @@ class TArrayPropertyTest(unittest.TestCase):
try_wrong_values(prop)
- def test_size_limit(self):
- prop = TArrayProperty([1,2], 4)
+ def test_size_limits(self):
+ prop = TArrayProperty([1,2], None, 4)
try:
prop.set([1,2,4,5,6,7])
- assert False, "Size limit constraint was not properly applied"
- except SizeConstraintError:
+ assert False, "Maximum size limit constraint was not properly applied"
+ except MaxSizeConstraintError:
+ pass
+
+ prop = TArrayProperty([1,2,3,4], 2)
+
+ try:
+ prop.set([1])
+ assert False, "Minimum size limit constraint was not properly applied"
+ except MinSizeConstraintError:
pass
- except:
- assert False, "Wrong type of exception thrown"
-
def test_failing_constructor(self):
try:
- prop = TArrayProperty([100, 0, 20], 2)
+ prop = TArrayProperty([100, 0, 20], None, 2)
assert False, "Creation of the property should fail."
- except SizeConstraintError:
+ except MaxSizeConstraintError:
+ pass
+ try:
+ prop = TArrayProperty([100, 0, 20], 4, None)
+ assert False, "Creation of the property should fail."
+ except MinSizeConstraintError:
pass
- except:
- assert False, "Wrong exception type on failed constructor"
class TColorPropertyTest(unittest.TestCase):
def test_basic_color(self):
diff --git a/src/sugar/tutorius/tests/run-tests.py b/src/sugar/tutorius/tests/run-tests.py
index 87edd57..042b10e 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,9 @@ 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 +69,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..bc29601
--- /dev/null
+++ b/src/sugar/tutorius/tests/serializertests.py
@@ -0,0 +1,196 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@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
+"""
+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 shutil
+
+from sugar.tutorius import gtkutils, overlayer
+from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
+from sugar.tutorius.actions import *
+from sugar.tutorius.filters import *
+from sugar.tutorius.bundler import XMLSerializer, Serializer
+import sugar
+from uuid import *
+import rpdb2
+
+class SerializerInterfaceTest(unittest.TestCase):
+ """
+ For completeness' sake.
+ """
+ def test_save(self):
+ ser = Serializer()
+
+ try:
+ ser.save_fsm(None)
+ assert False, "save_fsm() should throw an unimplemented error"
+ except:
+ pass
+
+ def test_load(self):
+ ser = Serializer()
+
+ try:
+ ser.load_fsm(str(uuid.uuid1()))
+ assert False, "load_fsm() should throw an unimplemented error"
+ except:
+ pass
+
+class XMLSerializerTest(unittest.TestCase):
+ """
+ Tests the transformation of XML to FSM, then back.
+ """
+ def setUp(self):
+ # Make the serializer believe the test is in a activity path
+ self.testpath = "/tmp/testdata/"
+ os.environ["SUGAR_BUNDLE_PATH"] = self.testpath
+ os.environ["SUGAR_PREFIX"] = self.testpath
+## os.mkdir(sugar.tutorius.bundler._get_store_root())
+
+ # Create the sample FSM
+ self.fsm = FiniteStateMachine("testingMachine")
+
+ # Add a few states
+ act1 = BubbleMessage(message="Hi", pos=[300, 450])
+ ev1 = GtkWidgetEventFilter("0.12.31.2.2", "clicked", "Second")
+ act2 = BubbleMessage(message="Second message", pos=[250, 150], tailpos=[1,2])
+
+ st1 = State("INIT")
+ st1.add_action(act1)
+ st1.add_event_filter(ev1)
+
+ st2 = State("Second")
+
+ st2.add_action(act2)
+
+ self.fsm.add_state(st1)
+ self.fsm.add_state(st2)
+
+ self.uuid = uuid1()
+
+ # Flag to set to True if the output can be deleted after execution of
+ # the test
+ self.remove = True
+
+ def tearDown(self):
+ """
+ Removes the created files, if need be.
+ """
+ if self.remove == True:
+ os.remove(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)) + "/fsm.xml")
+ if os.path.isdir(self.testpath):
+ shutil.rmtree(self.testpath)
+
+ def test_save(self):
+ """
+ Writes an FSM to disk, then compares the file to the expected results.
+ "Remove" boolean argument specify if the test data must be removed or not
+ """
+ xml_ser = XMLSerializer()
+ os.makedirs(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)))
+ #rpdb2.start_embedded_debugger('flakyPass')
+ xml_ser.save_fsm(self.fsm, "fsm.xml", os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)))
+
+ def test_save_and_load(self):
+ """
+ Load up the written FSM and compare it with the object representation.
+ """
+ self.test_save()
+ testpath = "/tmp/testdata/"
+ #rpdb2.start_embedded_debugger('flakyPass')
+ xml_ser = XMLSerializer()
+
+ # This interface needs to be redone... It's not clean because there is
+ # a responsibility mixup between the XML reader and the bundler.
+ loaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+ # Compare the two FSMs
+ assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \
+ 'FSM underlying dictionary differ from original to pickled/reformed one'
+ assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \
+ 'FSM underlying dictionary differ from original to pickled/reformed one'
+ assert loaded_fsm._states.get("INIT").get_action_list()[0].message.value == \
+ self.fsm._states.get("INIT").get_action_list()[0].message.value, \
+ 'FSM underlying State underlying Action differ from original to reformed one'
+ assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself"
+
+ def test_all_actions(self):
+ """
+ Inserts all the known action types in a FSM, then attempt to load it.
+ """
+ st = State("INIT")
+
+ act1 = BubbleMessage("Hi!", pos=[10,120], tailpos=[-12,30])
+ act2 = DialogMessage("Hello again.", pos=[120,10])
+ act3 = WidgetIdentifyAction()
+ act4 = DisableWidgetAction("0.0.0.1.0.0.0")
+ act5 = TypeTextAction("0.0.0.1.1.1.0.0", "New text")
+ act6 = ClickAction("0.0.1.0.1.1")
+ act7 = OnceWrapper(act1)
+ act8 = ChainAction([act1, act2, act3, act4])
+ actions = [act1, act2, act3, act4, act5, act6, act7, act8]
+
+ for action in actions:
+ st.add_action(action)
+
+ self.fsm.remove_state("Second")
+ self.fsm.remove_state("INIT")
+ self.fsm.add_state(st)
+
+ xml_ser = XMLSerializer()
+
+ self.test_save()
+
+ reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+ def test_all_filters(self):
+ """
+ Inserts all the known action types in a FSM, then attempt to load it.
+ """
+ st = State("INIT")
+
+ ev1 = TimerEvent("Second", 1000)
+ ev2 = GtkWidgetEventFilter("Second", "0.0.1.1.0.0.1", "clicked")
+ ev3 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", text="Typed stuff")
+ ev4 = GtkWidgetTypeFilter("Second", "0.0.1.1.1.2.3", strokes="acbd")
+ filters = [ev1, ev2, ev3, ev4]
+
+ for filter in filters:
+ st.add_event_filter(filter)
+
+ self.fsm.remove_state("INIT")
+ self.fsm.add_state(st)
+
+ xml_ser = XMLSerializer()
+
+ self.test_save()
+
+ reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
+
+if __name__ == "__main__":
+ unittest.main()