render.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import argparse
  2. import sys
  3. import subprocess
  4. import multiprocessing
  5. import os
  6. from sccd.util.os_tools import *
  7. from sccd.util.indenting_writer import *
  8. from sccd.statechart.parser.xml import *
  9. if __name__ == '__main__':
  10. parser = argparse.ArgumentParser(
  11. description="Render statecharts as SVG images.")
  12. 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.")
  13. 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)")
  14. parser.add_argument('--keep-smcat', action='store_true', help="Whether to NOT delete intermediary SMCAT files after producing SVG output. Default = off (delete files)")
  15. 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")
  16. parser.add_argument('--pool-size', metavar='INT', type=int, default=multiprocessing.cpu_count()+1, help="Number of worker processes. Default = CPU count + 1.")
  17. args = parser.parse_args()
  18. srcs = get_files(args.path,
  19. filter=lambda file: (file.startswith("statechart_") or file.startswith("test_") or file.startswith("model_")) and file.endswith(".xml"))
  20. if len(srcs):
  21. if not args.no_svg:
  22. try:
  23. subprocess.run(["state-machine-cat", "-h"], capture_output=True)
  24. except:
  25. print("Failed to run 'state-machine-cat'. Make sure this application is installed on your system.")
  26. exit()
  27. else:
  28. print("No input files specified.")
  29. print()
  30. parser.print_usage()
  31. exit()
  32. # From this point on, disable terminal colors as we write output files
  33. os.environ["ANSI_COLORS_DISABLED"] = "1"
  34. def process(src):
  35. try:
  36. path = os.path.dirname(src)
  37. parse_sc = statechart_parser_rules(Globals(), path, load_external=False)
  38. def find_statechart(el):
  39. def when_done(statechart):
  40. return statechart
  41. # When parsing <test>, only look for <statechart> node in it.
  42. # All other nodes will be ignored.
  43. return ({"statechart": parse_sc}, when_done)
  44. statechart = parse_f(src, {
  45. "test": find_statechart,
  46. "single_instance_cd": find_statechart,
  47. "statechart": parse_sc,
  48. }, ignore_unmatched=True)
  49. assert isinstance(statechart, Statechart)
  50. root = statechart.tree.root
  51. target_path = lambda ext: os.path.join(args.output_dir, dropext(src)+ext)
  52. smcat_target = target_path('.smcat')
  53. svg_target = target_path('.svg')
  54. make_dirs(smcat_target)
  55. f = open(smcat_target, 'w')
  56. w = IndentingWriter(f)
  57. def name_to_label(state):
  58. label = state.opt.full_name.split('/')[-1]
  59. if state.stable:
  60. label += " ✓"
  61. return label if len(label) else "root"
  62. def name_to_name(name):
  63. return name.replace('/','_')
  64. # Used for drawing initial state
  65. class PseudoState:
  66. @dataclass
  67. class Opt:
  68. full_name: str
  69. def __init__(self, name):
  70. self.stable = False
  71. self.opt = PseudoState.Opt(name)
  72. # Used for drawing initial state
  73. class PseudoTransition:
  74. def __init__(self, source, target):
  75. self.source = source
  76. self.target = target
  77. self.guard = None
  78. self.trigger = None
  79. self.actions = []
  80. transitions = []
  81. def write_state(s, hide=False):
  82. if not hide:
  83. w.write(name_to_name(s.opt.full_name))
  84. w.extendWrite(' [label="')
  85. w.extendWrite(name_to_label(s))
  86. w.extendWrite('"')
  87. if isinstance(s, ParallelState):
  88. w.extendWrite(' type=parallel')
  89. elif isinstance(s, ShallowHistoryState):
  90. w.extendWrite(' type=history')
  91. elif isinstance(s, DeepHistoryState):
  92. w.extendWrite(' type=deephistory')
  93. else:
  94. w.extendWrite(' type=regular')
  95. w.extendWrite(']')
  96. if s.enter or s.exit:
  97. w.extendWrite(' :')
  98. for a in s.enter:
  99. w.write("enter "+a.render())
  100. for a in s.exit:
  101. w.write("exit "+a.render())
  102. w.write()
  103. if s.children:
  104. if not hide:
  105. w.extendWrite(' {')
  106. w.indent()
  107. if s.default_state:
  108. w.write(name_to_name(s.opt.full_name)+'_initial [type=initial],')
  109. transitions.append(PseudoTransition(source=PseudoState(s.opt.full_name+'/initial'), target=s.default_state))
  110. s.children.reverse() # this appears to put orthogonal components in the right order :)
  111. for i, c in enumerate(s.children):
  112. write_state(c)
  113. w.extendWrite(',' if i < len(s.children)-1 else ';')
  114. if not hide:
  115. w.dedent()
  116. w.write('}')
  117. transitions.extend(s.transitions)
  118. write_state(root, hide=True)
  119. ctr = 0
  120. for t in transitions:
  121. label = ""
  122. if t.trigger:
  123. label += t.trigger.render()
  124. if t.guard:
  125. label += ' ['+t.guard.render()+']'
  126. if t.actions:
  127. label += ' '.join(a.render() for a in t.actions)
  128. w.write(name_to_name(t.source.opt.full_name) + ' -> ' + name_to_name(t.target.opt.full_name))
  129. if label:
  130. w.extendWrite(': '+label)
  131. w.extendWrite(';')
  132. f.close()
  133. if args.keep_smcat:
  134. print("Wrote "+smcat_target)
  135. if not args.no_svg:
  136. subprocess.run(["state-machine-cat", smcat_target, "-o", svg_target])
  137. print("Wrote "+svg_target)
  138. if not args.keep_smcat:
  139. os.remove(smcat_target)
  140. except SkipFile:
  141. # Raised for test files that have their statechart defined in an external file.
  142. # We skip these files because we parse that external file already directly.
  143. # print("Skip", src)
  144. pass
  145. except Exception as e:
  146. import traceback
  147. exc_info = sys.exc_info()
  148. traceback.print_exception(*exc_info)
  149. pool_size = min(args.pool_size, len(srcs))
  150. with multiprocessing.Pool(processes=pool_size) as pool:
  151. print("Created a pool of %d processes."%pool_size)
  152. pool.map(process, srcs)