Browse Source

Tests beginning with "fail_" instead of "test_" should give an error when loading. Add some tests of illegal situations.

Joeri Exelmans 5 years ago
parent
commit
431c30a80c

+ 2 - 2
src/sccd/execution/statechart_instance.py

@@ -94,8 +94,8 @@ class StatechartInstance(Instance):
         stable |= not input_events and not arenas_changed
 
         if arenas_changed:
-            print_debug(termcolor.colored('completed big step (time=%d)'%now+(" (stable)" if stable else ""), 'red'))
+            print_debug('completed big step (time=%d)'%now+(" (stable)" if stable else ""))
         else:
-            print_debug(termcolor.colored("(stable)" if stable else "", 'red'))
+            print_debug("(stable)" if stable else "")
 
         return (stable, output)

+ 6 - 6
src/sccd/model/xml_loader.py

@@ -178,13 +178,13 @@ def load_statechart(context: Context, sc_node) -> Statechart:
   datamodel_node = sc_node.find("datamodel")
   datamodel = load_datamodel(context, datamodel_node)
 
-  # tree_node = sc_node.find("tree")
-  # handler = TreeHandler(context)
-  # parse(ET.iterwalk(tree_node, events=("start", "end")), handler)
-  # state_tree = handler.tree
-
   tree_node = sc_node.find("tree")
-  state_tree = load_tree(context, datamodel, tree_node)
+  handler = TreeHandler(context, datamodel)
+  parse(ET.iterwalk(tree_node, events=("start", "end")), handler)
+  state_tree = handler.tree
+
+  # tree_node = sc_node.find("tree")
+  # state_tree = load_tree(context, datamodel, tree_node)
 
   semantics_node = sc_node.find("semantics")
   semantics = Semantics() # start with default semantics

+ 28 - 16
src/sccd/model/xml_parser.py

@@ -10,11 +10,12 @@ class XmlLoadError(Exception):
       parent = el
     # el = parent
     lines = etree.tostring(parent).decode('utf-8').strip().split('\n')
+    nbr_lines = len(etree.tostring(el).decode('utf-8').strip().split('\n'))
     lines_numbers = []
     l = parent.sourceline
     for line in lines:
       ll = ("%4d: " % l) + line
-      if l == el.sourceline:
+      if l >= el.sourceline and l < el.sourceline + nbr_lines:
         ll = termcolor.colored(ll, 'yellow')
       lines_numbers.append(ll)
       l += 1
@@ -47,8 +48,9 @@ class ElementHandler:
 
 class TreeHandler(ElementHandler):
 
-  def __init__(self, context):
+  def __init__(self, context, datamodel):
     self.context = context
+    self.datamodel = datamodel
 
   def end_raise(self, el):
     name = el.get("event")
@@ -63,7 +65,7 @@ class TreeHandler(ElementHandler):
     return a
 
   def end_code(self, el):
-    block = parse_block(self.context, block=el.text)
+    block = parse_block(self.context, self.datamodel, block=el.text)
     a = Code(block)
     self.top("actions").append(a)
     return a
@@ -76,7 +78,7 @@ class TreeHandler(ElementHandler):
 
     parent_children = self.top("state_children")
     already_there = parent_children.setdefault(short_name, state)
-    if already_there != state:
+    if already_there is not state:
       raise XmlLoadError(el, "Sibling state with the same id exists.")
 
     self.push("state", state)
@@ -157,8 +159,10 @@ class TreeHandler(ElementHandler):
 
   def end_tree(self, el):
     root_states = self.pop("state_children")
-    if len(root_states) != 1:
-      raise XmlLoadError(el, "More than one root <state>.")
+    if len(root_states) == 0:
+      raise XmlLoadError(el, "Missing root <state> !")
+    if len(root_states) > 1:
+      raise XmlLoadError(el, "Only one root <state> allowed.")
     root = list(root_states.values())[0]
 
     transitions = self.pop("transitions")
@@ -191,7 +195,7 @@ class TreeHandler(ElementHandler):
 
       # Trigger
       if after is not None:
-        after_expr = parse_expression(self.context, expr=after)
+        after_expr = parse_expression(self.context, self.datamodel, expr=after)
         # print(after_expr)
         event = "_after%d" % next_after_id # transition gets unique event name
         next_after_id += 1
@@ -207,7 +211,7 @@ class TreeHandler(ElementHandler):
       # Guard
       if cond is not None:
         try:
-          expr = parse_expression(self.context, expr=cond)
+          expr = parse_expression(self.context, self.datamodel, expr=cond)
         except Exception as e:
           raise XmlLoadError(t_el, "Condition '%s': %s" % (cond, str(e)))
         transition.guard = expr
@@ -222,15 +226,23 @@ def parse(event_generator, handler: ElementHandler):
   # for event, el in etree.iterparse(file, events=("start", "end")):
   for event, el in event_generator:
 
-    if event == "start":
-      start_method = getattr(handler, "start_"+el.tag, None)
-      if start_method:
-        start_method(el)
+    try:
 
-    elif event == "end":
-      end_method = getattr(handler, "end_"+el.tag)
-      if end_method:
-        end_method(el)
+      if event == "start":
+        start_method = getattr(handler, "start_"+el.tag, None)
+        if start_method:
+          start_method(el)
+
+      elif event == "end":
+        end_method = getattr(handler, "end_"+el.tag)
+        if end_method:
+          end_method(el)
+
+    except XmlLoadError:
+      raise
+    # Decorate non-XmlLoadErrors
+    except Exception as e:
+      raise XmlLoadError(el, e)
 
       # We don't need anything from this element anymore, so we clear it to save memory.
       # This is a technique mentioned in the lxml documentation:

+ 1 - 4
src/sccd/syntax/tree.py

@@ -185,10 +185,7 @@ class StateTree:
         def init_tree(state: State, parent_full_name: str, ancestors: List[State]):
             nonlocal next_id
 
-            if parent_full_name == '/':
-                full_name = '/' + state.short_name
-            else:
-                full_name = parent_full_name + '/' + state.short_name
+            full_name = parent_full_name + '/' + state.short_name
 
             state.gen = gen = StateGenerated(
                 state_id=next_id,

+ 62 - 54
src/sccd/test/xml_loader.py

@@ -1,79 +1,87 @@
 import os
-import lxml.etree as ET
+import lxml.etree as etree
 from lark import Lark, Transformer
 from sccd.test.test import *
 from sccd.model.model import *
 from sccd.model.xml_loader import *
 from sccd.syntax.statechart import *
+from sccd.util.debug import *
 from copy import deepcopy
 
-# For a test with "should_fail_load" attribute set, we generate a succeeding test if the loading failed :)
-class PseudoTest(unittest.TestCase):
-  def __init__(self, name: str, failed: bool = False, msg: str = ""):
+class PseudoSucceededTest(unittest.TestCase):
+  def __init__(self, name: str, msg):
     super().__init__()
     self.name = name
-    self.failed = failed
     self.msg = msg
 
   def __str__(self):
     return self.name
 
   def runTest(self):
-    if self.failed:
-      self.fail(self.msg)
+    print_debug(self.msg)
 
+class PseudoFailedTest(unittest.TestCase):
+  def __init__(self, name: str, e: Exception):
+    super().__init__()
+    self.name = name
+    self.e = e
+
+  def __str__(self):
+    return self.name
+
+  def runTest(self):
+    raise self.e
 
 # Returned list contains more than one test if the semantic configuration contains wildcard values.
 def load_test(src_file) -> List[Test]:
-  print("loading", src_file, "...")
+  should_fail = os.path.basename(src_file).startswith("fail_")
+
   namespace = Context()
 
-  test_node = ET.parse(src_file).getroot()
-  # should_fail_load = test_node.get("should_fail_load", "") == "true"
-
-# try:
-  sc_node = test_node.find("statechart")
-  src = sc_node.get("src")
-  if src is None:
-    statechart = load_statechart(namespace, sc_node)
-  else:
-    external_file = os.path.join(os.path.dirname(src_file), src)
-    print("loading", external_file, "...")
-    external_node = ET.parse(external_file).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)
-
-  def variant_description(i, variant) -> str:
-    if not variant:
-      return ""
-    return " (variant %d: %s)" % (i, ",".join(str(val) for val in variant.values()))
-
-# except Exception as e:
-#   if should_fail_load:
-#     print("load failed as expected:", e)
-#     return [ PseudoTest(name=src_file) ]
-#   else:
-#     return [ PseudoTest(name=src_file, failed=True, msg=str(e)) ]
-
-# if should_fail_load:
-#   return [ PseudoTest(name=src_file, failed=True, msg="Should not have succeeded at loading test.") ]
-
-  return [
-    Test(
-      name=src_file + variant_description(i, variant),
-      model=SingleInstanceModel(
-        namespace,
-        Statechart(tree=statechart.tree, datamodel=deepcopy(statechart.datamodel), semantics=dataclasses.replace(statechart.semantics, **variant))),
-      input=input,
-      output=output)
-    for i, variant in enumerate(statechart.semantics.wildcard_cart_product())
-  ]
+  test_node = etree.parse(src_file).getroot()
+
+  try:
+    sc_node = test_node.find("statechart")
+    src = sc_node.get("src")
+    if src is None:
+      statechart = load_statechart(namespace, sc_node)
+    else:
+      external_file = os.path.join(os.path.dirname(src_file), src)
+      # print("loading", external_file, "...")
+      external_node = etree.parse(external_file).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)
+
+    def variant_description(i, variant) -> str:
+      if not variant:
+        return ""
+      return " (variant %d: %s)" % (i, ",".join(str(val) for val in variant.values()))
+
+    if should_fail:
+      return [PseudoFailedTest(name=src_file, e=Exception("Unexpectedly succeeded at loading."))]
+    else:
+      return [
+        Test(
+          name=src_file + variant_description(i, variant),
+          model=SingleInstanceModel(
+            namespace,
+            Statechart(tree=statechart.tree, datamodel=deepcopy(statechart.datamodel), semantics=dataclasses.replace(statechart.semantics, **variant))),
+          input=input,
+          output=output)
+        for i, variant in enumerate(statechart.semantics.wildcard_cart_product())
+      ]
+
+  except Exception as e:
+    if should_fail:
+      return [PseudoSucceededTest(name=src_file, msg=str(e))]
+    else:
+      return [PseudoFailedTest(name=src_file, e=e)]
 
 def load_input(input_node) -> TestInput:
   input = []

+ 21 - 21
test/lib/os_tools.py

@@ -8,33 +8,33 @@ filter_xml = lambda x: x.endswith('.xml')
 # recursively find all the files that adhere to an optional filename filter,
 # merge the results while eliminating duplicates.
 def get_files(paths: List[str], filter: Callable[[str], bool] = filter_any) -> List[str]:
-  already_have: Set[str] = set()
-  src_files = []
+    already_have: Set[str] = set()
+    src_files = []
 
-  def add_file(path):
-      if path not in already_have:
-          already_have.add(path)
-          src_files.append(path)
+    def add_file(path):
+        if path not in already_have:
+            already_have.add(path)
+            src_files.append(path)
 
-  for p in paths:
-      if os.path.isdir(p):
-          # recursively scan directories
-          for r, dirs, files in os.walk(p):
-              files.sort()
-              for f in files:
-                  if filter(f):
-                      add_file(os.path.join(r,f))
-      elif os.path.isfile(p):
-          add_file(p)
-      else:
-          print("%s: not a file or a directory, skipped." % p)
+    for p in paths:
+        if os.path.isdir(p):
+            # recursively scan directories
+            for r, dirs, files in os.walk(p):
+                files.sort()
+                for f in files:
+                    if filter(f):
+                        add_file(os.path.join(r,f))
+        elif os.path.isfile(p):
+            add_file(p)
+        else:
+            print("%s: not a file or a directory, skipped." % p)
 
-  return src_files
+    return src_files
 
 # Drop file extension
 def dropext(file):
-  return os.path.splitext(file)[0]
+    return os.path.splitext(file)[0]
 
 # Ensure path of directories exists
 def make_dirs(file):
-  os.makedirs(os.path.dirname(file), exist_ok=True)
+    os.makedirs(os.path.dirname(file), exist_ok=True)

+ 1 - 1
test/test.py

@@ -11,7 +11,7 @@ if __name__ == '__main__':
     args = parser.parse_args()
 
     src_files = get_files(args.path,
-        filter=lambda file: file.startswith("test_") and file.endswith(".xml"))
+        filter=lambda file: (file.startswith("test_") or file.startswith("fail_")) and file.endswith(".xml"))
 
     suite = unittest.TestSuite()
     for src_file in src_files:

+ 0 - 4
test/test_files/features/datamodel/fail_test_static_types.xml

@@ -14,11 +14,7 @@
             <!-- illegal assignment, LHS is int, RHS is string -->
             <code>x="hello"</code>
           </onentry>
-          <!-- illegal condition, x is type 'int' -->
-          <transition cond="x == True" target="/b" />
         </state>
-
-        <state id="b"/>
       </state>
     </tree>
   </statechart>

+ 1 - 0
test/test_files/features/datamodel/test_guard_action.xml

@@ -4,6 +4,7 @@
     <semantics/>
     <datamodel>
       <var id="x" expr="0"/>
+      <var id="y" expr="x"/><!-- this is allowed, y is also 0 -->
     </datamodel>
     <tree>
       <state initial="counting">

+ 0 - 26
test/test_files/features/datamodel/test_static_types.xml

@@ -1,26 +0,0 @@
-<?xml version="1.0" ?>
-<test should_fail_load="true">
-  <statechart>
-    <semantics/>
-
-    <datamodel>
-      <var id="x" expr="5"/>
-    </datamodel>
-
-    <tree>
-      <state initial="a">
-        <state id="a">
-          <transition cond="x == True" target="/b">
-            <!-- illegal assignment, x is type 'int' -->
-            <!-- <code>x = "hello"</code> -->
-          </transition>
-        </state>
-
-        <state id="b"/>
-      </state>
-    </tree>
-  </statechart>
-
-  <output>
-  </output>
-</test>

+ 18 - 0
test/test_files/syntax/fail_missing_initial.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <!-- missing initial -->
+      <state>
+        <state id="a"/>
+        <state id="b"/>
+      </state>
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 14 - 0
test/test_files/syntax/fail_missing_root.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <!-- not allowed: no root state -->
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 16 - 0
test/test_files/syntax/fail_multiple_root.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <!-- not allowed: more than one root state -->
+      <state id="a"/>
+      <state id="b"/>
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 17 - 0
test/test_files/syntax/fail_sibling_id.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <state initial="a">
+        <state id="a"/>
+        <state id="a"/>
+      </state>
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 30 - 0
test/test_files/syntax/test_no_id.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <!-- At this time, states without id implicitly have id="".
+           It seems to give no problems since every state in the hierarchy still
+           has a globally unique name, '/' for root, '//' for the id-less child of root, etc.
+           This may be changed in the future. -->
+      <state>
+        <state initial="">
+          <state>
+            <transition target="../b"/>
+          </state>
+          <state id="b">
+            <onentry><raise event="ok" port="out"/></onentry>
+          </state>
+        </state>
+      </state>
+    </tree>
+  </statechart>
+
+  <output>
+    <big_step>
+      <event name="ok" port="out" />
+    </big_step>
+  </output>
+</test>