Browse Source

Allow wildcard value for fields in semantic options, resulting in automatic generation of test cases for all possible values for those wildcard fields. Cleaner statechart / test loading code. Add 'render' script for new statechart / test XML format.

Joeri Exelmans 5 years ago
parent
commit
0aefc3aa70

+ 4 - 3
src/sccd/runtime/controller.py

@@ -5,6 +5,7 @@ from sccd.runtime.event_queue import EventQueue, EventQueueDeque, Timestamp
 from sccd.runtime.event import *
 from sccd.runtime.object_manager import ObjectManager
 from sccd.runtime.debug import print_debug
+from sccd.runtime.model import *
 
 @dataclasses.dataclass
 class InputEvent:
@@ -23,7 +24,7 @@ class Controller:
         event: Event
         targets: List[Instance]
 
-    def __init__(self, model):
+    def __init__(self, model: AbstractModel):
         self.model = model
         self.object_manager = ObjectManager(model)
         self.queue: EventQueue[EventQueueEntry] = EventQueue()
@@ -37,12 +38,12 @@ class Controller:
             if input.name == "":
                 raise Exception("Input event can't have an empty name.")
         
-            if input.port not in self.model.inports:
+            if input.port not in self.model.namespace.inports:
                 raise Exception("No such port: '" + input.port + "'")
 
 
             e = Event(
-                id=self.model.event_namespace.get_id(input.name),
+                id=self.model.namespace.get_event_id(input.name),
                 name=input.name,
                 port=input.port,
                 parameters=input.parameters)

+ 45 - 15
src/sccd/runtime/model.py

@@ -1,31 +1,61 @@
+from abc import *
 from dataclasses import *
 from typing import *
 from sccd.runtime.statechart_syntax import *
 from sccd.runtime.semantic_options import *
 
-# Mapping from event name to event ID
-class EventNamespace:
+@dataclass
+class ModelNamespace:
   def __init__(self):
-    self.mapping: Dict[str, int] = {}
+    self.events: Dict[str, int] = {}
+    self.inports: List[str] = []
+    self.outports: List[str] = []
+
+  def assign_event_id(self, name: str) -> int:
+    return self.events.setdefault(name, len(self.events))
+
+  def get_event_id(self, name: str) -> int:
+    return self.events[name]
 
-  def assign_id(self, event: str) -> int:
-    return self.mapping.setdefault(event, len(self.mapping))
+  def add_inport(self, port: str):
+    if port not in self.inports:
+      self.inports.append(port)
 
-  def get_id(self, event: str) -> int:
-    return self.mapping[event]
+  def add_outport(self, port: str):
+    if port not in self.outports:
+      self.outports.append(port)
 
 @dataclass
-class Statechart:
+class StateTree:
   root: State
   states: Dict[str, State] # mapping from state "full name" (e.g. "/parallel/ortho1/a") to state
   state_list: List[State] # depth-first order
   transition_list: List[Transition] # source state depth-first order, then document order
+
+@dataclass
+class Statechart:
+  tree: StateTree
   semantics: SemanticConfiguration = SemanticConfiguration()
 
-class Model:
-  def __init__(self):
-    self.event_namespace: EventNamespace = EventNamespace()
-    self.inports: List[str] = []
-    self.outports: List[str] = []
-    self.classes: Dict[str, Statechart] = {}
-    self.default_class: Optional[str] = None
+@dataclass
+class AbstractModel(ABC):
+  namespace: ModelNamespace
+
+  @abstractmethod
+  def get_default_class(self) -> Statechart:
+    pass
+
+@dataclass
+class MultiInstanceModel(AbstractModel):
+  classes: Dict[str, Statechart]
+  default_class: Optional[str]
+
+  def get_default_class(self) -> Statechart:
+    return self.classes[self.default_class]
+
+@dataclass
+class SingleInstanceModel(AbstractModel):
+  statechart: Statechart
+
+  def get_default_class(self) -> Statechart:
+    return self.statechart

+ 2 - 1
src/sccd/runtime/object_manager.py

@@ -16,7 +16,8 @@ class ObjectManager(Instance):
         # we need to maintain this set in order to do broadcasts
         self.instances = [self] # object manager is an instance too!
 
-        self._create(model.default_class)
+        i = StatechartInstance(model.get_default_class(), self)
+        self.instances.append(i)
 
     def _create(self, class_name) -> StatechartInstance:
         # Instantiate the model for each class at most once:

+ 22 - 2
src/sccd/runtime/semantic_options.py

@@ -1,5 +1,7 @@
 from enum import Enum
-from dataclasses import dataclass
+from dataclasses import dataclass, fields
+from typing import *
+import itertools
 
 class BigStepMaximality(Enum):
   TAKE_ONE = 0
@@ -9,7 +11,6 @@ class ComboStepMaximality(Enum):
   COMBO_TAKE_ONE = 0
   COMBO_TAKE_MANY = 1
 
-
 class InternalEventLifeline(Enum):
   QUEUE = 0
   NEXT_SMALL_STEP = 1
@@ -28,6 +29,7 @@ class Concurrency(Enum):
   SINGLE = 0
   MANY = 1
 
+
 @dataclass
 class SemanticConfiguration:
   big_step_maximality: BigStepMaximality = BigStepMaximality.TAKE_MANY
@@ -36,3 +38,21 @@ class SemanticConfiguration:
   input_event_lifeline: InputEventLifeline = InputEventLifeline.FIRST_COMBO_STEP
   priority: Priority = Priority.SOURCE_PARENT
   concurrency: Concurrency = Concurrency.SINGLE
+
+  # Check if any field has been set to None.
+  def has_wildcard(self):
+    for field in fields(self):
+      if getattr(self, field.name) is None:
+        return True
+    return False
+
+  # List of mappings from field name to value for that field.
+  # Each mapping in the list can be used as parameter to the dataclasses.replace function
+  # to create a new semantic configuration with the changes applied.
+  def wildcard_cart_product(self) -> Iterable[Mapping[str, Any]]:
+    wildcard_fields = []
+    for field in fields(self):
+      if getattr(self, field.name) is None:
+        wildcard_fields.append(field)
+    types = (field.type for field in wildcard_fields)
+    return ({wildcard_fields[i].name: option for i,option in enumerate(configuration)} for configuration in itertools.product(*types))

+ 29 - 25
src/sccd/runtime/statechart_instance.py

@@ -7,13 +7,17 @@ from sccd.runtime.event import *
 from sccd.runtime.semantic_options import *
 from sccd.runtime.debug import print_debug
 from sccd.runtime.bitmap import *
+from sccd.runtime.model import *
 from collections import Counter
 
 ELSE_GUARD = "ELSE_GUARD"
 
 class StatechartInstance(Instance):
-    def __init__(self, model, object_manager):
-        self.model = model
+    def __init__(self, statechart: Statechart, object_manager):
+        if statechart.semantics.has_wildcard():
+            raise Exception("Model semantics has unexpanded wildcard for some fields.")
+
+        self.statechart = statechart
         self.object_manager = object_manager
 
         self.data_model = DataModel({
@@ -41,7 +45,7 @@ class StatechartInstance(Instance):
 
     # enter default states, generating a set of output events
     def initialize(self, now: Timestamp) -> Tuple[bool, List[OutputEvent]]:
-        states = self.model.root.getEffectiveTargetStates(self)
+        states = self.statechart.tree.root.getEffectiveTargetStates(self)
         self.configuration.extend(states)
         self.configuration_bitmap = Bitmap.from_list(s.state_id for s in states)
         for state in states:
@@ -59,12 +63,12 @@ class StatechartInstance(Instance):
         self._combo_step.reset()
         self._small_step.reset()
 
-        print_debug(termcolor.colored('attempt big step, input_events='+str(input_events), 'red'))
+        # print_debug(termcolor.colored('attempt big step, input_events='+str(input_events), 'red'))
 
         while self.combo_step():
             print_debug(termcolor.colored('completed combo step', 'yellow'))
             self._big_step.has_stepped = True
-            if self.model.semantics.big_step_maximality == BigStepMaximality.TAKE_ONE:
+            if self.statechart.semantics.big_step_maximality == BigStepMaximality.TAKE_ONE:
                 break # Take One -> only one combo step allowed
 
         # can the next big step still contain transitions, even if there are no input events?
@@ -98,20 +102,20 @@ class StatechartInstance(Instance):
 
         candidates = self._transition_candidates2()
 
-        candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up
-        print_debug(termcolor.colored("small step candidates: "+
-            str(list(map(
-                lambda t: reduce(lambda x,y:x+y,list(map(
-                    lambda s: "to "+s.name,
-                    t.targets))),
-                candidates))), 'blue'))
+        # candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
+        # print_debug(termcolor.colored("small step candidates: "+
+        #     str(list(map(
+        #         lambda t: reduce(lambda x,y:x+y,list(map(
+        #             lambda s: "to "+s.name,
+        #             t.targets))),
+        #         candidates))), 'blue'))
 
         for c in candidates:
-            if self.model.semantics.concurrency == Concurrency.SINGLE:
+            if self.statechart.semantics.concurrency == Concurrency.SINGLE:
                 self._fire_transition(c)
                 self._small_step.has_stepped = True
                 break
-            elif self.model.semantics.concurrency == Concurrency.MANY:
+            elif self.statechart.semantics.concurrency == Concurrency.MANY:
                 raise Exception("Not implemented!")
         return self._small_step.has_stepped
 
@@ -126,7 +130,7 @@ class StatechartInstance(Instance):
         except KeyError:
             # outgoing transitions whose arenas don't overlap with already fired transitions
             self.transition_mem[key] = transitions = [t for s in self.configuration if not changed_bitmap.has(s.state_id) for t in s.transitions]
-            if self.model.semantics.priority == Priority.SOURCE_CHILD:
+            if self.statechart.semantics.priority == Priority.SOURCE_CHILD:
                 # Transitions are already in parent -> child (depth-first) order
                 # Only the first transition of the candidates will be executed.
                 # To get SOURCE-CHILD semantics, we simply reverse the list of candidates:
@@ -149,8 +153,8 @@ class StatechartInstance(Instance):
         try:
             transitions = self.event_mem[key]
         except KeyError:
-            self.event_mem[key] = transitions = [t for t in self.model.transition_list if (not t.trigger or enabled_events_bitmap.has(t.trigger.id)) and not changed_bitmap.has(t.source.state_id)]
-            if self.model.semantics.priority == Priority.SOURCE_CHILD:
+            self.event_mem[key] = transitions = [t for t in self.statechart.tree.transition_list if (not t.trigger or enabled_events_bitmap.has(t.trigger.id)) and not changed_bitmap.has(t.source.state_id)]
+            if self.statechart.semantics.priority == Priority.SOURCE_CHILD:
                 # Transitions are already in parent -> child (depth-first) order
                 # Only the first transition of the candidates will be executed.
                 # To get SOURCE-CHILD semantics, we simply reverse the list of candidates:
@@ -180,11 +184,11 @@ class StatechartInstance(Instance):
     # List of current small step enabled events
     def _enabled_events(self) -> List[Event]:
         events = self._small_step.current_events + self._combo_step.current_events
-        if self.model.semantics.input_event_lifeline == InputEventLifeline.WHOLE or (
+        if self.statechart.semantics.input_event_lifeline == InputEventLifeline.WHOLE or (
             not self._big_step.has_stepped and
-                (self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_COMBO_STEP or (
+                (self.statechart.semantics.input_event_lifeline == InputEventLifeline.FIRST_COMBO_STEP or (
                 not self._combo_step.has_stepped and
-                    self.model.semantics.input_event_lifeline == InputEventLifeline.FIRST_SMALL_STEP))):
+                    self.statechart.semantics.input_event_lifeline == InputEventLifeline.FIRST_SMALL_STEP))):
             events += self._big_step.input_events
         return events
 
@@ -250,7 +254,7 @@ class StatechartInstance(Instance):
         try:
             self.configuration = self.config_mem[self.configuration_bitmap]
         except:
-            self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.model.state_list if self.configuration_bitmap.has(s.state_id)]
+            self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.statechart.tree.state_list if self.configuration_bitmap.has(s.state_id)]
         # t.enabled_event = None
         
     # def getChildren(self, link_name):
@@ -278,16 +282,16 @@ class StatechartInstance(Instance):
                 time_offset=after.delay))
 
     def _raiseInternalEvent(self, event):
-        if self.model.semantics.internal_event_lifeline == InternalEventLifeline.NEXT_SMALL_STEP:
+        if self.statechart.semantics.internal_event_lifeline == InternalEventLifeline.NEXT_SMALL_STEP:
             self._small_step.addNextEvent(event)
-        elif self.model.semantics.internal_event_lifeline == InternalEventLifeline.NEXT_COMBO_STEP:
+        elif self.statechart.semantics.internal_event_lifeline == InternalEventLifeline.NEXT_COMBO_STEP:
             self._combo_step.addNextEvent(event)
-        elif self.model.semantics.internal_event_lifeline == InternalEventLifeline.QUEUE:
+        elif self.statechart.semantics.internal_event_lifeline == InternalEventLifeline.QUEUE:
             self._big_step.addOutputEvent(OutputEvent(event, InstancesTarget([self])))
 
     # Return whether the current configuration includes ALL the states given.
     def inState(self, state_strings: List[str]) -> bool:
-        state_ids_bitmap = Bitmap.from_list((self.model.states[state_string].state_id for state_string in state_strings))
+        state_ids_bitmap = Bitmap.from_list((self.statechart.tree.states[state_string].state_id for state_string in state_strings))
         in_state = self.configuration_bitmap.has_all(state_ids_bitmap)
         if in_state:
             print_debug("in state"+str(state_strings))

+ 1 - 1
src/sccd/runtime/test.py

@@ -10,7 +10,7 @@ TestInput = List[InputEvent]
 TestOutput = List[List[Event]]
 
 class Test(unittest.TestCase):
-  def __init__(self, name: str, model: Model, input: TestInput, output: TestOutput):
+  def __init__(self, name: str, model: AbstractModel, input: TestInput, output: TestOutput):
     super().__init__()
     self.name = name
     self.model = model

+ 16 - 13
src/sccd/runtime/xml_loader.py

@@ -26,12 +26,13 @@ class Test:
   input_events: List[InputEvent]
   expected_events: List[Event]
 
-def load_model(src_file) -> Tuple[Model, Optional[Test]]:
+def load_model(src_file) -> Tuple[MultiInstanceModel, Optional[Test]]:
   tree = ET.parse(src_file)
   schema.assertValid(tree)
   root = tree.getroot()
 
-  model = Model()
+  namespace = ModelNamespace()
+  model = MultiInstanceModel(namespace, classes={}, default_class=None)
 
   classes = root.findall(".//class", root.nsmap)
   for c in classes:
@@ -39,22 +40,22 @@ def load_model(src_file) -> Tuple[Model, Optional[Test]]:
     default = c.get("default", "")
 
     scxml_node = c.find("scxml", root.nsmap)
-    statechart = load_statechart(scxml_node, model.event_namespace)
+    statechart = load_statechart(scxml_node, model.namespace)
 
     model.classes[class_name] = statechart
     if default or len(classes) == 1:
       model.default_class = class_name
 
-  def find_ports(element_path, collection):
+  def find_ports(element_path, add_function):
     elements = root.findall(element_path, root.nsmap)
     for e in elements:
       port = e.get("port")
-      if port != None and port not in collection:
-        collection.append(port)
+      if port != None:
+        add_function(port)
   # Any 'port' attribute of a <transition> element is an input port
-  find_ports(".//transition", model.inports)
+  find_ports(".//transition", namespace.add_inport)
   # Any 'port' attribute of a <raise> element is an output port
-  find_ports(".//raise", model.outports)
+  find_ports(".//raise", namespace.add_outport)
 
   test = None
   test_node = root.find(".//test", root.nsmap)
@@ -82,7 +83,7 @@ def load_model(src_file) -> Tuple[Model, Optional[Test]]:
 
   return (model, test)
 
-def load_statechart(scxml_node, event_namespace: EventNamespace) -> Statechart:
+def load_statechart(scxml_node, namespace: ModelNamespace) -> Statechart:
 
   def load_action(action_node) -> Optional[Action]:
     tag = ET.QName(action_node).localname
@@ -90,7 +91,7 @@ def load_statechart(scxml_node, event_namespace: EventNamespace) -> Statechart:
       event = action_node.get("event")
       port = action_node.get("port")
       if not port:
-        return RaiseInternalEvent(name=event, parameters=[], event_id=event_namespace.assign_id(event))
+        return RaiseInternalEvent(name=event, parameters=[], event_id=namespace.assign_event_id(event))
       else:
         return RaiseOutputEvent(name=event, parameters=[], outport=port, time_offset=0)
     else:
@@ -179,9 +180,9 @@ def load_statechart(scxml_node, event_namespace: EventNamespace) -> Statechart:
     if after is not None:
       event = "_after%d" % next_after_id # transition gets unique event name
       next_after_id += 1
-      trigger = AfterTrigger(event_namespace.assign_id(event), event, Timestamp(after))
+      trigger = AfterTrigger(namespace.assign_event_id(event), event, Timestamp(after))
     elif event is not None:
-      trigger = Trigger(event_namespace.assign_id(event), event, port)
+      trigger = Trigger(namespace.assign_event_id(event), event, port)
     else:
       trigger = None
     transition.setTrigger(trigger)
@@ -216,7 +217,9 @@ def load_statechart(scxml_node, event_namespace: EventNamespace) -> Statechart:
       value = aspect.type[key.upper()]
       setattr(semantics, aspect.name, value)
 
-  return Statechart(root=root, states=states, state_list=state_list, transition_list=transition_list, semantics=semantics)
+  return Statechart(
+    tree=StateTree(root=root, states=states, state_list=state_list, transition_list=transition_list),
+    semantics=semantics)
 
 class ParseError(Exception):
   def __init__(self, msg):

+ 178 - 169
src/sccd/runtime/xml_loader2.py

@@ -30,191 +30,200 @@ def load_expression(parse_node) -> Expression:
     return Array(elements)
   raise ParseError("Can't handle expression type: "+parse_node.data)
 
-# A statechart can only be loaded within the context of a model
-def load_statechart(model: Model, dir, sc_node, name="", default: bool = False):
-
-  def _load(sc_node) -> Statechart:
-
-    def load_action(action_node) -> Optional[Action]:
-      tag = ET.QName(action_node).localname
-      if tag == "raise":
-        name = action_node.get("event")
-        port = action_node.get("port")
-        if not port:
-          event_id = model.event_namespace.assign_id(name)
-          return RaiseInternalEvent(name=name, parameters=[], event_id=event_id)
-        else:
-          if port not in model.outports:
-            model.outports.append(port)
-          return RaiseOutputEvent(name=name, parameters=[], outport=port, time_offset=0)
-      else:
-        raise None
-
-    # parent_node: XML node containing any number of action nodes as direct children
-    def load_actions(parent_node) -> List[Action]:
-      return list(filter(lambda x: x is not None, map(lambda child: load_action(child), parent_node)))
-
-    transition_nodes: List[Tuple[Any, State]] = [] # List of (<transition>, State) tuples
-
-    # Recursively create state hierarchy from XML node
-    # Adding <transition> elements to the 'transitions' list as a side effect
-    def load_state(state_node) -> Optional[State]:
-      state = None
-      name = state_node.get("id", "")
-      tag = ET.QName(state_node).localname
-      if tag == "state":
-          state = State(name)
-      elif tag == "parallel" : 
-          state = ParallelState(name)
-      elif tag == "history":
-        is_deep = state_node.get("type", "shallow") == "deep"
-        if is_deep:
-          state = DeepHistoryState(name)
-        else:
-          state = ShallowHistoryState(name)
+# Load state tree from XML <tree> node.
+# Namespace is required for building event namespace and in/outport discovery.
+def load_state_tree(namespace: ModelNamespace, tree_node) -> StateTree:
+  def load_action(action_node) -> Optional[Action]:
+    tag = ET.QName(action_node).localname
+    if tag == "raise":
+      name = action_node.get("event")
+      port = action_node.get("port")
+      if not port:
+        event_id = namespace.assign_event_id(name)
+        return RaiseInternalEvent(name=name, parameters=[], event_id=event_id)
       else:
-        return None
-
-      initial = state_node.get("initial", "")
-      for xml_child in state_node.getchildren():
-          child = load_state(xml_child) # may throw
-          if child:
-            state.addChild(child)
-            if child.short_name == initial:
-              state.default_state = child
-      if not initial and len(state.children) == 1:
-          state.default_state = state.children[0]
-
-      for xml_t in state_node.findall("transition", state_node.nsmap):
-        transition_nodes.append((xml_t, state))
-
-      # Parse enter/exit actions
-      def _get_enter_exit(tag, setter):
-        node = state_node.find(tag, state_node.nsmap)
-        if node is not None:
-          actions = load_actions(node)
-          setter(actions)
-
-      _get_enter_exit("onentry", state.setEnter)
-      _get_enter_exit("onexit", state.setExit)
-
-      return state
-
-    # Build tree structure
-    tree_node = sc_node.find("tree")
-    root_node = tree_node.find("state")
-    root = load_state(root_node)
-
-    # Add transitions
-    next_after_id = 0
-    for t_node, source in transition_nodes:
-      # Parse and find target state
-      target_string = t_node.get("target", "")
-      parse_tree = parser.parse(target_string, start="state_ref")
-      def find_state(sequence) -> State:
-        if sequence.data == "relative_path":
-          el = source
-        elif sequence.data == "absolute_path":
-          el = root
-        for item in sequence.children:
-          if item.type == "PARENT_NODE":
-            el = el.parent
-          elif item.type == "CURRENT_NODE":
-            continue
-          elif item.type == "IDENTIFIER":
-            el = [x for x in el.children if x.short_name == item.value][0]
-        return el
-      targets = [find_state(seq) for seq in parse_tree.children]
-
-      transition = Transition(source, targets)
-
-      # Trigger
-      event = t_node.get("event")
-      port = t_node.get("port")
-      after = t_node.get("after")
-      if after is not None:
-        event = "_after%d" % next_after_id # transition gets unique event name
-        next_after_id += 1
-        trigger = AfterTrigger(event_namespace.assign_id(event), event, Timestamp(after))
-      elif event is not None:
-        trigger = Trigger(event_namespace.assign_id(event), event, port)
-        if port not in model.inports:
-            model.inports.append(port)
+        namespace.add_outport(port)
+        return RaiseOutputEvent(name=name, parameters=[], outport=port, time_offset=0)
+    else:
+      raise None
+
+  # parent_node: XML node containing any number of action nodes as direct children
+  def load_actions(parent_node) -> List[Action]:
+    return list(filter(lambda x: x is not None, map(lambda child: load_action(child), parent_node)))
+
+  transition_nodes: List[Tuple[Any, State]] = [] # List of (<transition>, State) tuples
+
+  # Recursively create state hierarchy from XML node
+  # Adding <transition> elements to the 'transitions' list as a side effect
+  def load_state(state_node) -> Optional[State]:
+    state = None
+    name = state_node.get("id", "")
+    tag = ET.QName(state_node).localname
+    if tag == "state":
+        state = State(name)
+    elif tag == "parallel" : 
+        state = ParallelState(name)
+    elif tag == "history":
+      is_deep = state_node.get("type", "shallow") == "deep"
+      if is_deep:
+        state = DeepHistoryState(name)
       else:
-        trigger = None
-      transition.setTrigger(trigger)
-      # Actions
-      actions = load_actions(t_node)
-      transition.setActions(actions)
-      # Guard
-      cond = t_node.get("cond")
-      if cond is not None:
-        parse_tree = parser.parse(cond, start="expr")
-        # print(parse_tree)
-        # print(parse_tree.pretty())
-        cond_expr = load_expression(parse_tree)
-        transition.setGuard(cond_expr)
-      source.addTransition(transition)
-
-    # Calculate stuff like list of ancestors, descendants, etc.
-    # Also get depth-first ordered lists of states and transitions (by source)
-    states: Dict[str, State] = {}
-    state_list: List[State] = []
-    transition_list: List[Transition] = []
-    root.init_tree(0, "", states, state_list, transition_list)
-
-    for t in transition_list:
-      t.optimize()
-
-    # Semantics - We use reflection to find the xml attribute names and values
-    semantics_node = sc_node.find("semantics")
-    semantics = SemanticConfiguration()
-    load_semantics(semantics_node, semantics)
-
-    # TODO: process datamodel node
-    datamodel_node = sc_node.find("datamodel")
-
-    statechart = Statechart(root=root, states=states, state_list=state_list, transition_list=transition_list, semantics=semantics)
-
-    model.classes[name] = statechart
-    if default:
-      model.default_class = name
-    return statechart
-
-  # Start of function:
-  src = sc_node.get("src")
-  if src is None:
-    _load(sc_node)
-  else:
-    external_sc_node = ET.parse(os.path.join(dir, src)).getroot()
-    statechart = _load(external_sc_node)
-
-    semantics_node = sc_node.find("override-semantics")
-    load_semantics(semantics_node, statechart.semantics)
-
-def load_semantics(semantics_node, semantics: SemanticConfiguration):
+        state = ShallowHistoryState(name)
+    else:
+      return None
+
+    initial = state_node.get("initial", "")
+    for xml_child in state_node.getchildren():
+        child = load_state(xml_child) # may throw
+        if child:
+          state.addChild(child)
+          if child.short_name == initial:
+            state.default_state = child
+    if not initial and len(state.children) == 1:
+        state.default_state = state.children[0]
+
+    for xml_t in state_node.findall("transition", state_node.nsmap):
+      transition_nodes.append((xml_t, state))
+
+    # Parse enter/exit actions
+    def _get_enter_exit(tag, setter):
+      node = state_node.find(tag, state_node.nsmap)
+      if node is not None:
+        actions = load_actions(node)
+        setter(actions)
+
+    _get_enter_exit("onentry", state.setEnter)
+    _get_enter_exit("onexit", state.setExit)
+
+    return state
+
+  # Build tree structure
+  root_node = tree_node.find("state")
+  root = load_state(root_node)
+
+  # Add transitions
+  next_after_id = 0
+  for t_node, source in transition_nodes:
+    # Parse and find target state
+    target_string = t_node.get("target", "")
+    parse_tree = parser.parse(target_string, start="state_ref")
+    def find_state(sequence) -> State:
+      if sequence.data == "relative_path":
+        el = source
+      elif sequence.data == "absolute_path":
+        el = root
+      for item in sequence.children:
+        if item.type == "PARENT_NODE":
+          el = el.parent
+        elif item.type == "CURRENT_NODE":
+          continue
+        elif item.type == "IDENTIFIER":
+          el = [x for x in el.children if x.short_name == item.value][0]
+      return el
+    targets = [find_state(seq) for seq in parse_tree.children]
+
+    transition = Transition(source, targets)
+
+    # Trigger
+    name = t_node.get("event")
+    port = t_node.get("port")
+    after = t_node.get("after")
+    if after is not None:
+      name = "_after%d" % next_after_id # transition gets unique event name
+      next_after_id += 1
+      trigger = AfterTrigger(namespace.assign_event_id(name), name, Timestamp(after))
+    elif name is not None:
+      trigger = Trigger(namespace.assign_event_id(name), name, port)
+      namespace.add_inport(port)
+    else:
+      trigger = None
+    transition.setTrigger(trigger)
+    # Actions
+    actions = load_actions(t_node)
+    transition.setActions(actions)
+    # Guard
+    cond = t_node.get("cond")
+    if cond is not None:
+      parse_tree = parser.parse(cond, start="expr")
+      # print(parse_tree)
+      # print(parse_tree.pretty())
+      cond_expr = load_expression(parse_tree)
+      transition.setGuard(cond_expr)
+    source.addTransition(transition)
+
+  # Calculate stuff like list of ancestors, descendants, etc.
+  # Also get depth-first ordered lists of states and transitions (by source)
+  states: Dict[str, State] = {}
+  state_list: List[State] = []
+  transition_list: List[Transition] = []
+  root.init_tree(0, "", states, state_list, transition_list)
+
+  for t in transition_list:
+    t.optimize()
+
+  return StateTree(root=root, states=states, state_list=state_list, transition_list=transition_list)
+
+# Namespace is required for building event namespace and in/outport discovery.
+def load_statechart(namespace: ModelNamespace, sc_node) -> Statechart:
+  tree_node = sc_node.find("tree")
+  state_tree = load_state_tree(namespace, tree_node)
+
+  semantics_node = sc_node.find("semantics")
+  semantics = SemanticConfiguration() # start with default semantics
+  load_semantics(semantics, semantics_node)
+
+  datamodel_node = sc_node.find("datamodel")
+  # TODO: process datamodel node
+
+  return Statechart(tree=state_tree, semantics=semantics)
+
+def load_semantics(semantics: SemanticConfiguration, semantics_node):
   if semantics_node is not None:
     # Use reflection to find the possible XML attributes and their values
     for aspect in dataclasses.fields(SemanticConfiguration):
       key = semantics_node.get(aspect.name)
       if key is not None:
-        value = aspect.type[key.upper()]
-        setattr(semantics, aspect.name, value)
+        if key == "*":
+          setattr(semantics, aspect.name, None)
+        else:
+          value = aspect.type[key.upper()]
+          setattr(semantics, aspect.name, value)
+
+# Returned list contains more than one test if the semantic configuration contains wildcard values.
+def load_test(src_file) -> List[Test]:
+  namespace = ModelNamespace()
 
-def load_test(src_file) -> Test:
-  # We'll create a model with one statechart
-  model = Model()
   test_node = ET.parse(src_file).getroot()
   sc_node = test_node.find("statechart")
-  load_statechart(model, os.path.dirname(src_file), sc_node, name="??", default=True)
+  src = sc_node.get("src")
+  if src is None:
+    statechart = load_statechart(namespace, sc_node)
+  else:
+    external_node = ET.parse(os.path.join(os.path.dirname(src_file), src)).getroot()
+    statechart = load_statechart(namespace, external_node)
+    semantics_node = sc_node.find("override_semantics")
+    load_semantics(statechart.semantics, semantics_node)
 
   input_node = test_node.find("input")
   output_node = test_node.find("output")
-
   input = load_input(input_node)
   output = load_output(output_node)
 
-  return Test(src_file, model, input, output)
+  def variant_description(i, variant) -> str:
+    if not variant:
+      return ""
+    return " (variant %d: %s)" % (i, ",".join(str(val) for val in variant.values()))
+
+  return [
+    Test(
+      src_file + variant_description(i, variant),
+      SingleInstanceModel(
+        namespace,
+        Statechart(tree=statechart.tree, semantics=dataclasses.replace(statechart.semantics, **variant))),
+      input,
+      output)
+    for i, variant in enumerate(statechart.semantics.wildcard_cart_product())
+  ]
 
 def load_input(input_node) -> TestInput:
   input = []

+ 2 - 2
test/new_test_files/take_many.test.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="sequence.statechart.xml">
-    <override-semantics
+  <statechart src="../models/flat.statechart.xml">
+    <override_semantics
       big_step_maximality="take_many"/>
   </statechart>
   <input/>

+ 2 - 2
test/new_test_files/take_one.test.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" ?>
 <test>
-  <statechart src="sequence.statechart.xml">
-    <override-semantics
+  <statechart src="../models/flat.statechart.xml">
+    <override_semantics
       big_step_maximality="take_one"/>
   </statechart>
   <input/>

+ 15 - 0
test/new_test_files/big_step_maximality/ortho_takemany.test.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="../models/ortho.statechart.xml">
+    <override_semantics
+      big_step_maximality="take_many"/>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+      <event name="in_e" port="out"/>
+      <event name="in_c" port="out"/>
+      <event name="in_f" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 17 - 0
test/new_test_files/big_step_maximality/ortho_takeone.test.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="../models/ortho.statechart.xml">
+    <override_semantics
+      big_step_maximality="take_one"/>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+      <event name="in_e" port="out"/>
+    </big_step>
+    <big_step>
+      <event name="in_c" port="out"/>
+      <event name="in_f" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 16 - 0
test/new_test_files/event_lifeline/flat_nextbs.test.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="../models/flat_intevent.statechart.xml">
+    <override_semantics
+      big_step_maximality="*"
+      internal_event_lifeline="queue"/>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+    </big_step>
+    <big_step>
+      <event name="in_c" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 14 - 0
test/new_test_files/event_lifeline/flat_takemany_nextss.test.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="../models/flat_intevent.statechart.xml">
+    <override_semantics
+      big_step_maximality="take_many"
+      internal_event_lifeline="next_small_step"/>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+      <event name="in_c" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 13 - 0
test/new_test_files/event_lifeline/flat_takeone_nextss.test.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart src="../models/flat_intevent.statechart.xml">
+    <override_semantics
+      big_step_maximality="take_one"
+      internal_event_lifeline="next_small_step"/>
+  </statechart>
+  <output>
+    <big_step>
+      <event name="in_b" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 64 - 0
test/new_test_files/models/flat.statechart.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: state transitions Pages: 1 -->
+<svg width="114pt" height="231pt"
+ viewBox="0.00 0.00 114.00 231.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 227)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-227 110,-227 110,4 -4,4"/>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="53" cy="-217.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _a -->
+<g id="node2" class="node">
+<title>_a</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="81,-184 25,-184 25,-148 81,-148 81,-184"/>
+<text text-anchor="start" x="49.6646" y="-162.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">a</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M37.3333,-149C37.3333,-149 68.6667,-149 68.6667,-149 74.3333,-149 80,-154.6667 80,-160.3333 80,-160.3333 80,-171.6667 80,-171.6667 80,-177.3333 74.3333,-183 68.6667,-183 68.6667,-183 37.3333,-183 37.3333,-183 31.6667,-183 26,-177.3333 26,-171.6667 26,-171.6667 26,-160.3333 26,-160.3333 26,-154.6667 31.6667,-149 37.3333,-149"/>
+</g>
+<!-- __initial&#45;&gt;_a -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_a</title>
+<path fill="none" stroke="#000000" d="M53,-211.9886C53,-207.6293 53,-201.1793 53,-194.4801"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-194.0122 53,-184.0122 49.5001,-194.0122 56.5001,-194.0122"/>
+<text text-anchor="middle" x="54.3895" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _b -->
+<g id="node3" class="node">
+<title>_b</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-120 0,-120 0,-74 106,-74 106,-120"/>
+<text text-anchor="start" x="49.6646" y="-103.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">b</text>
+<text text-anchor="start" x="5.5022" y="-83.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_b</text>
+<polygon fill="#000000" stroke="#000000" points="0,-97 0,-97 106,-97 106,-97 0,-97"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-75C13,-75 93,-75 93,-75 99,-75 105,-81 105,-87 105,-87 105,-107 105,-107 105,-113 99,-119 93,-119 93,-119 13,-119 13,-119 7,-119 1,-113 1,-107 1,-107 1,-87 1,-87 1,-81 7,-75 13,-75"/>
+</g>
+<!-- _a&#45;&gt;_b -->
+<g id="edge2" class="edge">
+<title>_a&#45;&gt;_b</title>
+<path fill="none" stroke="#000000" d="M53,-147.8711C53,-142.4482 53,-136.3229 53,-130.2494"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-130.21 53,-120.21 49.5001,-130.21 56.5001,-130.21"/>
+<text text-anchor="middle" x="54.3895" y="-131" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _c -->
+<g id="node4" class="node">
+<title>_c</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-46 0,-46 0,0 106,0 106,-46"/>
+<text text-anchor="start" x="50" y="-29.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">c</text>
+<text text-anchor="start" x="5.8376" y="-9.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_c</text>
+<polygon fill="#000000" stroke="#000000" points="0,-23 0,-23 106,-23 106,-23 0,-23"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-1C13,-1 93,-1 93,-1 99,-1 105,-7 105,-13 105,-13 105,-33 105,-33 105,-39 99,-45 93,-45 93,-45 13,-45 13,-45 7,-45 1,-39 1,-33 1,-33 1,-13 1,-13 1,-7 7,-1 13,-1"/>
+</g>
+<!-- _b&#45;&gt;_c -->
+<g id="edge3" class="edge">
+<title>_b&#45;&gt;_c</title>
+<path fill="none" stroke="#000000" d="M53,-73.9916C53,-68.476 53,-62.474 53,-56.5881"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-56.249 53,-46.2491 49.5001,-56.2491 56.5001,-56.249"/>
+<text text-anchor="middle" x="54.3895" y="-57" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+</g>
+</svg>

test/new_test_files/sequence.statechart.xml → test/new_test_files/models/flat.statechart.xml


+ 64 - 0
test/new_test_files/models/flat_intevent.statechart.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: state transitions Pages: 1 -->
+<svg width="114pt" height="231pt"
+ viewBox="0.00 0.00 114.00 231.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 227)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-227 110,-227 110,4 -4,4"/>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="53" cy="-217.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _a -->
+<g id="node2" class="node">
+<title>_a</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="81,-184 25,-184 25,-148 81,-148 81,-184"/>
+<text text-anchor="start" x="49.6646" y="-162.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">a</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M37.3333,-149C37.3333,-149 68.6667,-149 68.6667,-149 74.3333,-149 80,-154.6667 80,-160.3333 80,-160.3333 80,-171.6667 80,-171.6667 80,-177.3333 74.3333,-183 68.6667,-183 68.6667,-183 37.3333,-183 37.3333,-183 31.6667,-183 26,-177.3333 26,-171.6667 26,-171.6667 26,-160.3333 26,-160.3333 26,-154.6667 31.6667,-149 37.3333,-149"/>
+</g>
+<!-- __initial&#45;&gt;_a -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_a</title>
+<path fill="none" stroke="#000000" d="M53,-211.9886C53,-207.6293 53,-201.1793 53,-194.4801"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-194.0122 53,-184.0122 49.5001,-194.0122 56.5001,-194.0122"/>
+<text text-anchor="middle" x="54.3895" y="-195" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _b -->
+<g id="node3" class="node">
+<title>_b</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-120 0,-120 0,-74 106,-74 106,-120"/>
+<text text-anchor="start" x="49.6646" y="-103.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">b</text>
+<text text-anchor="start" x="5.5022" y="-83.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_b</text>
+<polygon fill="#000000" stroke="#000000" points="0,-97 0,-97 106,-97 106,-97 0,-97"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-75C13,-75 93,-75 93,-75 99,-75 105,-81 105,-87 105,-87 105,-107 105,-107 105,-113 99,-119 93,-119 93,-119 13,-119 13,-119 7,-119 1,-113 1,-107 1,-107 1,-87 1,-87 1,-81 7,-75 13,-75"/>
+</g>
+<!-- _a&#45;&gt;_b -->
+<g id="edge2" class="edge">
+<title>_a&#45;&gt;_b</title>
+<path fill="none" stroke="#000000" d="M53,-147.8711C53,-142.4482 53,-136.3229 53,-130.2494"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-130.21 53,-120.21 49.5001,-130.21 56.5001,-130.21"/>
+<text text-anchor="start" x="53" y="-131" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">^f &#160;&#160;</text>
+</g>
+<!-- _c -->
+<g id="node4" class="node">
+<title>_c</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="106,-46 0,-46 0,0 106,0 106,-46"/>
+<text text-anchor="start" x="50" y="-29.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">c</text>
+<text text-anchor="start" x="5.8376" y="-9.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_c</text>
+<polygon fill="#000000" stroke="#000000" points="0,-23 0,-23 106,-23 106,-23 0,-23"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M13,-1C13,-1 93,-1 93,-1 99,-1 105,-7 105,-13 105,-13 105,-33 105,-33 105,-39 99,-45 93,-45 93,-45 13,-45 13,-45 7,-45 1,-39 1,-33 1,-33 1,-13 1,-13 1,-7 7,-1 13,-1"/>
+</g>
+<!-- _b&#45;&gt;_c -->
+<g id="edge3" class="edge">
+<title>_b&#45;&gt;_c</title>
+<path fill="none" stroke="#000000" d="M53,-73.9916C53,-68.476 53,-62.474 53,-56.5881"/>
+<polygon fill="#000000" stroke="#000000" points="56.5001,-56.249 53,-46.2491 49.5001,-56.2491 56.5001,-56.249"/>
+<text text-anchor="start" x="53" y="-57" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000">f &#160;&#160;</text>
+</g>
+</g>
+</svg>

+ 25 - 0
test/new_test_files/models/flat_intevent.statechart.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" ?>
+<statechart>
+  <semantics/>
+  <datamodel/>
+  <tree>
+    <state initial="a">
+      <state id="a">
+        <transition target="/b">
+          <raise event="f"/>
+        </transition>
+      </state>
+      <state id="b">
+        <onentry>
+          <raise event="in_b" port="out"/>
+        </onentry>
+        <transition event="f" target="/c"/>
+      </state>
+      <state id="c">
+        <onentry>
+          <raise event="in_c" port="out"/>
+        </onentry>
+      </state>
+    </state>
+  </tree>
+</statechart>

+ 145 - 0
test/new_test_files/models/ortho.statechart.svg

@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ -->
+<!-- Title: state transitions Pages: 1 -->
+<svg width="300pt" height="532pt"
+ viewBox="0.00 0.00 300.00 532.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 528)">
+<title>state transitions</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-528 296,-528 296,4 -4,4"/>
+<g id="clust1" class="cluster">
+<title>cluster__p</title>
+<path fill="none" stroke="#000000" stroke-width="2" d="M20,-8C20,-8 272,-8 272,-8 278,-8 284,-14 284,-20 284,-20 284,-473 284,-473 284,-479 278,-485 272,-485 272,-485 20,-485 20,-485 14,-485 8,-479 8,-473 8,-473 8,-20 8,-20 8,-14 14,-8 20,-8"/>
+<text text-anchor="start" x="142.6646" y="-466.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">p</text>
+</g>
+<g id="clust2" class="cluster">
+<title>cluster__p_o0</title>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="154,-16 154,-447 276,-447 276,-16 154,-16"/>
+<text text-anchor="start" x="208.8292" y="-428.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">o0</text>
+</g>
+<g id="clust3" class="cluster">
+<title>cluster__p_o1</title>
+<polygon fill="none" stroke="#000000" stroke-dasharray="5,2" points="24,-16 24,-447 146,-447 146,-16 24,-16"/>
+<text text-anchor="start" x="78.8292" y="-428.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">o1</text>
+</g>
+<!-- __initial -->
+<g id="node1" class="node">
+<title>__initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="16" cy="-518.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p -->
+<!-- __initial&#45;&gt;_p -->
+<g id="edge1" class="edge">
+<title>__initial&#45;&gt;_p</title>
+<path fill="none" stroke="#000000" d="M16,-512.9533C16,-508.7779 16,-502.5043 16,-495.0332"/>
+<polygon fill="#000000" stroke="#000000" points="19.5001,-494.9971 16,-484.9971 12.5001,-494.9972 19.5001,-494.9971"/>
+<text text-anchor="middle" x="17.3895" y="-496" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o0 -->
+<!-- _p_o0_initial -->
+<g id="node4" class="node">
+<title>_p_o0_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="215" cy="-403.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p_o0_a -->
+<g id="node5" class="node">
+<title>_p_o0_a</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="243,-316 187,-316 187,-280 243,-280 243,-316"/>
+<text text-anchor="start" x="211.6646" y="-294.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">a</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M199.3333,-281C199.3333,-281 230.6667,-281 230.6667,-281 236.3333,-281 242,-286.6667 242,-292.3333 242,-292.3333 242,-303.6667 242,-303.6667 242,-309.3333 236.3333,-315 230.6667,-315 230.6667,-315 199.3333,-315 199.3333,-315 193.6667,-315 188,-309.3333 188,-303.6667 188,-303.6667 188,-292.3333 188,-292.3333 188,-286.6667 193.6667,-281 199.3333,-281"/>
+</g>
+<!-- _p_o0_initial&#45;&gt;_p_o0_a -->
+<g id="edge2" class="edge">
+<title>_p_o0_initial&#45;&gt;_p_o0_a</title>
+<path fill="none" stroke="#000000" d="M215,-397.8288C215,-393.1736 215,-386.4097 215,-380.5 215,-380.5 215,-380.5 215,-333.5 215,-331.1079 215,-328.6252 215,-326.1342"/>
+<polygon fill="#000000" stroke="#000000" points="218.5001,-326.0597 215,-316.0598 211.5001,-326.0598 218.5001,-326.0597"/>
+<text text-anchor="middle" x="216.3895" y="-354" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o0_b -->
+<g id="node6" class="node">
+<title>_p_o0_b</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="268,-198 162,-198 162,-152 268,-152 268,-198"/>
+<text text-anchor="start" x="211.6646" y="-181.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">b</text>
+<text text-anchor="start" x="167.5022" y="-161.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_b</text>
+<polygon fill="#000000" stroke="#000000" points="162,-175 162,-175 268,-175 268,-175 162,-175"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M175,-153C175,-153 255,-153 255,-153 261,-153 267,-159 267,-165 267,-165 267,-185 267,-185 267,-191 261,-197 255,-197 255,-197 175,-197 175,-197 169,-197 163,-191 163,-185 163,-185 163,-165 163,-165 163,-159 169,-153 175,-153"/>
+</g>
+<!-- _p_o0_a&#45;&gt;_p_o0_b -->
+<g id="edge3" class="edge">
+<title>_p_o0_a&#45;&gt;_p_o0_b</title>
+<path fill="none" stroke="#000000" d="M215,-279.9402C215,-274.3497 215,-268.1701 215,-262.5 215,-262.5 215,-262.5 215,-215.5 215,-213.127 215,-210.6757 215,-208.2081"/>
+<polygon fill="#000000" stroke="#000000" points="218.5001,-208.1306 215,-198.1306 211.5001,-208.1306 218.5001,-208.1306"/>
+<text text-anchor="middle" x="216.3895" y="-236" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o0_c -->
+<g id="node7" class="node">
+<title>_p_o0_c</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="268,-70 162,-70 162,-24 268,-24 268,-70"/>
+<text text-anchor="start" x="212" y="-53.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">c</text>
+<text text-anchor="start" x="167.8376" y="-33.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_c</text>
+<polygon fill="#000000" stroke="#000000" points="162,-47 162,-47 268,-47 268,-47 162,-47"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M175,-25C175,-25 255,-25 255,-25 261,-25 267,-31 267,-37 267,-37 267,-57 267,-57 267,-63 261,-69 255,-69 255,-69 175,-69 175,-69 169,-69 163,-63 163,-57 163,-57 163,-37 163,-37 163,-31 169,-25 175,-25"/>
+</g>
+<!-- _p_o0_b&#45;&gt;_p_o0_c -->
+<g id="edge4" class="edge">
+<title>_p_o0_b&#45;&gt;_p_o0_c</title>
+<path fill="none" stroke="#000000" d="M215,-151.8694C215,-146.1895 215,-140.125 215,-134.5 215,-134.5 215,-134.5 215,-87.5 215,-85.127 215,-82.6757 215,-80.2081"/>
+<polygon fill="#000000" stroke="#000000" points="218.5001,-80.1306 215,-70.1306 211.5001,-80.1306 218.5001,-80.1306"/>
+<text text-anchor="middle" x="216.3895" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o1 -->
+<!-- _p_o1_initial -->
+<g id="node9" class="node">
+<title>_p_o1_initial</title>
+<ellipse fill="#000000" stroke="#000000" stroke-width="2" cx="85" cy="-403.5" rx="5.5" ry="5.5"/>
+</g>
+<!-- _p_o1_d -->
+<g id="node10" class="node">
+<title>_p_o1_d</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="113,-316 57,-316 57,-280 113,-280 113,-316"/>
+<text text-anchor="start" x="81.6646" y="-294.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">d</text>
+<path fill="none" stroke="#000000" stroke-width="2" d="M69.3333,-281C69.3333,-281 100.6667,-281 100.6667,-281 106.3333,-281 112,-286.6667 112,-292.3333 112,-292.3333 112,-303.6667 112,-303.6667 112,-309.3333 106.3333,-315 100.6667,-315 100.6667,-315 69.3333,-315 69.3333,-315 63.6667,-315 58,-309.3333 58,-303.6667 58,-303.6667 58,-292.3333 58,-292.3333 58,-286.6667 63.6667,-281 69.3333,-281"/>
+</g>
+<!-- _p_o1_initial&#45;&gt;_p_o1_d -->
+<g id="edge5" class="edge">
+<title>_p_o1_initial&#45;&gt;_p_o1_d</title>
+<path fill="none" stroke="#000000" d="M85,-397.8288C85,-393.1736 85,-386.4097 85,-380.5 85,-380.5 85,-380.5 85,-333.5 85,-331.1079 85,-328.6252 85,-326.1342"/>
+<polygon fill="#000000" stroke="#000000" points="88.5001,-326.0597 85,-316.0598 81.5001,-326.0598 88.5001,-326.0597"/>
+<text text-anchor="middle" x="86.3895" y="-354" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o1_e -->
+<g id="node11" class="node">
+<title>_p_o1_e</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="138,-198 32,-198 32,-152 138,-152 138,-198"/>
+<text text-anchor="start" x="81.6646" y="-181.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">e</text>
+<text text-anchor="start" x="37.5022" y="-161.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_e</text>
+<polygon fill="#000000" stroke="#000000" points="32,-175 32,-175 138,-175 138,-175 32,-175"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M45,-153C45,-153 125,-153 125,-153 131,-153 137,-159 137,-165 137,-165 137,-185 137,-185 137,-191 131,-197 125,-197 125,-197 45,-197 45,-197 39,-197 33,-191 33,-185 33,-185 33,-165 33,-165 33,-159 39,-153 45,-153"/>
+</g>
+<!-- _p_o1_d&#45;&gt;_p_o1_e -->
+<g id="edge6" class="edge">
+<title>_p_o1_d&#45;&gt;_p_o1_e</title>
+<path fill="none" stroke="#000000" d="M85,-279.9402C85,-274.3497 85,-268.1701 85,-262.5 85,-262.5 85,-262.5 85,-215.5 85,-213.127 85,-210.6757 85,-208.2081"/>
+<polygon fill="#000000" stroke="#000000" points="88.5001,-208.1306 85,-198.1306 81.5001,-208.1306 88.5001,-208.1306"/>
+<text text-anchor="middle" x="86.3895" y="-236" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+<!-- _p_o1_f -->
+<g id="node12" class="node">
+<title>_p_o1_f</title>
+<polygon fill="transparent" stroke="transparent" stroke-width="2" points="136.5,-70 33.5,-70 33.5,-24 136.5,-24 136.5,-70"/>
+<text text-anchor="start" x="83.8326" y="-53.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">f</text>
+<text text-anchor="start" x="39.6702" y="-33.2" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#000000">onentry/ ^out.in_f</text>
+<polygon fill="#000000" stroke="#000000" points="34,-47 34,-47 137,-47 137,-47 34,-47"/>
+<path fill="none" stroke="#000000" stroke-width="2" d="M46.5,-25C46.5,-25 123.5,-25 123.5,-25 129.5,-25 135.5,-31 135.5,-37 135.5,-37 135.5,-57 135.5,-57 135.5,-63 129.5,-69 123.5,-69 123.5,-69 46.5,-69 46.5,-69 40.5,-69 34.5,-63 34.5,-57 34.5,-57 34.5,-37 34.5,-37 34.5,-31 40.5,-25 46.5,-25"/>
+</g>
+<!-- _p_o1_e&#45;&gt;_p_o1_f -->
+<g id="edge7" class="edge">
+<title>_p_o1_e&#45;&gt;_p_o1_f</title>
+<path fill="none" stroke="#000000" d="M85,-151.8694C85,-146.1895 85,-140.125 85,-134.5 85,-134.5 85,-134.5 85,-87.5 85,-85.127 85,-82.6757 85,-80.2081"/>
+<polygon fill="#000000" stroke="#000000" points="88.5001,-80.1306 85,-70.1306 81.5001,-80.1306 88.5001,-80.1306"/>
+<text text-anchor="middle" x="86.3895" y="-108" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#000000"> </text>
+</g>
+</g>
+</svg>

+ 43 - 0
test/new_test_files/models/ortho.statechart.xml

@@ -0,0 +1,43 @@
+<?xml version="1.0" ?>
+<statechart>
+  <semantics/>
+  <datamodel/>
+  <tree>
+    <state>
+      <parallel id="p">
+        <state id="o0" initial="a">
+          <state id="a">
+            <transition target="../b"/>
+          </state>
+          <state id="b">
+            <onentry>
+              <raise event="in_b" port="out"/>
+            </onentry>
+            <transition target="../c"/>
+          </state>
+          <state id="c">
+            <onentry>
+              <raise event="in_c" port="out"/>
+            </onentry>
+          </state>
+        </state>
+        <state id="o1" initial="d">
+          <state id="d">
+            <transition target="../e"/>
+          </state>
+          <state id="e">
+            <onentry>
+              <raise event="in_e" port="out"/>
+            </onentry>
+            <transition target="../f"/>
+          </state>
+          <state id="f">
+            <onentry>
+              <raise event="in_f" port="out"/>
+            </onentry>
+          </state>
+        </state>
+      </parallel>
+    </state>
+  </tree>
+</statechart>

+ 146 - 0
test/render2.py

@@ -0,0 +1,146 @@
+import argparse
+import sys
+import subprocess
+import multiprocessing
+from lib.os_tools import *
+from sccd.compiler.utils import FormattedWriter
+# from sccd.runtime.statechart_syntax import *
+from sccd.runtime.xml_loader2 import *
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description="Render statecharts as SVG images.")
+    parser.add_argument('path', metavar='PATH', type=str, nargs='*', help="Models to render. Can be a XML file or a directory. If a directory, it will be recursively scanned for XML files.")
+    parser.add_argument('--build-dir', metavar='DIR', type=str, default='build', help="As a first step, input XML files first must be compiled to python files. Directory to store these files. Defaults to 'build'")
+    parser.add_argument('--output-dir', metavar='DIR', type=str, default='', help="Directory for SVG rendered output. Defaults to '.' (putting the SVG files with the XML source files)")
+    parser.add_argument('--keep-smcat', action='store_true', help="Whether to NOT delete intermediary SMCAT files after producing SVG output. Default = off (delete files)")
+    parser.add_argument('--no-svg', action='store_true', help="Don't produce SVG output. This option only makes sense in combination with the --keep-smcat option. Default = off")
+    parser.add_argument('--pool-size', metavar='INT', type=int, default=multiprocessing.cpu_count()+1, help="Number of worker processes. Default = CPU count + 1.")
+    args = parser.parse_args()
+
+    srcs = get_files(args.path, filter=lambda file: file.endswith(".statechart.xml"))
+
+    if len(srcs):
+      if not args.no_svg:
+        try:
+          subprocess.run(["state-machine-cat", "-h"], capture_output=True)
+        except:
+            print("Failed to run 'state-machine-cat'. Make sure this application is installed on your system.")
+            exit()
+    else:
+      print("No input files specified.")      
+      print()
+      parser.print_usage()
+      exit()
+
+
+    def process(src):
+      model = load_statechart_as_model(src)
+
+      # Produce an output file for each class in the src file
+      for class_name, sc in model.classes.items():
+        target_path = lambda ext: os.path.join(args.output_dir, dropext(src)+ext)
+        smcat_target = target_path('.smcat')
+        svg_target = target_path('.svg')
+        
+        make_dirs(smcat_target)
+
+        f = open(smcat_target, 'w')
+        w = FormattedWriter(f)
+
+        def name_to_label(name):
+          label = name.split('/')[-1]
+          return label if len(label) else "root"
+        def name_to_name(name):
+          return name.replace('/','_')
+
+        # Used for drawing initial state
+        class PseudoState:
+          def __init__(self, name):
+            self.name = name
+        # Used for drawing initial state
+        class PseudoTransition:
+          def __init__(self, source, targets):
+            self.source = source
+            self.targets = targets
+            self.trigger = None
+            self.actions = []
+
+        transitions = []
+
+        def write_state(s, hide=False):
+          if not hide:
+            w.write(name_to_name(s.name))
+            w.extendWrite(' [label="')
+            w.extendWrite(name_to_label(s.name))
+            w.extendWrite('"')
+            if isinstance(s, ParallelState):
+              w.extendWrite(' type=parallel')
+            elif isinstance(s, ShallowHistoryState):
+              w.extendWrite(' type=history')
+            elif isinstance(s, DeepHistoryState):
+              w.extendWrite(' type=deephistory')
+            else:
+              w.extendWrite(' type=regular')
+            w.extendWrite(']')
+          if s.enter or s.exit:
+            w.extendWrite(' :')
+            for a in s.enter:
+              w.write("onentry/ "+a.render())
+            for a in s.exit:
+              w.write("onexit/ "+a.render())
+            w.write()
+          if s.children:
+            if not hide:
+              w.extendWrite(' {')
+              w.indent()
+            if s.default_state:
+              w.write(name_to_name(s.name)+'_initial [type=initial],')
+              transitions.append(PseudoTransition(source=PseudoState(s.name+'/initial'), targets=[s.default_state]))
+            for i, c in enumerate(s.children):
+              write_state(c)
+              w.extendWrite(',' if i < len(s.children)-1 else ';')
+            if not hide:
+              w.dedent()
+              w.write('}')
+          transitions.extend(s.transitions)
+
+        write_state(sc.root, hide=True)
+
+        ctr = 0
+        for t in transitions:
+          label = ""
+          if t.trigger:
+            label += t.trigger.render()
+          if t.actions:
+            raises = [a for a in t.actions if isinstance(a, RaiseEvent)]
+            label += ','.join([r.render() for r in raises])
+
+          if len(t.targets) == 1:
+            w.write(name_to_name(t.source.name) + ' -> ' + name_to_name(t.targets[0].name))
+            if label:
+              w.extendWrite(': '+label)
+            w.extendWrite(';')
+          else:
+            w.write(name_to_name(t.source.name) + ' -> ' + ']split'+str(ctr))
+            if label:
+                w.extendWrite(': '+label)
+            w.extendWrite(';')
+            for tt in t.targets:
+              w.write(']split'+str(ctr) + ' -> ' + name_to_name(tt.name))
+              w.extendWrite(';')
+            ctr += 1
+
+        f.close()
+        if args.keep_smcat:
+          print("Wrote "+smcat_target)
+        if not args.no_svg:
+          subprocess.run(["state-machine-cat", smcat_target, "-o", svg_target])
+          print("Wrote "+svg_target)
+        if not args.keep_smcat:
+          os.remove(smcat_target)
+
+    pool_size = min(args.pool_size, len(srcs))
+    with multiprocessing.Pool(processes=pool_size) as pool:
+      print("Created a pool of %d processes."%pool_size)
+      pool.map(process, srcs)

+ 3 - 1
test/test2.py

@@ -15,7 +15,9 @@ if __name__ == '__main__':
 
     suite = unittest.TestSuite()
     for src_file in src_files:
-        suite.addTest(load_test(src_file))
+        tests = load_test(src_file)
+        for test in tests:
+            suite.addTest(test)
 
     if len(src_files) == 0:
         print("No input files specified.")