1+ import sys
2+ import re
3+ import networkx as nx
4+ from pathlib import Path
5+ import matplotlib .pyplot as plt
6+
7+ # States that do not need to have a trigger to idle
8+ EXCLUDED_STATES = []
9+
10+ def get_states (manager_states_path ):
11+ file = Path (manager_states_path ).read_text (encoding = 'utf8' )
12+ states = re .findall (r'\s*(?:\w+)\s*\((?:[\s\S]*?)\)[,;]' , file , re .MULTILINE | re .IGNORECASE )
13+ return states
14+
15+ def parse_states (states ):
16+ parsed_states = []
17+ for state in states :
18+ state_name_match = re .match (r'^\s*(\w+)\s*\(' , state , re .IGNORECASE )
19+ sub_states = re .findall (r'\b(?:\w+\.)+\w+\b' , state )
20+ parsed_states .append ({
21+ 'stateName' : state_name_match [1 ] if state_name_match else None ,
22+ 'subStates' : sub_states
23+ })
24+ return parsed_states
25+
26+ def get_triggers (manager_path ):
27+ file = Path (manager_path ).read_text (encoding = 'utf8' )
28+ triggers = re .findall (r'addTrigger\(.+?\)' , file , re .MULTILINE | re .IGNORECASE )
29+ return triggers
30+
31+ def parse_triggers (triggers ):
32+ parsed_triggers = []
33+ for trigger in triggers :
34+ matched = re .search (r'\s*(\w+)\s*,\s*(\w+)\s*,\s*((?:[\w:]+)|(?:\(\)\s*->\s*[\w.]+\(\)))\s*\)' , trigger )
35+ if matched :
36+ parsed_triggers .append ({
37+ 'from' : matched [1 ],
38+ 'to' : matched [2 ],
39+ 'condition' : matched [3 ]
40+ })
41+ return parsed_triggers
42+
43+ def create_state_map (states , triggers ):
44+ state_map = {state ['stateName' ]: state for state in states }
45+
46+ for trigger in triggers :
47+ state = state_map .get (trigger ['from' ])
48+ if state is not None :
49+ if 'connectionsTo' not in state :
50+ state ['connectionsTo' ] = []
51+ state ['connectionsTo' ].append ({
52+ 'to' : trigger ['to' ],
53+ 'condition' : trigger ['condition' ]
54+ })
55+ return state_map
56+
57+ def generate_graph (state_map ):
58+ graph = nx .DiGraph ()
59+ for (state_name , state_info ) in state_map .items ():
60+ graph .add_node (state_name , label = state_name )
61+ for connection in state_info .get ('connectionsTo' , []):
62+ graph .add_edge (state_name , connection ['to' ], weight = 1 )
63+
64+
65+ pos = nx .shell_layout (graph )
66+ pos ["IDLE" ] = (0 , 0 )
67+
68+ fixed_nodes = ["IDLE" ]
69+ pos = nx .spring_layout (graph , pos = pos , fixed = fixed_nodes , k = 0.2 )
70+
71+ edge_labels = nx .get_edge_attributes (graph , 'label' )
72+
73+ nx .draw (graph , pos , with_labels = True , node_color = 'lightblue' , node_size = 1000 , font_size = 6 , arrows = True )
74+ nx .draw_networkx_edge_labels (graph , pos , edge_labels = edge_labels , font_size = 6 )
75+
76+ plt .title ("State Machine Visualization" )
77+ plt .tight_layout ()
78+ plt .savefig ("state_machine.png" , format = 'png' , dpi = 300 )
79+
80+
81+ def checkConnectedToIdle (state_map ):
82+ notToIdle = []
83+ for state_name , state in state_map .items ():
84+ if state_name in EXCLUDED_STATES : continue
85+ for connections in state ["connectionsTo" ]:
86+ if connections .to == "IDLE" : continue
87+ else : notToIdle .append (state_name )
88+ return notToIdle
89+
90+
91+ def main (managerStatesPath , managerPath ):
92+ states = get_states (managerStatesPath )
93+ states = parse_states (states )
94+
95+ triggers = get_triggers (managerPath )
96+ triggers = parse_triggers (triggers )
97+
98+ state_map = create_state_map (states , triggers )
99+
100+ generate_graph (state_map )
101+ notToIdle = checkConnectedToIdle (state_map )
102+ if notToIdle :
103+ print (f"ERROR: The following states are not connected to IDLE: { ', ' .join (notToIdle )} " )
104+ return 1
105+ else :
106+ print ("Success: All states are connected to IDLE." )
107+ return 0
108+ if __name__ == '__main__' :
109+ main (sys .argv [1 ], sys .argv [2 ])
0 commit comments