render.py 6.5 KB

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