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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Tests for Stage class"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from chartflow.fsm.stage import Stage, _NamedNode
|
|
5
|
+
from chartflow.fsm.meta import clear_prototypes, StageMachineMeta
|
|
6
|
+
from chartflow.fsm.decorators import recursive, behavior, sequence, selector, parallel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def reset_state():
|
|
11
|
+
"""Reset state before each test"""
|
|
12
|
+
clear_prototypes()
|
|
13
|
+
StageMachineMeta.__ID_PROTOS__ = {}
|
|
14
|
+
yield
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestNamedNode:
|
|
18
|
+
"""Test _NamedNode base class"""
|
|
19
|
+
|
|
20
|
+
def test_parent_child_relationship(self):
|
|
21
|
+
"""Test parent-child relationships"""
|
|
22
|
+
parent = _NamedNode()
|
|
23
|
+
parent._name = "parent"
|
|
24
|
+
child = _NamedNode()
|
|
25
|
+
child._name = "child"
|
|
26
|
+
|
|
27
|
+
parent._add_child(child)
|
|
28
|
+
|
|
29
|
+
assert child.parent is parent
|
|
30
|
+
assert child in parent._children
|
|
31
|
+
|
|
32
|
+
def test_remove_child(self):
|
|
33
|
+
"""Test removing a child"""
|
|
34
|
+
parent = _NamedNode()
|
|
35
|
+
parent._name = "parent"
|
|
36
|
+
child = _NamedNode()
|
|
37
|
+
child._name = "child"
|
|
38
|
+
|
|
39
|
+
parent._add_child(child)
|
|
40
|
+
parent._remove_child(child)
|
|
41
|
+
|
|
42
|
+
assert child.parent is None
|
|
43
|
+
assert child not in parent._children
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestStageInit:
|
|
47
|
+
"""Test Stage initialization"""
|
|
48
|
+
|
|
49
|
+
def test_stage_requires_name(self):
|
|
50
|
+
"""Test that Stage requires NAME"""
|
|
51
|
+
|
|
52
|
+
class BadStage(Stage):
|
|
53
|
+
NAME = ""
|
|
54
|
+
|
|
55
|
+
with pytest.raises(ValueError):
|
|
56
|
+
BadStage()
|
|
57
|
+
|
|
58
|
+
def test_stage_rejects_digit_suffix_name(self):
|
|
59
|
+
"""Test that Stage rejects NAME ending with digit"""
|
|
60
|
+
|
|
61
|
+
with pytest.raises(ValueError):
|
|
62
|
+
class BadStage(Stage):
|
|
63
|
+
NAME = "BadStage1"
|
|
64
|
+
|
|
65
|
+
def test_stage_generates_unique_ids(self):
|
|
66
|
+
"""Test that Stage generates unique IDs"""
|
|
67
|
+
|
|
68
|
+
class TestStage(Stage):
|
|
69
|
+
NAME = "TestStage"
|
|
70
|
+
|
|
71
|
+
stage1 = TestStage()
|
|
72
|
+
stage2 = TestStage()
|
|
73
|
+
|
|
74
|
+
assert stage1.uid != stage2.uid
|
|
75
|
+
assert stage1.uid == 0
|
|
76
|
+
assert stage2.uid == 1
|
|
77
|
+
|
|
78
|
+
def test_stage_full_name(self):
|
|
79
|
+
"""Test that Stage generates full name with uid"""
|
|
80
|
+
|
|
81
|
+
class TestStage(Stage):
|
|
82
|
+
NAME = "TestStage"
|
|
83
|
+
|
|
84
|
+
stage = TestStage()
|
|
85
|
+
|
|
86
|
+
assert stage.name == "TestStage0"
|
|
87
|
+
assert stage._name == "TestStage0"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestStageStates:
|
|
91
|
+
"""Test state execution"""
|
|
92
|
+
|
|
93
|
+
def test_stage_executes_initial_state(self):
|
|
94
|
+
"""Test that stage executes initial state"""
|
|
95
|
+
|
|
96
|
+
class TestStage(Stage):
|
|
97
|
+
NAME = "TestStage"
|
|
98
|
+
|
|
99
|
+
def initial(self, inst):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
stage = TestStage()
|
|
103
|
+
stage._current = "initial"
|
|
104
|
+
|
|
105
|
+
result = stage.handleStage(log=False)
|
|
106
|
+
|
|
107
|
+
assert stage.current == "initial"
|
|
108
|
+
|
|
109
|
+
def test_stage_transitions_state(self):
|
|
110
|
+
"""Test that stage transitions on state return"""
|
|
111
|
+
|
|
112
|
+
class TestStage(Stage):
|
|
113
|
+
NAME = "TestStage"
|
|
114
|
+
|
|
115
|
+
def state_a(self, inst):
|
|
116
|
+
return "state_b"
|
|
117
|
+
|
|
118
|
+
def state_b(self, inst):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
stage = TestStage()
|
|
122
|
+
stage._current = "state_a"
|
|
123
|
+
|
|
124
|
+
stage.handleStage(log=False)
|
|
125
|
+
|
|
126
|
+
assert stage.current == "state_b"
|
|
127
|
+
|
|
128
|
+
def test_stage_recursive_execution(self):
|
|
129
|
+
"""Test recursive state execution"""
|
|
130
|
+
|
|
131
|
+
class TestStage(Stage):
|
|
132
|
+
NAME = "TestStage"
|
|
133
|
+
|
|
134
|
+
@recursive
|
|
135
|
+
def counter(self, inst):
|
|
136
|
+
if not hasattr(self, 'count'):
|
|
137
|
+
self.count = 0
|
|
138
|
+
self.count += 1
|
|
139
|
+
if self.count >= 3:
|
|
140
|
+
return "done"
|
|
141
|
+
return "counter"
|
|
142
|
+
|
|
143
|
+
def done(self, inst):
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
stage = TestStage()
|
|
147
|
+
stage._current = "counter"
|
|
148
|
+
|
|
149
|
+
stage.handleStage(log=False)
|
|
150
|
+
|
|
151
|
+
assert stage.count == 3
|
|
152
|
+
assert stage.current == "done"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestStageBehaviorTree:
|
|
156
|
+
"""Test behavior tree composites"""
|
|
157
|
+
|
|
158
|
+
def test_behavior_transition(self):
|
|
159
|
+
"""Test @behavior decorator"""
|
|
160
|
+
|
|
161
|
+
class TestStage(Stage):
|
|
162
|
+
NAME = "TestStage"
|
|
163
|
+
|
|
164
|
+
@behavior("second")
|
|
165
|
+
def first(self, inst):
|
|
166
|
+
return True # Success triggers behavior
|
|
167
|
+
|
|
168
|
+
def second(self, inst):
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
stage = TestStage()
|
|
172
|
+
stage._current = "first"
|
|
173
|
+
|
|
174
|
+
stage.handleStage(log=False)
|
|
175
|
+
|
|
176
|
+
assert stage.current == "second"
|
|
177
|
+
|
|
178
|
+
def test_sequence_execution(self):
|
|
179
|
+
"""Test @sequence decorator"""
|
|
180
|
+
|
|
181
|
+
class TestStage(Stage):
|
|
182
|
+
NAME = "TestStage"
|
|
183
|
+
|
|
184
|
+
@sequence("b", "c")
|
|
185
|
+
def a(self, inst):
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
def b(self, inst):
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
def c(self, inst):
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
stage = TestStage()
|
|
195
|
+
stage._current = "a"
|
|
196
|
+
|
|
197
|
+
# First call enters sequence
|
|
198
|
+
stage.handleStage(log=False)
|
|
199
|
+
# Should be at "b" now
|
|
200
|
+
assert stage.current == "b"
|
|
201
|
+
|
|
202
|
+
def test_selector_execution(self):
|
|
203
|
+
"""Test @selector decorator"""
|
|
204
|
+
|
|
205
|
+
class TestStage(Stage):
|
|
206
|
+
NAME = "TestStage"
|
|
207
|
+
|
|
208
|
+
@selector("b", "c")
|
|
209
|
+
def a(self, inst):
|
|
210
|
+
return False # Failure triggers selector
|
|
211
|
+
|
|
212
|
+
def b(self, inst):
|
|
213
|
+
return True # Success stops selector
|
|
214
|
+
|
|
215
|
+
def c(self, inst):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
stage = TestStage()
|
|
219
|
+
stage._current = "a"
|
|
220
|
+
|
|
221
|
+
stage.handleStage(log=False)
|
|
222
|
+
|
|
223
|
+
# After a fails, should try b
|
|
224
|
+
assert stage.current == "b"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestStageHooks:
|
|
228
|
+
"""Test lifecycle hooks"""
|
|
229
|
+
|
|
230
|
+
def test_onchanged_hook(self):
|
|
231
|
+
"""Test onChanged hook is called"""
|
|
232
|
+
|
|
233
|
+
class TestStage(Stage):
|
|
234
|
+
NAME = "TestStage"
|
|
235
|
+
|
|
236
|
+
def __init__(self):
|
|
237
|
+
super().__init__()
|
|
238
|
+
self.changes = []
|
|
239
|
+
|
|
240
|
+
def state_a(self, inst):
|
|
241
|
+
return "state_b"
|
|
242
|
+
|
|
243
|
+
def state_b(self, inst):
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def onChanged(self, src, dst, inst):
|
|
247
|
+
self.changes.append((src, dst))
|
|
248
|
+
|
|
249
|
+
stage = TestStage()
|
|
250
|
+
stage._current = "state_a"
|
|
251
|
+
|
|
252
|
+
stage.handleStage(log=False)
|
|
253
|
+
|
|
254
|
+
assert ("state_a", "state_b") in stage.changes
|
|
255
|
+
|
|
256
|
+
def test_onloading_hook(self):
|
|
257
|
+
"""Test onLoading hook is called"""
|
|
258
|
+
|
|
259
|
+
class TestStage(Stage):
|
|
260
|
+
NAME = "TestStage"
|
|
261
|
+
|
|
262
|
+
def __init__(self):
|
|
263
|
+
self.loaded = False
|
|
264
|
+
super().__init__()
|
|
265
|
+
|
|
266
|
+
def onLoading(self, it):
|
|
267
|
+
self.loaded = True
|
|
268
|
+
|
|
269
|
+
stage = TestStage()
|
|
270
|
+
|
|
271
|
+
assert stage.loaded
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TestStageProperties:
|
|
275
|
+
"""Test Stage properties"""
|
|
276
|
+
|
|
277
|
+
def test_inst_property(self):
|
|
278
|
+
"""Test inst property"""
|
|
279
|
+
|
|
280
|
+
class TestStage(Stage):
|
|
281
|
+
NAME = "TestStage"
|
|
282
|
+
|
|
283
|
+
mock_inst = object()
|
|
284
|
+
stage = TestStage(mock_inst)
|
|
285
|
+
|
|
286
|
+
assert stage.inst is mock_inst
|
|
287
|
+
|
|
288
|
+
def test_current_property(self):
|
|
289
|
+
"""Test current property"""
|
|
290
|
+
|
|
291
|
+
class TestStage(Stage):
|
|
292
|
+
NAME = "TestStage"
|
|
293
|
+
|
|
294
|
+
def my_state(self, inst):
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
stage = TestStage()
|
|
298
|
+
stage._current = "my_state"
|
|
299
|
+
|
|
300
|
+
assert stage.current == "my_state"
|
|
301
|
+
|
|
302
|
+
def test_current_setter_validates(self):
|
|
303
|
+
"""Test current setter validates state"""
|
|
304
|
+
|
|
305
|
+
class TestStage(Stage):
|
|
306
|
+
NAME = "TestStage"
|
|
307
|
+
|
|
308
|
+
def valid_state(self, inst):
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
stage = TestStage()
|
|
312
|
+
|
|
313
|
+
with pytest.raises(ValueError):
|
|
314
|
+
stage.current = "invalid_state"
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Test behavior tree events - verify CHANGED events are triggered for child nodes"""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
sys.path.insert(0, 'F:\\Python\\Python314\\Lib\\site-packages')
|
|
7
|
+
|
|
8
|
+
from chartflow.fsm import Stage
|
|
9
|
+
from chartflow.fsm.decorators import parallel, sequence, selector
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestStage(Stage):
|
|
13
|
+
"""Test stage with behavior tree - captures events synchronously"""
|
|
14
|
+
NAME = "TestStage"
|
|
15
|
+
LOG = True # Enable to see state transitions in console
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.events = []
|
|
20
|
+
|
|
21
|
+
def onChanged(self, src, dst, inst):
|
|
22
|
+
"""Synchronously capture state changes"""
|
|
23
|
+
super().onChanged(src, dst, inst)
|
|
24
|
+
self.events.append(("CHANGED", f"{src} -> {dst}"))
|
|
25
|
+
|
|
26
|
+
def entry(self, inst):
|
|
27
|
+
return "root"
|
|
28
|
+
|
|
29
|
+
@parallel("a", "b")
|
|
30
|
+
def root(self, inst):
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
def a(self, inst):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
@sequence("c", "d")
|
|
37
|
+
def b(self, inst):
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
def c(self, inst):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def d(self, inst):
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_parallel_behavior():
|
|
48
|
+
"""Test parallel behavior tree triggers CHANGED for children"""
|
|
49
|
+
print("="*60)
|
|
50
|
+
print("TEST: Parallel behavior - CHANGED events for children")
|
|
51
|
+
print("="*60)
|
|
52
|
+
|
|
53
|
+
stage = TestStage()
|
|
54
|
+
stage.events.clear() # Clear init events
|
|
55
|
+
|
|
56
|
+
print("\n--- Tick 1: entry -> root ---")
|
|
57
|
+
stage.handleStage(log=True)
|
|
58
|
+
print(f"Current state: {stage.current}")
|
|
59
|
+
print(f"Events: {stage.events}")
|
|
60
|
+
|
|
61
|
+
print("\n--- Tick 2: root parallel execution ---")
|
|
62
|
+
stage.events.clear()
|
|
63
|
+
stage.handleStage(log=True)
|
|
64
|
+
print(f"Current state: {stage.current}")
|
|
65
|
+
print(f"Events: {stage.events}")
|
|
66
|
+
|
|
67
|
+
# Check for child transitions
|
|
68
|
+
changed_events = [e for e in stage.events if e[0] == "CHANGED"]
|
|
69
|
+
child_transitions = [e for e in changed_events if "->" in e[1]]
|
|
70
|
+
|
|
71
|
+
print(f"\nChild transitions captured: {len(child_transitions)}")
|
|
72
|
+
for t in child_transitions:
|
|
73
|
+
print(f" {t[1]}")
|
|
74
|
+
|
|
75
|
+
# We expect to see transitions like:
|
|
76
|
+
# - root -> a
|
|
77
|
+
# - a -> b (or similar, depending on execution order)
|
|
78
|
+
success = len(child_transitions) > 0
|
|
79
|
+
|
|
80
|
+
# Check specific transitions
|
|
81
|
+
has_root_to_child = any("root ->" in e[1] for e in child_transitions)
|
|
82
|
+
|
|
83
|
+
if has_root_to_child:
|
|
84
|
+
print("PASS: Found CHANGED event from root to child")
|
|
85
|
+
else:
|
|
86
|
+
print("FAIL: No CHANGED event from root to child")
|
|
87
|
+
|
|
88
|
+
return success and has_root_to_child
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_sequence_behavior():
|
|
92
|
+
"""Test sequence behavior tree"""
|
|
93
|
+
print("\n" + "="*60)
|
|
94
|
+
print("TEST: Sequence behavior - step by step execution")
|
|
95
|
+
print("="*60)
|
|
96
|
+
|
|
97
|
+
class SeqStage(Stage):
|
|
98
|
+
NAME = "SeqStage"
|
|
99
|
+
LOG = True
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.events = []
|
|
104
|
+
|
|
105
|
+
def onChanged(self, src, dst, inst):
|
|
106
|
+
self.events.append(("CHANGED", f"{src} -> {dst}"))
|
|
107
|
+
|
|
108
|
+
def entry(self, inst):
|
|
109
|
+
return "seq"
|
|
110
|
+
|
|
111
|
+
@sequence("s1", "s2", "s3")
|
|
112
|
+
def seq(self, inst):
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def s1(self, inst):
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def s2(self, inst):
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def s3(self, inst):
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
stage = SeqStage()
|
|
125
|
+
stage.events.clear()
|
|
126
|
+
|
|
127
|
+
print("\n--- Tick 1: entry -> seq ---")
|
|
128
|
+
stage.handleStage(log=True)
|
|
129
|
+
print(f"Events: {stage.events}")
|
|
130
|
+
|
|
131
|
+
all_events = []
|
|
132
|
+
all_events.extend(stage.events)
|
|
133
|
+
|
|
134
|
+
print("\n--- Tick 2: seq starts ---")
|
|
135
|
+
stage.events.clear()
|
|
136
|
+
stage.handleStage(log=True)
|
|
137
|
+
print(f"Events: {stage.events}")
|
|
138
|
+
all_events.extend(stage.events)
|
|
139
|
+
|
|
140
|
+
# Sequence pauses after each child, so run more ticks
|
|
141
|
+
for i in range(5):
|
|
142
|
+
print(f"\n--- Tick {i+3} ---")
|
|
143
|
+
stage.events.clear()
|
|
144
|
+
stage.handleStage(log=True)
|
|
145
|
+
if stage.events:
|
|
146
|
+
print(f"Events: {stage.events}")
|
|
147
|
+
all_events.extend(stage.events)
|
|
148
|
+
if not stage.events:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
print(f"\n=== All CHANGED events ===")
|
|
152
|
+
changed = [e for e in all_events if e[0] == "CHANGED"]
|
|
153
|
+
for c in changed:
|
|
154
|
+
print(f" {c[1]}")
|
|
155
|
+
|
|
156
|
+
# Check for child transitions in sequence
|
|
157
|
+
child_changes = [e for e in changed if "seq" in e[1] or "s1" in e[1] or "s2" in e[1] or "s3" in e[1]]
|
|
158
|
+
|
|
159
|
+
# We expect at least: entry -> seq, and some transitions involving s1, s2, s3
|
|
160
|
+
success = len(child_changes) >= 2
|
|
161
|
+
|
|
162
|
+
if success:
|
|
163
|
+
print(f"PASS: Found {len(child_changes)} relevant CHANGED events")
|
|
164
|
+
else:
|
|
165
|
+
print(f"FAIL: Only found {len(child_changes)} relevant CHANGED events")
|
|
166
|
+
|
|
167
|
+
return success
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_selector_behavior():
|
|
171
|
+
"""Test selector behavior tree"""
|
|
172
|
+
print("\n" + "="*60)
|
|
173
|
+
print("TEST: Selector behavior - first success stops")
|
|
174
|
+
print("="*60)
|
|
175
|
+
|
|
176
|
+
class SelStage(Stage):
|
|
177
|
+
NAME = "SelStage"
|
|
178
|
+
LOG = True
|
|
179
|
+
|
|
180
|
+
def __init__(self):
|
|
181
|
+
super().__init__()
|
|
182
|
+
self.events = []
|
|
183
|
+
|
|
184
|
+
def onChanged(self, src, dst, inst):
|
|
185
|
+
self.events.append(("CHANGED", f"{src} -> {dst}"))
|
|
186
|
+
|
|
187
|
+
def entry(self, inst):
|
|
188
|
+
return "sel"
|
|
189
|
+
|
|
190
|
+
@selector("f1", "f2", "f3")
|
|
191
|
+
def sel(self, inst):
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
def f1(self, inst):
|
|
195
|
+
return False # Fail, try next
|
|
196
|
+
|
|
197
|
+
def f2(self, inst):
|
|
198
|
+
return True # Success, stop
|
|
199
|
+
|
|
200
|
+
def f3(self, inst):
|
|
201
|
+
return True # Won't execute
|
|
202
|
+
|
|
203
|
+
stage = SelStage()
|
|
204
|
+
stage.events.clear()
|
|
205
|
+
|
|
206
|
+
print("\n--- Tick 1: entry -> sel ---")
|
|
207
|
+
stage.handleStage(log=True)
|
|
208
|
+
|
|
209
|
+
all_events = list(stage.events)
|
|
210
|
+
|
|
211
|
+
print("\n--- Tick 2: selector execution ---")
|
|
212
|
+
stage.events.clear()
|
|
213
|
+
stage.handleStage(log=True)
|
|
214
|
+
all_events.extend(stage.events)
|
|
215
|
+
|
|
216
|
+
# Run more ticks
|
|
217
|
+
for i in range(3):
|
|
218
|
+
print(f"\n--- Tick {i+3} ---")
|
|
219
|
+
stage.events.clear()
|
|
220
|
+
stage.handleStage(log=True)
|
|
221
|
+
if stage.events:
|
|
222
|
+
print(f"Events: {stage.events}")
|
|
223
|
+
all_events.extend(stage.events)
|
|
224
|
+
|
|
225
|
+
print(f"\n=== All CHANGED events ===")
|
|
226
|
+
changed = [e for e in all_events if e[0] == "CHANGED"]
|
|
227
|
+
for c in changed:
|
|
228
|
+
print(f" {c[1]}")
|
|
229
|
+
|
|
230
|
+
# Selector should have transitioned to f1, then to f2 (when f1 fails)
|
|
231
|
+
has_child_transition = any("sel" in e[1] or "f1" in e[1] or "f2" in e[1] for e in changed)
|
|
232
|
+
|
|
233
|
+
if has_child_transition:
|
|
234
|
+
print("PASS: Selector has child transitions")
|
|
235
|
+
return True
|
|
236
|
+
else:
|
|
237
|
+
print("FAIL: No child transitions in selector")
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
results = []
|
|
243
|
+
|
|
244
|
+
results.append(("Parallel", test_parallel_behavior()))
|
|
245
|
+
results.append(("Sequence", test_sequence_behavior()))
|
|
246
|
+
results.append(("Selector", test_selector_behavior()))
|
|
247
|
+
|
|
248
|
+
print("\n" + "="*60)
|
|
249
|
+
print("FINAL RESULTS")
|
|
250
|
+
print("="*60)
|
|
251
|
+
all_pass = True
|
|
252
|
+
for name, passed in results:
|
|
253
|
+
status = "PASS" if passed else "FAIL"
|
|
254
|
+
print(f" {name}: {status}")
|
|
255
|
+
if not passed:
|
|
256
|
+
all_pass = False
|
|
257
|
+
|
|
258
|
+
if all_pass:
|
|
259
|
+
print("\nALL TESTS PASSED!")
|
|
260
|
+
sys.exit(0)
|
|
261
|
+
else:
|
|
262
|
+
print("\nSOME TESTS FAILED")
|
|
263
|
+
sys.exit(1)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Test nested behavior tree in parallel"""
|
|
2
|
+
from chartflow.fsm import Stage
|
|
3
|
+
from chartflow.fsm.decorators import sequence, parallel
|
|
4
|
+
|
|
5
|
+
# Use lowercase method names as states (Stage auto-discovers them)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DemoLogic(Stage):
|
|
9
|
+
NAME = "DemoLogic"
|
|
10
|
+
LOG = True
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.call_order = []
|
|
15
|
+
|
|
16
|
+
def entry(self, inst):
|
|
17
|
+
self.call_order.append("entry")
|
|
18
|
+
return "root"
|
|
19
|
+
|
|
20
|
+
@parallel("a", "b")
|
|
21
|
+
def root(self, inst):
|
|
22
|
+
self.call_order.append("root")
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
def a(self, inst):
|
|
26
|
+
self.call_order.append("a")
|
|
27
|
+
return False # FAIL
|
|
28
|
+
|
|
29
|
+
@sequence("c", "d")
|
|
30
|
+
def b(self, inst):
|
|
31
|
+
self.call_order.append("b")
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
def c(self, inst):
|
|
35
|
+
self.call_order.append("c")
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
def d(self, inst):
|
|
39
|
+
self.call_order.append("d")
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
def c(self, inst):
|
|
43
|
+
self.call_order.append("c")
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def d(self, inst):
|
|
47
|
+
self.call_order.append("d")
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_nested_parallel():
|
|
52
|
+
print("\n" + "="*60)
|
|
53
|
+
print("TEST: Nested sequence in parallel")
|
|
54
|
+
print("="*60)
|
|
55
|
+
|
|
56
|
+
stage = DemoLogic()
|
|
57
|
+
|
|
58
|
+
# Tick 1: entry -> root
|
|
59
|
+
print("\n--- Tick 1 ---")
|
|
60
|
+
stage.handleStage(log=True)
|
|
61
|
+
|
|
62
|
+
# Tick 2: parallel execution
|
|
63
|
+
print("\n--- Tick 2 ---")
|
|
64
|
+
stage.handleStage(log=True)
|
|
65
|
+
|
|
66
|
+
# Tick 3: continue parallel
|
|
67
|
+
print("\n--- Tick 3 ---")
|
|
68
|
+
stage.handleStage(log=True)
|
|
69
|
+
|
|
70
|
+
print(f"\nCall order: {stage.call_order}")
|
|
71
|
+
print(f"Current state: {stage.current_state}")
|
|
72
|
+
|
|
73
|
+
# Verify c and d were both called
|
|
74
|
+
assert "c" in stage.call_order, "c should be called"
|
|
75
|
+
assert "d" in stage.call_order, "d should be called"
|
|
76
|
+
print("PASS: Both c and d were called")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
test_nested_parallel()
|