ceph/doc/scripts/gen_state_diagram.py
Kefu Chai fa7ea35832 script/gen_state_diagram.py: s/python/python3/
python does not necessarily on a modern distro which has only python3,
so use python3 instead.

Signed-off-by: Kefu Chai <kchai@redhat.com>
2020-08-28 20:35:21 +08:00

216 lines
7.6 KiB
Python
Executable File

#!/usr/bin/python3
import re
import sys
def do_filter(generator):
return acc_lines(remove_multiline_comments(to_char(remove_single_line_comments(generator))))
def acc_lines(generator):
current = ""
for i in generator:
current += i
if i == ';' or \
i == '{' or \
i == '}':
yield current.lstrip("\n")
current = ""
def to_char(generator):
for line in generator:
for char in line:
if char != '\n':
yield char
else:
yield ' '
def remove_single_line_comments(generator):
for i in generator:
if len(i) and i[0] == '#':
continue
yield re.sub(r'//.*', '', i)
def remove_multiline_comments(generator):
saw = ""
in_comment = False
for char in generator:
if in_comment:
if saw == "*":
if char == "/":
in_comment = False
saw = ""
if char == "*":
saw = "*"
continue
if saw == "/":
if char == '*':
in_comment = True
saw = ""
continue
else:
yield saw
saw = ""
if char == '/':
saw = "/"
continue
yield char
class StateMachineRenderer(object):
def __init__(self):
self.states = {} # state -> parent
self.machines = {} # state-> initial
self.edges = {} # event -> [(state, state)]
self.context = [] # [(context, depth_encountered)]
self.context_depth = 0
self.state_contents = {}
self.subgraphnum = 0
self.clusterlabel = {}
def __str__(self):
return "-------------------\n\nstates: %s\n\n machines: %s\n\n edges: %s\n\n context %s\n\n state_contents %s\n\n--------------------" % (
self.states,
self.machines,
self.edges,
self.context,
self.state_contents
)
def read_input(self, input_lines):
previous_line = None
for line in input_lines:
self.get_state(line)
self.get_event(line)
# pass two lines at a time to get the context so that regexes can
# match on split signatures
self.get_context(line, previous_line)
previous_line = line
def get_context(self, line, previous_line):
match = re.search(r"(\w+::)*::(?P<tag>\w+)::\w+\(const (?P<event>\w+)", line)
if match is None and previous_line is not None:
# it is possible that we need to match on the previous line as well, so join
# them to make them one line and try and get this matching
joined_line = ' '.join([previous_line, line])
match = re.search(r"(\w+::)*::(?P<tag>\w+)::\w+\(\s*const (?P<event>\w+)", joined_line)
if match is not None:
self.context.append((match.group('tag'), self.context_depth, match.group('event')))
if '{' in line:
self.context_depth += 1
if '}' in line:
self.context_depth -= 1
while len(self.context) and self.context[-1][1] == self.context_depth:
self.context.pop()
def get_state(self, line):
if "boost::statechart::state_machine" in line:
tokens = re.search(
r"boost::statechart::state_machine<\s*(\w*),\s*(\w*)\s*>",
line)
if tokens is None:
raise Exception("Error: malformed state_machine line: " + line)
self.machines[tokens.group(1)] = tokens.group(2)
self.context.append((tokens.group(1), self.context_depth, ""))
return
if "boost::statechart::state" in line:
tokens = re.search(
r"boost::statechart::state<\s*(\w*),\s*(\w*)\s*,?\s*(\w*)\s*>",
line)
if tokens is None:
raise Exception("Error: malformed state line: " + line)
self.states[tokens.group(1)] = tokens.group(2)
if tokens.group(2) not in self.state_contents.keys():
self.state_contents[tokens.group(2)] = []
self.state_contents[tokens.group(2)].append(tokens.group(1))
if tokens.group(3):
self.machines[tokens.group(1)] = tokens.group(3)
self.context.append((tokens.group(1), self.context_depth, ""))
return
def get_event(self, line):
if "boost::statechart::transition" in line:
for i in re.finditer(r'boost::statechart::transition<\s*([\w:]*)\s*,\s*(\w*)\s*>',
line):
if i.group(1) not in self.edges.keys():
self.edges[i.group(1)] = []
if not self.context:
raise Exception("no context at line: " + line)
self.edges[i.group(1)].append((self.context[-1][0], i.group(2)))
i = re.search("return\s+transit<\s*(\w*)\s*>()", line)
if i is not None:
if not self.context:
raise Exception("no context at line: " + line)
if not self.context[-1][2]:
raise Exception("no event in context at line: " + line)
if self.context[-1][2] not in self.edges.keys():
self.edges[self.context[-1][2]] = []
self.edges[self.context[-1][2]].append((self.context[-1][0], i.group(1)))
def emit_dot(self):
top_level = []
for state in self.machines.keys():
if state not in self.states.keys():
top_level.append(state)
print('Top Level States: ', top_level, file=sys.stderr)
print('digraph G {')
print('\tsize="7,7"')
print('\tcompound=true;')
for i in self.emit_state(top_level[0]):
print('\t' + i)
for i in self.edges.keys():
for j in self.emit_event(i):
print(j)
print('}')
def emit_state(self, state):
if state in self.state_contents.keys():
self.clusterlabel[state] = "cluster%s" % (str(self.subgraphnum),)
yield "subgraph cluster%s {" % (str(self.subgraphnum),)
self.subgraphnum += 1
yield """\tlabel = "%s";""" % (state,)
yield """\tcolor = "blue";"""
for j in self.state_contents[state]:
for i in self.emit_state(j):
yield "\t"+i
yield "}"
else:
found = False
for (k, v) in self.machines.items():
if v == state:
yield state+"[shape=Mdiamond];"
found = True
break
if not found:
yield state+";"
def emit_event(self, event):
def append(app):
retval = "["
for i in app:
retval += (i + ",")
retval += "]"
return retval
for (fro, to) in self.edges[event]:
appendix = ['label="%s"' % (event,)]
if fro in self.machines.keys():
appendix.append("ltail=%s" % (self.clusterlabel[fro],))
while fro in self.machines.keys():
fro = self.machines[fro]
if to in self.machines.keys():
appendix.append("lhead=%s" % (self.clusterlabel[to],))
while to in self.machines.keys():
to = self.machines[to]
yield("%s -> %s %s;" % (fro, to, append(appendix)))
INPUT_GENERATOR = do_filter(line for line in sys.stdin)
RENDERER = StateMachineRenderer()
RENDERER.read_input(INPUT_GENERATOR)
RENDERER.emit_dot()