Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/sugar/tutorius/actions.py2
-rw-r--r--src/sugar/tutorius/bundler.py223
-rw-r--r--src/sugar/tutorius/tests/serializertests.py183
3 files changed, 212 insertions, 196 deletions
diff --git a/src/sugar/tutorius/actions.py b/src/sugar/tutorius/actions.py
index d2516f7..ff7f427 100644
--- a/src/sugar/tutorius/actions.py
+++ b/src/sugar/tutorius/actions.py
@@ -131,7 +131,7 @@ 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
diff --git a/src/sugar/tutorius/bundler.py b/src/sugar/tutorius/bundler.py
index 34b3a12..f9a3911 100644
--- a/src/sugar/tutorius/bundler.py
+++ b/src/sugar/tutorius/bundler.py
@@ -27,9 +27,8 @@ import xml.dom.minidom
from sugar.tutorius import gtkutils, overlayer
from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
+from sugar.tutorius.filters import *
from sugar.tutorius.actions import *
-import sugar.tutorius.filters
-#from sugar.tutorius.filters import EventFilter, TimerEvent, GtkWidgetEventFilter, GtkWidgetTypeFilter
from ConfigParser import SafeConfigParser
def _get_store_root():
@@ -132,47 +131,73 @@ class XMLSerializer(Serializer):
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:
- actionNode = doc.createElement("Action")
+ # Create the action node
+ actionNode = self._create_action_node(action, doc)
+ # Append it to the list
actionsList.appendChild(actionNode)
- # 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", 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]))
- # TODO : 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_actions_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)
-
+
return actionsList
def _create_event_filters_node(self, event_filters, doc):
@@ -181,23 +206,26 @@ class XMLSerializer(Serializer):
"""
eventFiltersList = doc.createElement("EventFiltersList")
for event_f in event_filters:
- eventFilterNode = eventFiltersList.appendChild("EventFilter")
+ eventFilterNode = doc.createElement("EventFilter")
+ eventFiltersList.appendChild(eventFilterNode)
- # Using .__class__ since type() doesn't have the same behavior
+ # 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 sugar.tutorius.filters.TimerEvent:
+ if type(event_f) is TimerEvent:
eventFilterNode.setAttribute("Timeout_s", str(event_f._timeout))
- elif type(event_f) is sugar.tutorius.filters.GtkWidgetEventFilter:
+ 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 sugar.tutorius.filters.GtkWidgetTypeFilter:
+ 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)
@@ -295,26 +323,24 @@ class XMLSerializer(Serializer):
@param filters_elem An XML Element representing a list of event filters
"""
reformed_event_filters_list = []
- # item(0) because there is always only one <EventFilterList> tag in the xml file
- # so states_elem should always contain only one element
- event_filter_element_list = filters_elem.item(0).getElementsByTagName("EventFilter")
+ 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(sugar.tutorius.filters.TimerEvent):
+ 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(sugar.tutorius.filters.GtkWidgetEventFilter):
+ 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(sugar.tutorius.filters.GtkWidgetTypeFilter):
+ 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"):
@@ -325,10 +351,57 @@ class XMLSerializer(Serializer):
new_event_filter = GtkWidgetTypeFilter(next_state, object_id, strokes=strokes)
if new_event_filter is not None:
- reformed_event_filter.append(new_event_filter)
+ 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.
@@ -336,48 +409,12 @@ class XMLSerializer(Serializer):
@param actions_elem An XML Element representing a list of Actions
"""
reformed_actions_list = []
- # item(0) because there is always only one <Actions> tag in the xml file
- # so states_elem should always contain only one element
- actions_element_list = actions_elem.item(0).getElementsByTagName("Action")
+ actions_element_list = actions_elem.getElementsByTagName("Action")
for action in actions_element_list:
- # TODO : 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]
- reformed_actions_list.append(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]
- reformed_actions_list.append(BubbleMessage(message,position,None,tail_pos))
- elif action.getAttribute("Class") == str(WidgetIdentifyAction):
- reformed_actions_list.append(WidgetIdentifyAction())
- elif action.getAttribute("Class") == str(ChainAction):
- # Load the subactions
- subActionsList = _load_xml_actions(action.getElementsByTagName("Actions"))
- reformed_actions_list.append(ChainAction(subActionsList))
- elif action.getAttribute("Class") == str(DisableWidgetAction):
- # Get the target
- targetName = action.getAttribute("Target")
- reformed_actions_list.append(DisableWidgetAction(targetName))
- elif action.getAttribute("Class") == str(TypeTextAction):
- # Get the widget and the text to type
- widget = action.getAttribute("Widget")
- text = action.getAttribute("Text")
-
- reformed_actions_list.append(TypeTextAction(widget, text))
- elif action.getAttribute("Class") == str(ClickAction):
- # Load the widget to click
- widget = action.getAttribute("Widget")
-
- reformed_actions_list.append(ClickAction(widget))
+ new_action = self._load_xml_action(action)
+
+ reformed_actions_list.append(new_action)
return reformed_actions_list
@@ -395,8 +432,10 @@ class XMLSerializer(Serializer):
for state in states_element_list:
stateName = state.getAttribute("Name")
- actions_list = self._load_xml_actions(state.getElementsByTagName("Actions"))
- event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList"))
+ # 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
@@ -425,12 +464,12 @@ class XMLSerializer(Serializer):
fsm.add_state(state)
# Load the actions on this FSM
- actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions"))
+ 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"))
+ events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
for event in events:
fsm.add_event_filter(event)
diff --git a/src/sugar/tutorius/tests/serializertests.py b/src/sugar/tutorius/tests/serializertests.py
index 2a743e1..bc29601 100644
--- a/src/sugar/tutorius/tests/serializertests.py
+++ b/src/sugar/tutorius/tests/serializertests.py
@@ -32,10 +32,10 @@ import shutil
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.bundler import *
-
+from sugar.tutorius.actions import *
+from sugar.tutorius.filters import *
+from sugar.tutorius.bundler import XMLSerializer, Serializer
+import sugar
from uuid import *
import rpdb2
@@ -56,7 +56,7 @@ class SerializerInterfaceTest(unittest.TestCase):
ser = Serializer()
try:
- ser.load_fsm(str(uuid.uuid1))
+ ser.load_fsm(str(uuid.uuid1()))
assert False, "load_fsm() should throw an unimplemented error"
except:
pass
@@ -66,6 +66,12 @@ 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")
@@ -76,6 +82,7 @@ class XMLSerializerTest(unittest.TestCase):
st1 = State("INIT")
st1.add_action(act1)
+ st1.add_event_filter(ev1)
st2 = State("Second")
@@ -86,33 +93,34 @@ class XMLSerializerTest(unittest.TestCase):
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, remove=True):
+ 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
- """
- # Make the serializer believe the test is in a activity path
- testpath = "/tmp/testdata/"
- os.environ["SUGAR_BUNDLE_PATH"] = testpath
- os.environ["SUGAR_PREFIX"] = testpath
-## os.mkdir(sugar.tutorius.bundler._get_store_root())
+ """
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)))
-
- #Remove test file and path
- if remove == True:
- os.remove(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)) + "/fsm.xml")
- if os.path.isdir(testpath):
- shutil.rmtree(testpath)
-
+
def test_save_and_load(self):
"""
Load up the written FSM and compare it with the object representation.
"""
- self.test_save(False)
+ self.test_save()
testpath = "/tmp/testdata/"
#rpdb2.start_embedded_debugger('flakyPass')
xml_ser = XMLSerializer()
@@ -130,90 +138,59 @@ class XMLSerializerTest(unittest.TestCase):
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"
-
- os.remove(os.path.join(sugar.tutorius.bundler._get_store_root(), str(self.uuid)) + "/fsm.xml")
- if os.path.isdir(testpath):
- shutil.rmtree(testpath)
-
-
-# Helper classes to help testing
-class SerializerTest(unittest.TestCase):
- """
- This class has to test the Serializer methods as well as the expected
- functionality.
- """
+ def test_all_actions(self):
+ """
+ Inserts all the known action types in a FSM, then attempt to load it.
+ """
+ st = State("INIT")
- # 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")
-
-
+ 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()