Browse Source

Better to generate SMCAT files in python than with XSLT

Joeri Exelmans 5 years ago
parent
commit
2e47398f26
6 changed files with 188 additions and 66 deletions
  1. 38 0
      test/lib/builder.py
  2. 29 0
      test/lib/os_tools.py
  3. 83 0
      test/render.py
  4. 2 2
      test/render.sh
  5. 16 4
      test/sccd_to_cat.xsl
  6. 20 60
      test/test.py

+ 38 - 0
test/lib/builder.py

@@ -0,0 +1,38 @@
+import os
+import importlib
+from sccd.compiler.sccdc import generate
+
+def dropext(file):
+  return os.path.splitext(src_file)[0]
+
+class Builder:
+  def __init__(self, build_dir: str):
+    self.build_dir = build_dir
+
+  def dropext(self, src_file: str) -> str:
+
+  def target_file(self, src_file: str) -> str:
+    return os.path.join(self.build_dir, dropext(src_file)+".py")
+
+  def module_name(self, src_file: str) -> str:
+    return os.path.join(self.build_dir, dropext(src_file)).replace(os.path.sep, ".")
+
+  def build(self, src_file: str):
+    target_file = self.target_file(src_file)
+
+    # Get src_file and target_file modification times
+    src_file_mtime = os.path.getmtime(src_file)
+    target_file_mtime = 0
+    try:
+        target_file_mtime = os.path.getmtime(target_file)
+    except FileNotFoundError:
+        pass
+
+    if src_file_mtime > target_file_mtime:
+        # (Re-)Compile test
+        os.makedirs(os.path.dirname(target_file), exist_ok=True)
+        generate(src_file, target_file, "python", Platforms.Threads)
+
+  def build_and_load(self, src_file: str):
+    self.build(src_file)
+    return importlib.import_module(self.module_name(src_file))

+ 29 - 0
test/lib/os_tools.py

@@ -0,0 +1,29 @@
+import os
+from typing import List, Callable
+
+# For a given list of files and or directories, get all the 
+def get_files(paths: List[str], filter: Callable[[str], bool]) -> List[str]:
+  already_have = set()
+  src_files = []
+
+  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)
+
+  return src_files
+
+xml_filter = lambda x: x.endswith('.xml')

+ 83 - 0
test/render.py

@@ -0,0 +1,83 @@
+import argparse
+import sys
+from lib.os_tools import *
+from lib.builder import Builder, dropext
+from sccd.compiler.utils import FormattedWriter
+from sccd.runtime.statecharts_core 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='BUILD_DIR', type=str, default='build', help="Directory for output files. Defaults to 'build'")
+    args = parser.parse_args()
+
+    builder = Builder(args.build_dir)
+    srcs = get_files(args.path, filter=xml_filter)
+
+    for src in srcs:
+      module = builder.build_and_load(src)
+      model = module.Model()
+
+      for class_name, _class in model.classes.items():
+        f = open(dropext(src)+'_'+class_name+'.smcat', 'w')
+        w = FormattedWriter(f)
+        sc = _class().statechart
+
+        def name_to_label(name):
+          label = name.split('/')[-1]
+          return label if len(label) else "root"
+        def name_to_name(name):
+          return name.replace('/','_')
+        class PseudoState:
+          def __init__(self, name):
+            self.name = name
+        class PseudoTransition:
+          def __init__(self, source, targets):
+            self.source = source
+            self.targets = targets
+            self.trigger = None
+
+        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, HistoryState):
+              w.extendWrite(' type=history')
+            elif isinstance(s, DeepHistoryState):
+              w.extendWrite(' type=deephistory')
+            else:
+              w.extendWrite(' type=regular')
+            w.extendWrite(']')
+          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)
+        for t in transitions:
+          w.write(name_to_name(t.source.name) + ' -> ' + name_to_name(t.targets[0].name))
+          label = ""
+          if t.trigger and t.trigger.name:
+              label = (t.trigger.port + '.' if t.trigger.port else '') + t.trigger.name
+          if label:
+            w.extendWrite(': '+label)
+          w.extendWrite(';')
+
+        f.close()

+ 2 - 2
test/render.sh

@@ -8,7 +8,7 @@
 cd semantics
 
 for SCCDFILE in $(find . -type f -name '*.xml'); do
-  saxonb-xslt -xsl:../sccd_to_cat.xsl -s:$SCCDFILE -o:${SCCDFILE%.xml}.smcat
+  saxonb-xslt -xsl:../sccd_to_smcat.xsl -s:$SCCDFILE -o:${SCCDFILE%.xml}.smcat
   state-machine-cat ${SCCDFILE%.xml}.smcat -o ${SCCDFILE%.xml}.svg
-  rm ${SCCDFILE%.xml}.smcat
+  #rm ${SCCDFILE%.xml}.smcat
 done

+ 16 - 4
test/sccd_to_cat.xsl

@@ -13,11 +13,11 @@
 
   <xsl:template match="sccd:scxml">
     <xsl:if test="@initial">
-      initial_<xsl:value-of select="count(ancestor::*)*100+count(preceding-sibling::*)"/>,
+      initial_<xsl:value-of select="concat(string(count(ancestor::*)),'_',string(count(preceding-sibling::*)))"/>,
     </xsl:if>
     <xsl:apply-templates select="(sccd:state|sccd:parallel|sccd:history)[1]"/>
     <xsl:if test="@initial">
-      initial_<xsl:value-of select="count(ancestor::*)*100+count(preceding-sibling::*)"/>
+      initial_<xsl:value-of select="concat(string(count(ancestor::*)),'_',string(count(preceding-sibling::*)))"/>
       -> <xsl:value-of select="@initial"/>;
     </xsl:if>
   </xsl:template>
@@ -26,10 +26,22 @@
     <!-- [BEGIN-TEMPLATE-<xsl:value-of select="@id"/>] -->
     <xsl:value-of select="@id"/>
 
+    <xsl:if test="self::sccd:parallel">
+      [type=parallel]
+    </xsl:if>
+    <xsl:if test="(self::sccd:history)">
+      <xsl:if test="@type = 'deep'">
+        [type=deephistory]
+      </xsl:if>
+      <xsl:if test="not(@type = 'deep')">
+        [type=history]
+      </xsl:if>
+    </xsl:if>
+
     <xsl:if test="sccd:state|sccd:parallel|sccd:history">
       {
         <xsl:if test="@initial">
-          initial_<xsl:value-of select="count(ancestor::*)*100+count(preceding-sibling::*)"/>,
+          initial_<xsl:value-of select="concat(string(count(ancestor::*)),'_',string(count(preceding-sibling::*)))"/>,
         </xsl:if>
         <xsl:apply-templates select="(sccd:state|sccd:parallel|sccd:history)[1]"/>
       }
@@ -47,7 +59,7 @@
     </xsl:choose>
 
     <xsl:if test="@initial">
-      initial_<xsl:value-of select="count(ancestor::*)*100+count(preceding-sibling::*)"/>
+      initial_<xsl:value-of select="concat(string(count(ancestor::*)),'_',string(count(preceding-sibling::*)))"/>
       -> <xsl:value-of select="@initial"/>;
     </xsl:if>
 

+ 20 - 60
test/test.py

@@ -1,49 +1,26 @@
 import os
-import importlib
 import unittest
 import argparse
 import threading
 import queue
-
-from sccd.compiler.sccdc import generate
-from sccd.compiler.generic_generator import Platforms
 from sccd.runtime.infinity import INFINITY
 from sccd.runtime.event import Event
-from sccd.compiler.compiler_exceptions import *
 from sccd.runtime.controller import Controller
-
-BUILD_DIR = "build"
+from lib.builder import Builder
+from lib.os_tools import *
 
 class PyTestCase(unittest.TestCase):
-    def __init__(self, src_file):
+    def __init__(self, src_file, builder):
         unittest.TestCase.__init__(self)
         self.src_file = src_file
-        self.name = os.path.splitext(self.src_file)[0]
-        self.target_file = os.path.join(BUILD_DIR, self.name+".py")
+        self.builder = builder
 
     def __str__(self):
-        return self.name
+        return self.src_file
 
     def runTest(self):
-        # Get src_file and target_file modification times
-        src_file_mtime = os.path.getmtime(self.src_file)
-        target_file_mtime = 0
-        try:
-            target_file_mtime = os.path.getmtime(self.target_file)
-        except FileNotFoundError:
-            pass
-
-        if src_file_mtime > target_file_mtime:
-            # (Re-)Compile test
-            os.makedirs(os.path.dirname(self.target_file), exist_ok=True)
-            try:
-                generate(self.src_file, self.target_file, "python", Platforms.Threads)
-            except TargetLanguageException :
-                self.skipTest("meant for different target language.")
-                return
-
-        # Load compiled test
-        module = importlib.import_module(os.path.join(BUILD_DIR, self.name).replace(os.path.sep, "."))
+        # Build & load
+        module = self.builder.build_and_load(self.src_file)
         inputs = module.Test.input_events
         expected = module.Test.expected_events # list of lists of Event objects
         model = module.Model()
@@ -110,42 +87,25 @@ class PyTestCase(unittest.TestCase):
                 self.assertTrue(matches, "Slot %d entry differs: Expected %s, but got %s instead." % (slot_index, exp_slot, output))
                 slot_index += 1
 
-if __name__ == '__main__':
-    suite = unittest.TestSuite()
 
-    parser = argparse.ArgumentParser(description="Run SCCD tests.")
-    parser.add_argument('test', metavar='test_path', type=str, nargs='*', help="Test to run. Can be a XML file or a directory. If a directory, it will be recursively scanned for XML files.")
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description="Run SCCD tests.",
+        epilog="Set environment variable SCCDDEBUG=1 to display debug information about the inner workings of state machines.")
+    parser.add_argument('path', metavar='PATH', type=str, nargs='*', help="Tests to run. 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='BUILD_DIR', type=str, default='build', help="Directory for built tests. Defaults to 'build'")
     args = parser.parse_args()
 
-    already_have = set()
-    src_files = []
-
-    def add_file(path):
-        if path not in already_have:
-            already_have.add(path)
-            src_files.append(path)
-
-    for p in args.test:
-        if os.path.isdir(p):
-            # recursively scan directories
-            for r, dirs, files in os.walk(p):
-                files.sort()
-                for f in files:
-                    if f.endswith('.xml'):
-                        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)
-
-    # src_files should now contain a list of XML files that need to be compiled an ran
+    src_files = get_files(args.path, filter=xml_filter)
 
+    builder = Builder(args.build_dir)
+    suite = unittest.TestSuite()
     for src_file in src_files:
-        suite.addTest(PyTestCase(src_file))
-
-    unittest.TextTestRunner(verbosity=2).run(suite)
+        suite.addTest(PyTestCase(src_file, builder))
 
     if len(src_files) == 0:
         print("Note: no test files specified.")
         print()
-        parser.print_usage()
+        parser.print_usage()
+
+    unittest.TextTestRunner(verbosity=2).run(suite)