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,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
测试状态方法的参数检查功能
|
|
3
|
+
|
|
4
|
+
验证内容:
|
|
5
|
+
1. 方法接受 inst 参数 - 应该传递 inst
|
|
6
|
+
2. 方法不接受任何参数 - 不应该传递 inst
|
|
7
|
+
3. 方法接受 *args - 应该传递 inst
|
|
8
|
+
4. 方法接受 **kwargs - 应该传递 inst
|
|
9
|
+
5. 缓存机制 - __runtime_paramable__ 应该被设置
|
|
10
|
+
6. 集成测试 - Stage 子类的各种状态方法
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from chartflow.fsm.stage import (
|
|
15
|
+
Stage,
|
|
16
|
+
_checkMethodAcceptsParam,
|
|
17
|
+
_invokeStateMethod,
|
|
18
|
+
_RUNTIME_PARAMABLE_CACHE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestCheckMethodAcceptsParam:
|
|
23
|
+
"""测试 _checkMethodAcceptsParam 函数"""
|
|
24
|
+
|
|
25
|
+
def setup_method(self):
|
|
26
|
+
"""每个测试前清空缓存"""
|
|
27
|
+
_RUNTIME_PARAMABLE_CACHE.clear()
|
|
28
|
+
|
|
29
|
+
def test_no_params(self):
|
|
30
|
+
"""测试不接受参数的方法"""
|
|
31
|
+
def noParams(self):
|
|
32
|
+
return "ok"
|
|
33
|
+
|
|
34
|
+
result = _checkMethodAcceptsParam(noParams)
|
|
35
|
+
assert result is False
|
|
36
|
+
assert noParams.__runtime_paramable__ is False
|
|
37
|
+
|
|
38
|
+
def test_with_self_and_param(self):
|
|
39
|
+
"""测试有self和一个参数的方法"""
|
|
40
|
+
class MyClass:
|
|
41
|
+
def method(self, inst):
|
|
42
|
+
return inst
|
|
43
|
+
|
|
44
|
+
# 测试未绑定方法
|
|
45
|
+
result = _checkMethodAcceptsParam(MyClass.method)
|
|
46
|
+
assert result is True
|
|
47
|
+
assert MyClass.method.__runtime_paramable__ is True
|
|
48
|
+
|
|
49
|
+
def test_with_self_no_other_params(self):
|
|
50
|
+
"""测试只有self参数的方法"""
|
|
51
|
+
class MyClass:
|
|
52
|
+
def method(self):
|
|
53
|
+
return "ok"
|
|
54
|
+
|
|
55
|
+
# 测试未绑定方法
|
|
56
|
+
result = _checkMethodAcceptsParam(MyClass.method)
|
|
57
|
+
assert result is False
|
|
58
|
+
assert MyClass.method.__runtime_paramable__ is False
|
|
59
|
+
|
|
60
|
+
def test_with_args(self):
|
|
61
|
+
"""测试接受 *args 的方法"""
|
|
62
|
+
def withArgs(self, *args):
|
|
63
|
+
return args
|
|
64
|
+
|
|
65
|
+
result = _checkMethodAcceptsParam(withArgs)
|
|
66
|
+
assert result is True
|
|
67
|
+
assert withArgs.__runtime_paramable__ is True
|
|
68
|
+
|
|
69
|
+
def test_with_kwargs(self):
|
|
70
|
+
"""测试接受 **kwargs 的方法"""
|
|
71
|
+
def withKwargs(self, **kwargs):
|
|
72
|
+
return kwargs
|
|
73
|
+
|
|
74
|
+
result = _checkMethodAcceptsParam(withKwargs)
|
|
75
|
+
assert result is True
|
|
76
|
+
assert withKwargs.__runtime_paramable__ is True
|
|
77
|
+
|
|
78
|
+
def test_with_args_and_kwargs(self):
|
|
79
|
+
"""测试接受 *args, **kwargs 的方法"""
|
|
80
|
+
def withBoth(self, *args, **kwargs):
|
|
81
|
+
return (args, kwargs)
|
|
82
|
+
|
|
83
|
+
result = _checkMethodAcceptsParam(withBoth)
|
|
84
|
+
assert result is True
|
|
85
|
+
assert withBoth.__runtime_paramable__ is True
|
|
86
|
+
|
|
87
|
+
def test_with_default_param(self):
|
|
88
|
+
"""测试有默认参数的方法"""
|
|
89
|
+
def withDefault(self, inst=None):
|
|
90
|
+
return inst
|
|
91
|
+
|
|
92
|
+
result = _checkMethodAcceptsParam(withDefault)
|
|
93
|
+
assert result is True
|
|
94
|
+
assert withDefault.__runtime_paramable__ is True
|
|
95
|
+
|
|
96
|
+
def test_cache_is_used(self):
|
|
97
|
+
"""测试缓存被使用"""
|
|
98
|
+
def testMethod(self, inst):
|
|
99
|
+
return inst
|
|
100
|
+
|
|
101
|
+
# 第一次调用
|
|
102
|
+
result1 = _checkMethodAcceptsParam(testMethod)
|
|
103
|
+
assert result1 is True
|
|
104
|
+
assert testMethod.__runtime_paramable__ is True
|
|
105
|
+
|
|
106
|
+
# 修改缓存值
|
|
107
|
+
testMethod.__runtime_paramable__ = False
|
|
108
|
+
|
|
109
|
+
# 第二次调用应该使用缓存
|
|
110
|
+
result2 = _checkMethodAcceptsParam(testMethod)
|
|
111
|
+
assert result2 is False # 使用缓存值
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestInvokeStateMethod:
|
|
115
|
+
"""测试 _invokeStateMethod 函数"""
|
|
116
|
+
|
|
117
|
+
def test_invoke_with_param(self):
|
|
118
|
+
"""测试调用接受参数的方法"""
|
|
119
|
+
received = []
|
|
120
|
+
|
|
121
|
+
class TestClass:
|
|
122
|
+
def withParam(self, inst):
|
|
123
|
+
received.append(inst)
|
|
124
|
+
return "result"
|
|
125
|
+
|
|
126
|
+
obj = TestClass()
|
|
127
|
+
# 使用 bound method(self 已自动绑定)
|
|
128
|
+
result = _invokeStateMethod(obj.withParam, "my_inst")
|
|
129
|
+
assert result == "result"
|
|
130
|
+
assert received == ["my_inst"]
|
|
131
|
+
|
|
132
|
+
def test_invoke_without_param(self):
|
|
133
|
+
"""测试调用不接受参数的方法"""
|
|
134
|
+
received = []
|
|
135
|
+
|
|
136
|
+
class TestClass:
|
|
137
|
+
def noParams(self):
|
|
138
|
+
received.append("called")
|
|
139
|
+
return "result"
|
|
140
|
+
|
|
141
|
+
obj = TestClass()
|
|
142
|
+
# 使用 bound method(self 已自动绑定)
|
|
143
|
+
result = _invokeStateMethod(obj.noParams, "my_inst")
|
|
144
|
+
assert result == "result"
|
|
145
|
+
assert received == ["called"]
|
|
146
|
+
|
|
147
|
+
def test_invoke_with_args(self):
|
|
148
|
+
"""测试调用接受 *args 的方法"""
|
|
149
|
+
received = []
|
|
150
|
+
|
|
151
|
+
class TestClass:
|
|
152
|
+
def withArgs(self, *args):
|
|
153
|
+
received.extend(args)
|
|
154
|
+
return "result"
|
|
155
|
+
|
|
156
|
+
obj = TestClass()
|
|
157
|
+
# 使用 bound method(self 已自动绑定)
|
|
158
|
+
result = _invokeStateMethod(obj.withArgs, "my_inst")
|
|
159
|
+
assert result == "result"
|
|
160
|
+
assert "my_inst" in received
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TestStageIntegration:
|
|
164
|
+
"""集成测试 - 测试 Stage 类的各种状态方法"""
|
|
165
|
+
|
|
166
|
+
def setup_method(self):
|
|
167
|
+
"""每个测试前清空缓存"""
|
|
168
|
+
_RUNTIME_PARAMABLE_CACHE.clear()
|
|
169
|
+
|
|
170
|
+
def test_state_with_inst_param(self):
|
|
171
|
+
"""测试接受 inst 参数的状态方法"""
|
|
172
|
+
received = []
|
|
173
|
+
|
|
174
|
+
class MyInst:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
class MyStage(Stage):
|
|
178
|
+
NAME = "MyStage"
|
|
179
|
+
|
|
180
|
+
def start(self, inst):
|
|
181
|
+
received.append(("start", inst))
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
my_inst = MyInst()
|
|
185
|
+
stage = MyStage(my_inst)
|
|
186
|
+
stage.handleStage(log=False)
|
|
187
|
+
|
|
188
|
+
assert len(received) == 1
|
|
189
|
+
assert received[0] == ("start", my_inst)
|
|
190
|
+
|
|
191
|
+
def test_state_without_params(self):
|
|
192
|
+
"""测试不接受参数的状态方法"""
|
|
193
|
+
received = []
|
|
194
|
+
|
|
195
|
+
class MyInst:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
class MyStage(Stage):
|
|
199
|
+
NAME = "MyStage"
|
|
200
|
+
|
|
201
|
+
def start(self):
|
|
202
|
+
received.append("start_called")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
my_inst = MyInst()
|
|
206
|
+
stage = MyStage(my_inst)
|
|
207
|
+
stage.handleStage(log=False)
|
|
208
|
+
|
|
209
|
+
assert len(received) == 1
|
|
210
|
+
assert received[0] == "start_called"
|
|
211
|
+
|
|
212
|
+
def test_state_with_args(self):
|
|
213
|
+
"""测试接受 *args 的状态方法"""
|
|
214
|
+
received = []
|
|
215
|
+
|
|
216
|
+
class MyInst:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
class MyStage(Stage):
|
|
220
|
+
NAME = "MyStage"
|
|
221
|
+
|
|
222
|
+
def start(self, *args):
|
|
223
|
+
received.extend(args)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
my_inst = MyInst()
|
|
227
|
+
stage = MyStage(my_inst)
|
|
228
|
+
stage.handleStage(log=False)
|
|
229
|
+
|
|
230
|
+
assert my_inst in received
|
|
231
|
+
|
|
232
|
+
def test_mixed_states(self):
|
|
233
|
+
"""测试混合参数类型的状态方法"""
|
|
234
|
+
received = []
|
|
235
|
+
|
|
236
|
+
class MyInst:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
class MyStage(Stage):
|
|
240
|
+
NAME = "MyStage"
|
|
241
|
+
|
|
242
|
+
def start(self, inst):
|
|
243
|
+
received.append(("start", inst))
|
|
244
|
+
return "process"
|
|
245
|
+
|
|
246
|
+
def process(self):
|
|
247
|
+
received.append("process_no_inst")
|
|
248
|
+
return "end"
|
|
249
|
+
|
|
250
|
+
def end(self, inst):
|
|
251
|
+
received.append(("end", inst))
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
my_inst = MyInst()
|
|
255
|
+
stage = MyStage(my_inst)
|
|
256
|
+
|
|
257
|
+
# 执行多个 tick
|
|
258
|
+
stage.handleStage(log=False) # start -> process
|
|
259
|
+
stage.handleStage(log=False) # process -> end
|
|
260
|
+
stage.handleStage(log=False) # end
|
|
261
|
+
|
|
262
|
+
assert len(received) == 3
|
|
263
|
+
assert received[0] == ("start", my_inst)
|
|
264
|
+
assert received[1] == "process_no_inst"
|
|
265
|
+
assert received[2] == ("end", my_inst)
|
|
266
|
+
|
|
267
|
+
def test_runtime_paramable_cache_set_on_stage(self):
|
|
268
|
+
"""测试 __runtime_paramable__ 缓存被设置在 Stage 方法上"""
|
|
269
|
+
|
|
270
|
+
class MyStage(Stage):
|
|
271
|
+
NAME = "MyStage"
|
|
272
|
+
|
|
273
|
+
def start(self, inst):
|
|
274
|
+
return "noInst" # 转移到 noInst
|
|
275
|
+
|
|
276
|
+
def noInst(self):
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
# 创建实例前检查
|
|
280
|
+
assert not hasattr(MyStage.start, '__runtime_paramable__')
|
|
281
|
+
assert not hasattr(MyStage.noInst, '__runtime_paramable__')
|
|
282
|
+
|
|
283
|
+
# 创建并执行(使用 None 作为 inst 避免元类问题)
|
|
284
|
+
stage = MyStage(None)
|
|
285
|
+
stage.handleStage(log=False) # 执行 start,然后转移到 noInst
|
|
286
|
+
stage.handleStage(log=False) # 执行 noInst
|
|
287
|
+
|
|
288
|
+
# 执行后检查:bound method 应该有缓存属性
|
|
289
|
+
# 从实例上获取的 bound method 可以设置属性
|
|
290
|
+
start_method = stage.start # 获取 bound method
|
|
291
|
+
noInst_method = stage.noInst
|
|
292
|
+
|
|
293
|
+
# bound method 可能无法直接设置 __runtime_paramable__
|
|
294
|
+
# 但模块级缓存应该存在
|
|
295
|
+
# 缓存键是 (id(obj), id(func)) 对于 bound method
|
|
296
|
+
start_cache_key = (id(stage), id(start_method.__func__))
|
|
297
|
+
noInst_cache_key = (id(stage), id(noInst_method.__func__))
|
|
298
|
+
|
|
299
|
+
assert start_cache_key in _RUNTIME_PARAMABLE_CACHE or hasattr(start_method, '__runtime_paramable__')
|
|
300
|
+
assert noInst_cache_key in _RUNTIME_PARAMABLE_CACHE or hasattr(noInst_method, '__runtime_paramable__')
|
|
301
|
+
|
|
302
|
+
# 验证缓存值
|
|
303
|
+
start_result = _checkMethodAcceptsParam(start_method)
|
|
304
|
+
noInst_result = _checkMethodAcceptsParam(noInst_method)
|
|
305
|
+
assert start_result is True
|
|
306
|
+
assert noInst_result is False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TestEdgeCases:
|
|
310
|
+
"""测试边界情况"""
|
|
311
|
+
|
|
312
|
+
def setup_method(self):
|
|
313
|
+
"""每个测试前清空缓存"""
|
|
314
|
+
_RUNTIME_PARAMABLE_CACHE.clear()
|
|
315
|
+
|
|
316
|
+
def test_method_with_multiple_params(self):
|
|
317
|
+
"""测试接受多个参数的方法"""
|
|
318
|
+
def multiParam(self, a, b, c):
|
|
319
|
+
return (a, b, c)
|
|
320
|
+
|
|
321
|
+
result = _checkMethodAcceptsParam(multiParam)
|
|
322
|
+
assert result is True
|
|
323
|
+
assert multiParam.__runtime_paramable__ is True
|
|
324
|
+
|
|
325
|
+
def test_method_with_only_defaults(self):
|
|
326
|
+
"""测试所有参数都有默认值的方法"""
|
|
327
|
+
def allDefaults(self, a=1, b=2):
|
|
328
|
+
return (a, b)
|
|
329
|
+
|
|
330
|
+
result = _checkMethodAcceptsParam(allDefaults)
|
|
331
|
+
assert result is True
|
|
332
|
+
assert allDefaults.__runtime_paramable__ is True
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Tests for Scheduler"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from chartflow.fsm.scheduler import Scheduler
|
|
5
|
+
from chartflow.fsm.meta import StageMachineMeta, clear_prototypes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def reset_scheduler():
|
|
10
|
+
"""Reset scheduler before each test"""
|
|
11
|
+
Scheduler._instance = None
|
|
12
|
+
clear_prototypes()
|
|
13
|
+
yield
|
|
14
|
+
Scheduler._instance = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestSchedulerSingleton:
|
|
18
|
+
"""Test scheduler singleton behavior"""
|
|
19
|
+
|
|
20
|
+
def test_singleton(self):
|
|
21
|
+
"""Test that Scheduler is a singleton"""
|
|
22
|
+
s1 = Scheduler()
|
|
23
|
+
s2 = Scheduler()
|
|
24
|
+
assert s1 is s2
|
|
25
|
+
|
|
26
|
+
def test_same_instance_across_calls(self):
|
|
27
|
+
"""Test same instance returned across multiple calls"""
|
|
28
|
+
instances = [Scheduler() for _ in range(5)]
|
|
29
|
+
assert all(i is instances[0] for i in instances)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestSchedulerLogin:
|
|
33
|
+
"""Test class registration via login()"""
|
|
34
|
+
|
|
35
|
+
def test_login_registers_class(self):
|
|
36
|
+
"""Test that login registers a class"""
|
|
37
|
+
scheduler = Scheduler()
|
|
38
|
+
|
|
39
|
+
class TestLogic(metaclass=StageMachineMeta):
|
|
40
|
+
NAME = "TestLogic"
|
|
41
|
+
|
|
42
|
+
def state_a(self, inst):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
scheduler.login(TestLogic)
|
|
46
|
+
assert "TestLogic" in scheduler.prototypes
|
|
47
|
+
assert scheduler.prototypes["TestLogic"] is TestLogic
|
|
48
|
+
|
|
49
|
+
def test_login_validates_name(self):
|
|
50
|
+
"""Test that login validates NAME attribute"""
|
|
51
|
+
scheduler = Scheduler()
|
|
52
|
+
|
|
53
|
+
class BadLogic:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
with pytest.raises(ValueError):
|
|
57
|
+
scheduler.login(BadLogic)
|
|
58
|
+
|
|
59
|
+
def test_login_rejects_digit_suffix(self):
|
|
60
|
+
"""Test that login rejects NAME ending with digit"""
|
|
61
|
+
scheduler = Scheduler()
|
|
62
|
+
|
|
63
|
+
class BadLogic:
|
|
64
|
+
NAME = "BadLogic1"
|
|
65
|
+
|
|
66
|
+
with pytest.raises(ValueError):
|
|
67
|
+
scheduler.login(BadLogic)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MockLogic:
|
|
71
|
+
"""Mock logic class for testing"""
|
|
72
|
+
NAME = "MockLogic"
|
|
73
|
+
PRIORITY = 0
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self._alive = True
|
|
77
|
+
self._enabled = True
|
|
78
|
+
self._isEntered = False
|
|
79
|
+
self.check_called = False
|
|
80
|
+
self.enter_called = False
|
|
81
|
+
self.step_called = False
|
|
82
|
+
self.exit_called = False
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def alive(self):
|
|
86
|
+
return self._alive
|
|
87
|
+
|
|
88
|
+
@alive.setter
|
|
89
|
+
def alive(self, value):
|
|
90
|
+
self._alive = value
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def enabled(self):
|
|
94
|
+
return self._enabled
|
|
95
|
+
|
|
96
|
+
@enabled.setter
|
|
97
|
+
def enabled(self, value):
|
|
98
|
+
self._enabled = value
|
|
99
|
+
|
|
100
|
+
def _check(self, is_seek):
|
|
101
|
+
self.check_called = True
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def _enter(self):
|
|
105
|
+
self.enter_called = True
|
|
106
|
+
self._isEntered = True
|
|
107
|
+
|
|
108
|
+
def _step(self):
|
|
109
|
+
self.step_called = True
|
|
110
|
+
|
|
111
|
+
def _exit(self):
|
|
112
|
+
self.exit_called = True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestSchedulerAdd:
|
|
116
|
+
"""Test instance registration via add()"""
|
|
117
|
+
|
|
118
|
+
def test_add_instance_directly(self):
|
|
119
|
+
"""Test adding an instance directly"""
|
|
120
|
+
scheduler = Scheduler()
|
|
121
|
+
mock = MockLogic()
|
|
122
|
+
|
|
123
|
+
result = scheduler.add(mock)
|
|
124
|
+
|
|
125
|
+
assert result is mock
|
|
126
|
+
assert mock in scheduler.updates or mock in scheduler.waits
|
|
127
|
+
|
|
128
|
+
def test_add_by_name(self):
|
|
129
|
+
"""Test adding by registered name"""
|
|
130
|
+
scheduler = Scheduler()
|
|
131
|
+
|
|
132
|
+
class TestLogic(metaclass=StageMachineMeta):
|
|
133
|
+
NAME = "TestLogic"
|
|
134
|
+
PRIORITY = 0
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self._alive = True
|
|
138
|
+
self._enabled = True
|
|
139
|
+
self._isEntered = False
|
|
140
|
+
|
|
141
|
+
def _check(self, is_seek):
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
def _enter(self):
|
|
145
|
+
self._isEntered = True
|
|
146
|
+
|
|
147
|
+
def _step(self):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def _exit(self):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
scheduler.login(TestLogic)
|
|
154
|
+
instance = scheduler.add("TestLogic")
|
|
155
|
+
|
|
156
|
+
assert isinstance(instance, TestLogic)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestSchedulerHandle:
|
|
160
|
+
"""Test the main handle() loop"""
|
|
161
|
+
|
|
162
|
+
def test_handle_calls_lifecycle(self):
|
|
163
|
+
"""Test that handle calls all lifecycle methods"""
|
|
164
|
+
scheduler = Scheduler()
|
|
165
|
+
mock = MockLogic()
|
|
166
|
+
scheduler.add(mock)
|
|
167
|
+
|
|
168
|
+
scheduler.handle()
|
|
169
|
+
|
|
170
|
+
assert mock.check_called
|
|
171
|
+
assert mock.enter_called
|
|
172
|
+
assert mock.step_called
|
|
173
|
+
|
|
174
|
+
def test_handle_removes_dead_instances(self):
|
|
175
|
+
"""Test that handle removes dead instances"""
|
|
176
|
+
scheduler = Scheduler()
|
|
177
|
+
mock = MockLogic()
|
|
178
|
+
scheduler.add(mock)
|
|
179
|
+
|
|
180
|
+
scheduler.handle()
|
|
181
|
+
assert mock.enter_called # First tick enters
|
|
182
|
+
|
|
183
|
+
mock._alive = False
|
|
184
|
+
scheduler.handle()
|
|
185
|
+
|
|
186
|
+
assert mock.exit_called
|
|
187
|
+
assert mock not in scheduler.updates
|
|
188
|
+
|
|
189
|
+
def test_handle_skips_disabled_instances(self):
|
|
190
|
+
"""Test that handle skips disabled instances"""
|
|
191
|
+
scheduler = Scheduler()
|
|
192
|
+
mock = MockLogic()
|
|
193
|
+
mock._enabled = False
|
|
194
|
+
scheduler.add(mock)
|
|
195
|
+
|
|
196
|
+
scheduler.handle()
|
|
197
|
+
|
|
198
|
+
assert mock.check_called # Check is still called
|
|
199
|
+
assert not mock.enter_called # But not entered
|
|
200
|
+
assert not mock.step_called
|
|
201
|
+
|
|
202
|
+
def test_handle_priority_ordering(self):
|
|
203
|
+
"""Test that instances are processed by priority"""
|
|
204
|
+
scheduler = Scheduler()
|
|
205
|
+
|
|
206
|
+
class HighPriority(MockLogic):
|
|
207
|
+
PRIORITY = 10
|
|
208
|
+
|
|
209
|
+
class LowPriority(MockLogic):
|
|
210
|
+
PRIORITY = 0
|
|
211
|
+
|
|
212
|
+
low = LowPriority()
|
|
213
|
+
high = HighPriority()
|
|
214
|
+
|
|
215
|
+
scheduler.add(low)
|
|
216
|
+
scheduler.add(high)
|
|
217
|
+
|
|
218
|
+
# Both should be in updates after handle
|
|
219
|
+
scheduler.handle()
|
|
220
|
+
|
|
221
|
+
assert high in scheduler.updates
|
|
222
|
+
assert low in scheduler.updates
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestSchedulerEnableDisable:
|
|
226
|
+
"""Test enable/disable functionality"""
|
|
227
|
+
|
|
228
|
+
def test_disable_prevents_processing(self):
|
|
229
|
+
"""Test that global disable prevents processing"""
|
|
230
|
+
scheduler = Scheduler()
|
|
231
|
+
mock = MockLogic()
|
|
232
|
+
scheduler.add(mock)
|
|
233
|
+
|
|
234
|
+
Scheduler.Disable()
|
|
235
|
+
scheduler.handle()
|
|
236
|
+
|
|
237
|
+
# Global disable prevents all processing
|
|
238
|
+
assert not mock.check_called
|
|
239
|
+
|
|
240
|
+
# Re-enable for other tests
|
|
241
|
+
Scheduler.Enable()
|
|
242
|
+
|
|
243
|
+
def test_instance_enable_disable(self):
|
|
244
|
+
"""Test instance-level enable/disable"""
|
|
245
|
+
scheduler = Scheduler()
|
|
246
|
+
mock = MockLogic()
|
|
247
|
+
scheduler.add(mock)
|
|
248
|
+
|
|
249
|
+
scheduler.disable()
|
|
250
|
+
# Global disable affects all new handles
|
|
251
|
+
|
|
252
|
+
scheduler.enable()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestSchedulerLenAndContains:
|
|
256
|
+
"""Test __len__ and __contains__"""
|
|
257
|
+
|
|
258
|
+
def test_len_counts_instances(self):
|
|
259
|
+
"""Test that __len__ returns total instance count"""
|
|
260
|
+
scheduler = Scheduler()
|
|
261
|
+
|
|
262
|
+
assert len(scheduler) == 0
|
|
263
|
+
|
|
264
|
+
mock1 = MockLogic()
|
|
265
|
+
mock2 = MockLogic()
|
|
266
|
+
scheduler.add(mock1)
|
|
267
|
+
scheduler.add(mock2)
|
|
268
|
+
|
|
269
|
+
assert len(scheduler) == 2
|
|
270
|
+
|
|
271
|
+
def test_contains_checks_instance(self):
|
|
272
|
+
"""Test that __contains__ checks for instance"""
|
|
273
|
+
scheduler = Scheduler()
|
|
274
|
+
mock = MockLogic()
|
|
275
|
+
|
|
276
|
+
assert mock not in scheduler
|
|
277
|
+
|
|
278
|
+
scheduler.add(mock)
|
|
279
|
+
|
|
280
|
+
assert mock in scheduler
|