chartflow 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chartflow/__init__.py +28 -0
- chartflow/_verify_importexport.py +169 -0
- chartflow/edit/__init__.py +119 -0
- chartflow/edit/code_editor.py +1007 -0
- chartflow/edit/code_folder.py +523 -0
- chartflow/edit/completer.py +2652 -0
- chartflow/edit/context_analyzer.py +521 -0
- chartflow/edit/demo.py +469 -0
- chartflow/edit/line_number_area.py +548 -0
- chartflow/edit/minimap.py +581 -0
- chartflow/edit/python_editor.py +509 -0
- chartflow/edit/syntax_highlighter.py +291 -0
- chartflow/edit/test_editor.py +190 -0
- chartflow/flow/__init__.py +31 -0
- chartflow/flow/_demo.py +98 -0
- chartflow/flow/chart.py +1101 -0
- chartflow/flow/editor.py +375 -0
- chartflow/flow/flow.py +6 -0
- chartflow/flow/qchart.py +1250 -0
- chartflow/flow/test/__init__.py +0 -0
- chartflow/flow/test/test_chart.py +537 -0
- chartflow/flow/test_decorator_sync.py +102 -0
- chartflow/fsm/__init__.py +84 -0
- chartflow/fsm/decorators.py +395 -0
- chartflow/fsm/demo.py +381 -0
- chartflow/fsm/demo_pyqt6.py +881 -0
- chartflow/fsm/demo_tree.py +46 -0
- chartflow/fsm/logic.py +447 -0
- chartflow/fsm/meta.py +569 -0
- chartflow/fsm/scheduler.py +427 -0
- chartflow/fsm/stage.py +2079 -0
- chartflow/fsm/stdio.py +66 -0
- chartflow/fsm/test/__init__.py +7 -0
- chartflow/fsm/test/test_decorators.py +210 -0
- chartflow/fsm/test/test_events.py +202 -0
- chartflow/fsm/test/test_integration.py +596 -0
- chartflow/fsm/test/test_logic.py +371 -0
- chartflow/fsm/test/test_meta.py +192 -0
- chartflow/fsm/test/test_runtime_param.py +336 -0
- chartflow/fsm/test/test_scheduler.py +280 -0
- chartflow/fsm/test/test_stage.py +314 -0
- chartflow/fsm/test_behavior_events.py +263 -0
- chartflow/fsm/test_nested_parallel.py +80 -0
- chartflow/fsm/test_stage_spec.py +448 -0
- chartflow/fsm/utils.py +34 -0
- chartflow/node/__init__.py +66 -0
- chartflow/node/_node_base.py +727 -0
- chartflow/node/_node_interaction.py +488 -0
- chartflow/node/_node_interface.py +426 -0
- chartflow/node/_node_markers.py +648 -0
- chartflow/node/_node_ports.py +536 -0
- chartflow/node/_port_interface.py +111 -0
- chartflow/node/arrow_item.py +287 -0
- chartflow/node/color_utils.py +113 -0
- chartflow/node/connection_item.py +939 -0
- chartflow/node/demo.py +258 -0
- chartflow/node/demo_arrow_drag.py +46 -0
- chartflow/node/demo_decorator.py +63 -0
- chartflow/node/demo_ports.py +69 -0
- chartflow/node/enums.py +35 -0
- chartflow/node/example.py +84 -0
- chartflow/node/layout_utils.py +307 -0
- chartflow/node/main_window.py +196 -0
- chartflow/node/menu_bar.py +240 -0
- chartflow/node/node_canvas.py +1730 -0
- chartflow/node/node_item.py +754 -0
- chartflow/node/popmenu.py +472 -0
- chartflow/node/port_item.py +591 -0
- chartflow/node/qnode_editor.py +73 -0
- chartflow/node/selection_rect.py +53 -0
- chartflow/node/style_panel.py +615 -0
- chartflow/node/styles.py +63 -0
- chartflow/node/test_qnode.py +2336 -0
- chartflow/showcase.py +528 -0
- chartflow/theme.py +508 -0
- chartflow-0.1.dist-info/METADATA +23 -0
- chartflow-0.1.dist-info/RECORD +79 -0
- chartflow-0.1.dist-info/WHEEL +5 -0
- chartflow-0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qflow.chart module tests
|
|
3
|
+
|
|
4
|
+
Tests for IChart interface and FlowChart implementation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from chartflow.flow.chart import IChart, ChartNode, ChartEdge, ChartLogic
|
|
11
|
+
from chartflow.flow.qchart import QFlowChart
|
|
12
|
+
from chartflow.flow._demo import DemoChart
|
|
13
|
+
from chartflow.fsm.stage import Stage
|
|
14
|
+
from chartflow.fsm.logic import Logic
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Pre-defined Stage class for source extraction tests
|
|
18
|
+
class TestExtractStage(Stage):
|
|
19
|
+
"""Test Stage for code extraction tests."""
|
|
20
|
+
NAME = "TestExtractStage"
|
|
21
|
+
|
|
22
|
+
def idle(self, inst: Any) -> str:
|
|
23
|
+
x = 1
|
|
24
|
+
y = 2
|
|
25
|
+
return "working"
|
|
26
|
+
|
|
27
|
+
def working(self, inst: Any) -> str:
|
|
28
|
+
return "idle"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestChartNode(unittest.TestCase):
|
|
32
|
+
"""Test ChartNode dataclass."""
|
|
33
|
+
|
|
34
|
+
def testDefaultCreation(self) -> None:
|
|
35
|
+
"""Test creating ChartNode with defaults."""
|
|
36
|
+
node = ChartNode(nid="n1", name="TestNode")
|
|
37
|
+
self.assertEqual(node.nid, "n1")
|
|
38
|
+
self.assertEqual(node.name, "TestNode")
|
|
39
|
+
self.assertEqual(node.ntype, "state")
|
|
40
|
+
self.assertEqual(node.shape, "rectangle")
|
|
41
|
+
self.assertEqual(node.data, {})
|
|
42
|
+
self.assertEqual(node.states, [])
|
|
43
|
+
|
|
44
|
+
def testFullCreation(self) -> None:
|
|
45
|
+
"""Test creating ChartNode with all fields."""
|
|
46
|
+
node = ChartNode(
|
|
47
|
+
nid="n2",
|
|
48
|
+
name="FullNode",
|
|
49
|
+
ntype="logic",
|
|
50
|
+
shape="ellipse",
|
|
51
|
+
data={"key": "value"},
|
|
52
|
+
states=["idle", "active"]
|
|
53
|
+
)
|
|
54
|
+
self.assertEqual(node.nid, "n2")
|
|
55
|
+
self.assertEqual(node.name, "FullNode")
|
|
56
|
+
self.assertEqual(node.ntype, "logic")
|
|
57
|
+
self.assertEqual(node.shape, "ellipse")
|
|
58
|
+
self.assertEqual(node.data["key"], "value")
|
|
59
|
+
self.assertEqual(node.states, ["idle", "active"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TestChartEdge(unittest.TestCase):
|
|
63
|
+
"""Test ChartEdge dataclass."""
|
|
64
|
+
|
|
65
|
+
def testDefaultCreation(self) -> None:
|
|
66
|
+
"""Test creating ChartEdge with defaults."""
|
|
67
|
+
edge = ChartEdge(eid="e1", src="n1", dst="n2")
|
|
68
|
+
self.assertEqual(edge.eid, "e1")
|
|
69
|
+
self.assertEqual(edge.src, "n1")
|
|
70
|
+
self.assertEqual(edge.dst, "n2")
|
|
71
|
+
self.assertEqual(edge.label, "")
|
|
72
|
+
self.assertEqual(edge.etype, "transition")
|
|
73
|
+
self.assertFalse(edge.bidirectional)
|
|
74
|
+
|
|
75
|
+
def testFullCreation(self) -> None:
|
|
76
|
+
"""Test creating ChartEdge with all fields."""
|
|
77
|
+
edge = ChartEdge(
|
|
78
|
+
eid="e2",
|
|
79
|
+
src="n1",
|
|
80
|
+
dst="n3",
|
|
81
|
+
label="condition",
|
|
82
|
+
etype="flow",
|
|
83
|
+
bidirectional=True
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(edge.eid, "e2")
|
|
86
|
+
self.assertEqual(edge.label, "condition")
|
|
87
|
+
self.assertEqual(edge.etype, "flow")
|
|
88
|
+
self.assertTrue(edge.bidirectional)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestChartLogic(unittest.TestCase):
|
|
92
|
+
"""Test ChartLogic dataclass."""
|
|
93
|
+
|
|
94
|
+
def testDefaultCreation(self) -> None:
|
|
95
|
+
"""Test creating ChartLogic with defaults."""
|
|
96
|
+
logic = ChartLogic(node_id="n1")
|
|
97
|
+
self.assertEqual(logic.node_id, "n1")
|
|
98
|
+
self.assertEqual(logic.logic_type, "Stage")
|
|
99
|
+
self.assertEqual(logic.priority, 0)
|
|
100
|
+
self.assertEqual(logic.decorators, [])
|
|
101
|
+
self.assertEqual(logic.states, {})
|
|
102
|
+
self.assertEqual(logic.listeners, [])
|
|
103
|
+
|
|
104
|
+
def testFullCreation(self) -> None:
|
|
105
|
+
"""Test creating ChartLogic with all fields."""
|
|
106
|
+
logic = ChartLogic(
|
|
107
|
+
node_id="n2",
|
|
108
|
+
logic_type="Logic",
|
|
109
|
+
priority=10,
|
|
110
|
+
decorators=[{"type": "sequence"}],
|
|
111
|
+
states={"idle": "pass"},
|
|
112
|
+
listeners=[{"event": "tick"}]
|
|
113
|
+
)
|
|
114
|
+
self.assertEqual(logic.node_id, "n2")
|
|
115
|
+
self.assertEqual(logic.logic_type, "Logic")
|
|
116
|
+
self.assertEqual(logic.priority, 10)
|
|
117
|
+
self.assertEqual(len(logic.decorators), 1)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestIChart(unittest.TestCase):
|
|
121
|
+
"""Test IChart base class."""
|
|
122
|
+
|
|
123
|
+
def testInit(self) -> None:
|
|
124
|
+
"""Test IChart initialization."""
|
|
125
|
+
chart = QFlowChart()
|
|
126
|
+
self.assertEqual(chart.nodes, [])
|
|
127
|
+
self.assertEqual(chart.edges, [])
|
|
128
|
+
self.assertEqual(chart.logics, [])
|
|
129
|
+
self.assertIsNone(chart.startnode)
|
|
130
|
+
|
|
131
|
+
def testAddNode(self) -> None:
|
|
132
|
+
"""Test adding nodes."""
|
|
133
|
+
chart = QFlowChart()
|
|
134
|
+
node = ChartNode(nid="n1", name="Node1")
|
|
135
|
+
chart.addNode(node)
|
|
136
|
+
self.assertEqual(len(chart.nodes), 1)
|
|
137
|
+
self.assertEqual(chart.getNode("n1"), node)
|
|
138
|
+
|
|
139
|
+
def testRemoveNode(self) -> None:
|
|
140
|
+
"""Test removing nodes."""
|
|
141
|
+
chart = QFlowChart()
|
|
142
|
+
node = ChartNode(nid="n1", name="Node1")
|
|
143
|
+
chart.addNode(node)
|
|
144
|
+
removed = chart.removeNode("n1")
|
|
145
|
+
self.assertEqual(removed, node)
|
|
146
|
+
self.assertIsNone(chart.getNode("n1"))
|
|
147
|
+
|
|
148
|
+
def testAddEdge(self) -> None:
|
|
149
|
+
"""Test adding edges."""
|
|
150
|
+
chart = QFlowChart()
|
|
151
|
+
edge = ChartEdge(eid="e1", src="n1", dst="n2")
|
|
152
|
+
chart.addEdge(edge)
|
|
153
|
+
self.assertEqual(len(chart.edges), 1)
|
|
154
|
+
self.assertEqual(chart.getEdge("e1"), edge)
|
|
155
|
+
|
|
156
|
+
def testRemoveEdge(self) -> None:
|
|
157
|
+
"""Test removing edges."""
|
|
158
|
+
chart = QFlowChart()
|
|
159
|
+
edge = ChartEdge(eid="e1", src="n1", dst="n2")
|
|
160
|
+
chart.addEdge(edge)
|
|
161
|
+
removed = chart.removeEdge("e1")
|
|
162
|
+
self.assertEqual(removed, edge)
|
|
163
|
+
self.assertIsNone(chart.getEdge("e1"))
|
|
164
|
+
|
|
165
|
+
def testGetOutgoing(self) -> None:
|
|
166
|
+
"""Test getting outgoing edges."""
|
|
167
|
+
chart = QFlowChart()
|
|
168
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
169
|
+
chart.addNode(ChartNode(nid="n2", name="Node2"))
|
|
170
|
+
chart.addNode(ChartNode(nid="n3", name="Node3"))
|
|
171
|
+
edge1 = ChartEdge(eid="e1", src="n1", dst="n2")
|
|
172
|
+
edge2 = ChartEdge(eid="e2", src="n1", dst="n3")
|
|
173
|
+
edge3 = ChartEdge(eid="e3", src="n2", dst="n3")
|
|
174
|
+
chart.addEdge(edge1)
|
|
175
|
+
chart.addEdge(edge2)
|
|
176
|
+
chart.addEdge(edge3)
|
|
177
|
+
|
|
178
|
+
outgoing = chart.getOutgoing("n1")
|
|
179
|
+
self.assertEqual(len(outgoing), 2)
|
|
180
|
+
self.assertIn(edge1, outgoing)
|
|
181
|
+
self.assertIn(edge2, outgoing)
|
|
182
|
+
|
|
183
|
+
def testGetIncoming(self) -> None:
|
|
184
|
+
"""Test getting incoming edges."""
|
|
185
|
+
chart = QFlowChart()
|
|
186
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
187
|
+
chart.addNode(ChartNode(nid="n2", name="Node2"))
|
|
188
|
+
chart.addNode(ChartNode(nid="n3", name="Node3"))
|
|
189
|
+
edge1 = ChartEdge(eid="e1", src="n1", dst="n3")
|
|
190
|
+
edge2 = ChartEdge(eid="e2", src="n2", dst="n3")
|
|
191
|
+
chart.addEdge(edge1)
|
|
192
|
+
chart.addEdge(edge2)
|
|
193
|
+
|
|
194
|
+
incoming = chart.getIncoming("n3")
|
|
195
|
+
self.assertEqual(len(incoming), 2)
|
|
196
|
+
|
|
197
|
+
def testGetConnected(self) -> None:
|
|
198
|
+
"""Test getting connected nodes."""
|
|
199
|
+
chart = QFlowChart()
|
|
200
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
201
|
+
chart.addNode(ChartNode(nid="n2", name="Node2"))
|
|
202
|
+
chart.addNode(ChartNode(nid="n3", name="Node3"))
|
|
203
|
+
chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
|
|
204
|
+
chart.addEdge(ChartEdge(eid="e2", src="n1", dst="n3", bidirectional=True))
|
|
205
|
+
|
|
206
|
+
connected = chart.getConnected("n1")
|
|
207
|
+
self.assertEqual(len(connected), 2)
|
|
208
|
+
self.assertIn("n2", connected)
|
|
209
|
+
self.assertIn("n3", connected)
|
|
210
|
+
|
|
211
|
+
def testStartnode(self) -> None:
|
|
212
|
+
"""Test start node property."""
|
|
213
|
+
chart = QFlowChart()
|
|
214
|
+
chart.startnode = "entry"
|
|
215
|
+
self.assertEqual(chart.startnode, "entry")
|
|
216
|
+
|
|
217
|
+
def testClearChart(self) -> None:
|
|
218
|
+
"""Test clearing chart."""
|
|
219
|
+
chart = QFlowChart()
|
|
220
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
221
|
+
chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
|
|
222
|
+
chart.startnode = "n1"
|
|
223
|
+
|
|
224
|
+
chart.clearChart()
|
|
225
|
+
self.assertEqual(chart.nodes, [])
|
|
226
|
+
self.assertEqual(chart.edges, [])
|
|
227
|
+
self.assertIsNone(chart.startnode)
|
|
228
|
+
|
|
229
|
+
def testValidChart(self) -> None:
|
|
230
|
+
"""Test chart validation."""
|
|
231
|
+
chart = QFlowChart()
|
|
232
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
233
|
+
chart.addNode(ChartNode(nid="n2", name="Node2"))
|
|
234
|
+
chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
|
|
235
|
+
|
|
236
|
+
valid, msg = chart.validChart()
|
|
237
|
+
self.assertTrue(valid)
|
|
238
|
+
self.assertEqual(msg, "")
|
|
239
|
+
|
|
240
|
+
def testValidChartMissingStart(self) -> None:
|
|
241
|
+
"""Test validation with missing start node."""
|
|
242
|
+
chart = QFlowChart()
|
|
243
|
+
chart.startnode = "missing"
|
|
244
|
+
|
|
245
|
+
valid, msg = chart.validChart()
|
|
246
|
+
self.assertFalse(valid)
|
|
247
|
+
self.assertIn("Start node", msg)
|
|
248
|
+
|
|
249
|
+
def testValidChartMissingEdgeSrc(self) -> None:
|
|
250
|
+
"""Test validation with missing edge source."""
|
|
251
|
+
chart = QFlowChart()
|
|
252
|
+
chart.addNode(ChartNode(nid="n1", name="Node1"))
|
|
253
|
+
chart.addEdge(ChartEdge(eid="e1", src="missing", dst="n1"))
|
|
254
|
+
|
|
255
|
+
valid, msg = chart.validChart()
|
|
256
|
+
self.assertFalse(valid)
|
|
257
|
+
self.assertIn("source", msg)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestFlowChartJson(unittest.TestCase):
|
|
261
|
+
"""Test QFlowChart JSON import/export."""
|
|
262
|
+
|
|
263
|
+
def testFromJson(self) -> None:
|
|
264
|
+
"""Test loading from JSON."""
|
|
265
|
+
json_data = {
|
|
266
|
+
"nodes": [
|
|
267
|
+
{"id": "n1", "name": "Entry", "shape": "ELLIPSE", "type": "entry", "data": {}},
|
|
268
|
+
{"id": "n2", "name": "State1", "shape": "RECTANGLE", "type": "state", "data": {}}
|
|
269
|
+
],
|
|
270
|
+
"connections": [
|
|
271
|
+
{"source_id": "n1", "target_id": "n2", "is_bidirectional": False, "connection_type": "FLOW"}
|
|
272
|
+
],
|
|
273
|
+
"start_node_id": "n1"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
chart = QFlowChart()
|
|
277
|
+
chart.fromJson(json_data)
|
|
278
|
+
|
|
279
|
+
self.assertEqual(len(chart.nodes), 2)
|
|
280
|
+
self.assertEqual(len(chart.edges), 1)
|
|
281
|
+
self.assertEqual(chart.startnode, "n1")
|
|
282
|
+
self.assertEqual(chart.getNode("n1").name, "Entry")
|
|
283
|
+
|
|
284
|
+
def testToJson(self) -> None:
|
|
285
|
+
"""Test exporting to JSON."""
|
|
286
|
+
chart = QFlowChart()
|
|
287
|
+
chart.addNode(ChartNode(nid="n1", name="Entry", ntype="entry", shape="ellipse"))
|
|
288
|
+
chart.addNode(ChartNode(nid="n2", name="State1", ntype="state", shape="rectangle"))
|
|
289
|
+
chart.addEdge(ChartEdge(eid="e1", src="n1", dst="n2", etype="flow"))
|
|
290
|
+
chart.startnode = "n1"
|
|
291
|
+
|
|
292
|
+
json_data = chart.toJson()
|
|
293
|
+
|
|
294
|
+
self.assertEqual(len(json_data["nodes"]), 2)
|
|
295
|
+
self.assertEqual(len(json_data["connections"]), 1)
|
|
296
|
+
self.assertEqual(json_data["start_node_id"], "n1")
|
|
297
|
+
|
|
298
|
+
def testRoundTripJson(self) -> None:
|
|
299
|
+
"""Test JSON round-trip conversion."""
|
|
300
|
+
original = QFlowChart()
|
|
301
|
+
original.addNode(ChartNode(nid="n1", name="Entry", ntype="entry"))
|
|
302
|
+
original.addNode(ChartNode(nid="n2", name="Process", ntype="state"))
|
|
303
|
+
original.addEdge(ChartEdge(eid="e1", src="n1", dst="n2"))
|
|
304
|
+
original.startnode = "n1"
|
|
305
|
+
|
|
306
|
+
json_data = original.toJson()
|
|
307
|
+
restored = QFlowChart()
|
|
308
|
+
restored.fromJson(json_data)
|
|
309
|
+
|
|
310
|
+
self.assertEqual(len(restored.nodes), 2)
|
|
311
|
+
self.assertEqual(len(restored.edges), 1)
|
|
312
|
+
self.assertEqual(restored.startnode, "n1")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class TestFlowChartStage(unittest.TestCase):
|
|
316
|
+
"""Test QFlowChart Stage conversion."""
|
|
317
|
+
|
|
318
|
+
def testToStage(self) -> None:
|
|
319
|
+
"""Test converting to Stage instance."""
|
|
320
|
+
chart = DemoChart()
|
|
321
|
+
stage = chart.toStage()
|
|
322
|
+
|
|
323
|
+
self.assertIsInstance(stage, Stage)
|
|
324
|
+
|
|
325
|
+
def testFromStage(self) -> None:
|
|
326
|
+
"""Test loading from Stage instance."""
|
|
327
|
+
class TestStage(Stage):
|
|
328
|
+
NAME = "TestStage"
|
|
329
|
+
|
|
330
|
+
def idle(self, inst: Any) -> str:
|
|
331
|
+
return "working"
|
|
332
|
+
|
|
333
|
+
def working(self, inst: Any) -> str:
|
|
334
|
+
return "idle"
|
|
335
|
+
|
|
336
|
+
original = TestStage()
|
|
337
|
+
chart = QFlowChart()
|
|
338
|
+
chart.fromStage(original)
|
|
339
|
+
|
|
340
|
+
self.assertGreater(len(chart.nodes), 0)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class TestFlowChartLogic(unittest.TestCase):
|
|
344
|
+
"""Test QFlowChart Logic conversion."""
|
|
345
|
+
|
|
346
|
+
def testToLogic(self) -> None:
|
|
347
|
+
"""Test converting to Logic instance."""
|
|
348
|
+
chart = DemoChart()
|
|
349
|
+
logic = chart.toLogic()
|
|
350
|
+
|
|
351
|
+
self.assertIsInstance(logic, Logic)
|
|
352
|
+
|
|
353
|
+
def testFromLogic(self) -> None:
|
|
354
|
+
"""Test loading from Logic instance."""
|
|
355
|
+
class TestLogic(Logic):
|
|
356
|
+
NAME = "TestLogic"
|
|
357
|
+
PRIORITY = 5
|
|
358
|
+
|
|
359
|
+
def idle(self, inst: Any) -> str:
|
|
360
|
+
return "active"
|
|
361
|
+
|
|
362
|
+
def active(self, inst: Any) -> str:
|
|
363
|
+
return "idle"
|
|
364
|
+
|
|
365
|
+
original = TestLogic()
|
|
366
|
+
chart = QFlowChart()
|
|
367
|
+
chart.fromLogic(original)
|
|
368
|
+
|
|
369
|
+
self.assertGreater(len(chart.nodes), 0)
|
|
370
|
+
# Check priority metadata
|
|
371
|
+
for node in chart.nodes:
|
|
372
|
+
self.assertEqual(node.data.get("priority"), 5)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class TestExtractNodesCompatibility(unittest.TestCase):
|
|
376
|
+
"""Test QFlowChart compatibility with extractNodes/_importDict format."""
|
|
377
|
+
|
|
378
|
+
def testFromStageExtractsCode(self) -> None:
|
|
379
|
+
"""Test fromStage extracts function body code."""
|
|
380
|
+
# Use a pre-defined class from test file for reliable source extraction
|
|
381
|
+
stage = TestExtractStage()
|
|
382
|
+
chart = QFlowChart()
|
|
383
|
+
chart.fromStage(stage)
|
|
384
|
+
|
|
385
|
+
# Check code is extracted (code field exists)
|
|
386
|
+
idle_node = chart.getNode("state_0")
|
|
387
|
+
self.assertIsNotNone(idle_node)
|
|
388
|
+
self.assertIn("code", idle_node.data)
|
|
389
|
+
# Verify return statement is in code
|
|
390
|
+
self.assertIn('return "working"', idle_node.data["code"])
|
|
391
|
+
# Verify body statements are extracted
|
|
392
|
+
self.assertIn("x = 1", idle_node.data["code"])
|
|
393
|
+
self.assertIn("y = 2", idle_node.data["code"])
|
|
394
|
+
|
|
395
|
+
def testFromStageExtractsTargets(self) -> None:
|
|
396
|
+
"""Test fromStage extracts return targets."""
|
|
397
|
+
class TestStage(Stage):
|
|
398
|
+
NAME = "TestStage"
|
|
399
|
+
|
|
400
|
+
def idle(self, inst: Any) -> str:
|
|
401
|
+
return "working"
|
|
402
|
+
|
|
403
|
+
def working(self, inst: Any) -> str:
|
|
404
|
+
return "idle"
|
|
405
|
+
|
|
406
|
+
stage = TestStage()
|
|
407
|
+
chart = QFlowChart()
|
|
408
|
+
chart.fromStage(stage)
|
|
409
|
+
|
|
410
|
+
idle_node = chart.getNode("state_0")
|
|
411
|
+
self.assertIn("targets", idle_node.data)
|
|
412
|
+
self.assertEqual(idle_node.data["targets"], ["working"])
|
|
413
|
+
|
|
414
|
+
working_node = chart.getNode("state_1")
|
|
415
|
+
self.assertEqual(working_node.data["targets"], ["idle"])
|
|
416
|
+
|
|
417
|
+
def testFromStageCreatesEdgesFromTargets(self) -> None:
|
|
418
|
+
"""Test fromStage creates edges from extracted targets."""
|
|
419
|
+
class TestStage(Stage):
|
|
420
|
+
NAME = "TestStage"
|
|
421
|
+
|
|
422
|
+
def idle(self, inst: Any) -> str:
|
|
423
|
+
return "working"
|
|
424
|
+
|
|
425
|
+
def working(self, inst: Any) -> str:
|
|
426
|
+
return "idle"
|
|
427
|
+
|
|
428
|
+
stage = TestStage()
|
|
429
|
+
chart = QFlowChart()
|
|
430
|
+
chart.fromStage(stage)
|
|
431
|
+
|
|
432
|
+
# Should have edges: idle -> working, working -> idle
|
|
433
|
+
self.assertEqual(len(chart.edges), 2)
|
|
434
|
+
|
|
435
|
+
def testFromJsonExtractNodesFormat(self) -> None:
|
|
436
|
+
"""Test fromJson can load extractNodes format."""
|
|
437
|
+
extract_nodes_data = {
|
|
438
|
+
"idle": {
|
|
439
|
+
"code": "x = 1\\nreturn 'working'",
|
|
440
|
+
"targets": ["working"],
|
|
441
|
+
"decorates": ["listen"]
|
|
442
|
+
},
|
|
443
|
+
"working": {
|
|
444
|
+
"code": "return 'idle'",
|
|
445
|
+
"targets": ["idle"],
|
|
446
|
+
"decorates": []
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
chart = QFlowChart()
|
|
451
|
+
chart.fromJson(extract_nodes_data)
|
|
452
|
+
|
|
453
|
+
self.assertEqual(len(chart.nodes), 2)
|
|
454
|
+
|
|
455
|
+
idle_node = chart.getNode("state_0")
|
|
456
|
+
self.assertEqual(idle_node.name, "idle")
|
|
457
|
+
self.assertEqual(idle_node.data["code"], "x = 1\\nreturn 'working'")
|
|
458
|
+
self.assertEqual(idle_node.data["targets"], ["working"])
|
|
459
|
+
self.assertEqual(idle_node.data["decorates"], ["listen"])
|
|
460
|
+
|
|
461
|
+
def testToJsonOutputsNodeData(self) -> None:
|
|
462
|
+
"""Test toJson outputs node data including code/targets/decorates."""
|
|
463
|
+
chart = QFlowChart()
|
|
464
|
+
node = ChartNode(
|
|
465
|
+
nid="n1",
|
|
466
|
+
name="idle",
|
|
467
|
+
ntype="state",
|
|
468
|
+
data={
|
|
469
|
+
"code": "return 'working'",
|
|
470
|
+
"targets": ["working"],
|
|
471
|
+
"decorates": ["listen"]
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
chart.addNode(node)
|
|
475
|
+
|
|
476
|
+
json_data = chart.toJson()
|
|
477
|
+
|
|
478
|
+
self.assertEqual(len(json_data["nodes"]), 1)
|
|
479
|
+
self.assertEqual(json_data["nodes"][0]["data"]["code"], "return 'working'")
|
|
480
|
+
self.assertEqual(json_data["nodes"][0]["data"]["targets"], ["working"])
|
|
481
|
+
self.assertEqual(json_data["nodes"][0]["data"]["decorates"], ["listen"])
|
|
482
|
+
|
|
483
|
+
def testExtractNodesRoundTrip(self) -> None:
|
|
484
|
+
"""Test extractNodes -> fromJson round trip preserves data."""
|
|
485
|
+
original_data = {
|
|
486
|
+
"idle": {
|
|
487
|
+
"code": "x = 1\\ny = 2\\nreturn 'working'",
|
|
488
|
+
"targets": ["working"],
|
|
489
|
+
"decorates": []
|
|
490
|
+
},
|
|
491
|
+
"working": {
|
|
492
|
+
"code": "return 'idle'",
|
|
493
|
+
"targets": ["idle"],
|
|
494
|
+
"decorates": ["sequence"]
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
chart = QFlowChart()
|
|
499
|
+
chart.fromJson(original_data)
|
|
500
|
+
|
|
501
|
+
# Verify nodes created correctly
|
|
502
|
+
self.assertEqual(len(chart.nodes), 2)
|
|
503
|
+
|
|
504
|
+
# Verify edges created from targets
|
|
505
|
+
self.assertEqual(len(chart.edges), 2)
|
|
506
|
+
|
|
507
|
+
# Get node by name
|
|
508
|
+
idle_node = None
|
|
509
|
+
for node in chart.nodes:
|
|
510
|
+
if node.name == "idle":
|
|
511
|
+
idle_node = node
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
self.assertIsNotNone(idle_node)
|
|
515
|
+
self.assertEqual(idle_node.data["code"], "x = 1\\ny = 2\\nreturn 'working'")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class TestDemoChart(unittest.TestCase):
|
|
519
|
+
"""Test DemoChart factory function."""
|
|
520
|
+
|
|
521
|
+
def testDemoChart(self) -> None:
|
|
522
|
+
"""Test creating demo chart."""
|
|
523
|
+
chart = DemoChart()
|
|
524
|
+
|
|
525
|
+
self.assertEqual(len(chart.nodes), 4)
|
|
526
|
+
self.assertEqual(len(chart.edges), 4)
|
|
527
|
+
self.assertEqual(chart.startnode, "entry")
|
|
528
|
+
|
|
529
|
+
def testDemoChartValid(self) -> None:
|
|
530
|
+
"""Test demo chart is valid."""
|
|
531
|
+
chart = DemoChart()
|
|
532
|
+
valid, msg = chart.validChart()
|
|
533
|
+
self.assertTrue(valid, msg)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
unittest.main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Test decorator synchronization from Stage/Logic to qnode."""
|
|
2
|
+
|
|
3
|
+
from PyQt6.QtGui import QColor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from PyQt6.QtWidgets import QApplication
|
|
9
|
+
from PyQt6.QtCore import QPointF
|
|
10
|
+
|
|
11
|
+
from chartflow.fsm import Stage, Logic, recursive
|
|
12
|
+
from chartflow.flow import QFlowChart
|
|
13
|
+
from chartflow.node import QNodeCanvas, QNodeItem, NodeShape
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestStage(Stage):
|
|
17
|
+
"""Test stage with recursive entry state."""
|
|
18
|
+
NAME = "TestStage"
|
|
19
|
+
|
|
20
|
+
@recursive
|
|
21
|
+
def entry(self, inst, c, k):
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
def normal(self, inst, c, k):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestLogic(Logic):
|
|
29
|
+
"""Test logic with recursive and normal states."""
|
|
30
|
+
NAME = "TestLogic"
|
|
31
|
+
|
|
32
|
+
@recursive
|
|
33
|
+
def entry(self, inst, c, k, *refs):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def process(self, inst, c, k, *refs):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestDecoratorSync(unittest.TestCase):
|
|
41
|
+
"""Test decorator synchronization from Stage/Logic to qnode nodes."""
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def setUpClass(cls):
|
|
45
|
+
cls.app = QApplication.instance()
|
|
46
|
+
if cls.app is None:
|
|
47
|
+
cls.app = QApplication(sys.argv)
|
|
48
|
+
|
|
49
|
+
def testStageRecursiveDecorator(self):
|
|
50
|
+
"""Test that recursive state gets decorator in QFlowChart."""
|
|
51
|
+
chart = QFlowChart()
|
|
52
|
+
stage = TestStage()
|
|
53
|
+
chart.setStage(stage)
|
|
54
|
+
|
|
55
|
+
# Check that entry node has recursive decorator
|
|
56
|
+
entry_node = chart._nodemap.get("entry")
|
|
57
|
+
self.assertIsNotNone(entry_node)
|
|
58
|
+
self.assertEqual(entry_node.decorator, "recursive")
|
|
59
|
+
|
|
60
|
+
# Check that normal node does not have decorator
|
|
61
|
+
normal_node = chart._nodemap.get("normal")
|
|
62
|
+
self.assertIsNotNone(normal_node)
|
|
63
|
+
self.assertIsNone(normal_node.decorator)
|
|
64
|
+
|
|
65
|
+
def testLogicRecursiveDecorator(self):
|
|
66
|
+
"""Test that recursive state gets decorator in QFlowChart from Logic."""
|
|
67
|
+
chart = QFlowChart()
|
|
68
|
+
logic = TestLogic()
|
|
69
|
+
chart.setStage(logic)
|
|
70
|
+
|
|
71
|
+
# Check that entry node has recursive decorator
|
|
72
|
+
entry_node = chart._nodemap.get("entry")
|
|
73
|
+
self.assertIsNotNone(entry_node)
|
|
74
|
+
self.assertEqual(entry_node.decorator, "recursive")
|
|
75
|
+
|
|
76
|
+
# Check that process node does not have decorator
|
|
77
|
+
process_node = chart._nodemap.get("process")
|
|
78
|
+
self.assertIsNotNone(process_node)
|
|
79
|
+
self.assertIsNone(process_node.decorator)
|
|
80
|
+
|
|
81
|
+
def testDecoratorColorApplied(self):
|
|
82
|
+
"""Test that decorator color is actually applied."""
|
|
83
|
+
|
|
84
|
+
chart = QFlowChart()
|
|
85
|
+
stage = TestStage()
|
|
86
|
+
chart.setStage(stage)
|
|
87
|
+
|
|
88
|
+
entry_node = chart._nodemap.get("entry")
|
|
89
|
+
self.assertIsNotNone(entry_node)
|
|
90
|
+
|
|
91
|
+
# Get decorator color
|
|
92
|
+
color = entry_node._getDecoratorStrokeColor()
|
|
93
|
+
self.assertIsNotNone(color)
|
|
94
|
+
|
|
95
|
+
# Should be violet for recursive
|
|
96
|
+
self.assertEqual(color.red(), 238)
|
|
97
|
+
self.assertEqual(color.green(), 130)
|
|
98
|
+
self.assertEqual(color.blue(), 238)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
unittest.main()
|