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
chartflow/fsm/stdio.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""stdio compatibility module for qfsm.
|
|
3
|
+
|
|
4
|
+
This module provides a compatibility layer for the std.info/std.error/std.warn
|
|
5
|
+
functions used in the old version of qfsm.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Color codes for terminal output
|
|
9
|
+
class Colors:
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
RED = "\033[31m"
|
|
12
|
+
GREEN = "\033[32m"
|
|
13
|
+
YELLOW = "\033[33m"
|
|
14
|
+
BLUE = "\033[34m"
|
|
15
|
+
MAGENTA = "\033[35m"
|
|
16
|
+
CYAN = "\033[36m"
|
|
17
|
+
WHITE = "\033[37m"
|
|
18
|
+
BRIGHT = "\033[1m"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def info(tag: str, message: str, extra: str = "") -> None:
|
|
22
|
+
"""Print info message.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
tag: Message tag/source (e.g., "LogicName.fbm")
|
|
26
|
+
message: Main message content
|
|
27
|
+
extra: Extra information to append
|
|
28
|
+
"""
|
|
29
|
+
if extra:
|
|
30
|
+
print(f"[{tag}] {message} {extra}")
|
|
31
|
+
else:
|
|
32
|
+
print(f"[{tag}] {message}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def error(tag: str, message: str, *args) -> None:
|
|
36
|
+
"""Print error message.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
tag: Message tag/source
|
|
40
|
+
message: Error message
|
|
41
|
+
*args: Additional arguments to print
|
|
42
|
+
"""
|
|
43
|
+
msg = f"[{tag}] {message}"
|
|
44
|
+
if args:
|
|
45
|
+
msg += " " + " ".join(str(a) for a in args)
|
|
46
|
+
print(msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def warn(tag: str, message: str) -> None:
|
|
50
|
+
"""Print warning message.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
tag: Message tag/source
|
|
54
|
+
message: Warning message
|
|
55
|
+
"""
|
|
56
|
+
print(f"[{tag}] {message}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def debug(tag: str, message: str) -> None:
|
|
60
|
+
"""Print debug message.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
tag: Message tag/source
|
|
64
|
+
message: Debug message
|
|
65
|
+
"""
|
|
66
|
+
print(f"[{tag}] {message}")
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Tests for decorators"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from chartflow.fsm.decorators import (
|
|
5
|
+
recursive, listen, behavior,
|
|
6
|
+
sequence, selector, parallel
|
|
7
|
+
)
|
|
8
|
+
from chartflow.fsm.stage import Stage
|
|
9
|
+
from chartflow.fsm.meta import clear_prototypes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(autouse=True)
|
|
13
|
+
def reset_state():
|
|
14
|
+
"""Reset state before each test"""
|
|
15
|
+
clear_prototypes()
|
|
16
|
+
Stage._Stage__id__ = {}
|
|
17
|
+
yield
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestRecursiveDecorator:
|
|
21
|
+
"""Test @recursive decorator"""
|
|
22
|
+
|
|
23
|
+
def test_recursive_marked(self):
|
|
24
|
+
"""Test that @recursive marks method"""
|
|
25
|
+
|
|
26
|
+
class TestStage(Stage):
|
|
27
|
+
NAME = "TestStage"
|
|
28
|
+
|
|
29
|
+
@recursive
|
|
30
|
+
def my_state(self, inst):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
assert "my_state" in TestStage.__recursive__
|
|
34
|
+
|
|
35
|
+
def test_recursive_inheritance(self):
|
|
36
|
+
"""Test that @recursive is inherited"""
|
|
37
|
+
|
|
38
|
+
class BaseStage(Stage):
|
|
39
|
+
NAME = "BaseStage"
|
|
40
|
+
|
|
41
|
+
@recursive
|
|
42
|
+
def base_state(self, inst):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
class DerivedStage(BaseStage):
|
|
46
|
+
NAME = "DerivedStage"
|
|
47
|
+
|
|
48
|
+
def derived_state(self, inst):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
assert "base_state" in DerivedStage.__recursive__
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestListenDecorator:
|
|
55
|
+
"""Test @listen decorator"""
|
|
56
|
+
|
|
57
|
+
def test_listen_marked(self):
|
|
58
|
+
"""Test that @listen marks method"""
|
|
59
|
+
|
|
60
|
+
class TestStage(Stage):
|
|
61
|
+
NAME = "TestStage"
|
|
62
|
+
|
|
63
|
+
@listen("event1")
|
|
64
|
+
def handler1(self, data):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
@listen("event2", "source1", "source2")
|
|
68
|
+
def handler2(self, data):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
assert "event1" in TestStage.__listens__
|
|
72
|
+
assert "handler1" in [h[0] for h in TestStage.__listens__["event1"]]
|
|
73
|
+
|
|
74
|
+
assert "event2" in TestStage.__listens__
|
|
75
|
+
handlers = TestStage.__listens__["event2"]
|
|
76
|
+
assert any(h[0] == "handler2" and list(h[1]) == ["source1", "source2"] for h in handlers)
|
|
77
|
+
|
|
78
|
+
def test_listen_inheritance(self):
|
|
79
|
+
"""Test that @listen is inherited"""
|
|
80
|
+
|
|
81
|
+
class BaseStage(Stage):
|
|
82
|
+
NAME = "BaseStage"
|
|
83
|
+
|
|
84
|
+
@listen("event1")
|
|
85
|
+
def handler1(self, data):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
class DerivedStage(BaseStage):
|
|
89
|
+
NAME = "DerivedStage"
|
|
90
|
+
|
|
91
|
+
@listen("event2")
|
|
92
|
+
def handler2(self, data):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
assert "event1" in DerivedStage.__listens__
|
|
96
|
+
assert "event2" in DerivedStage.__listens__
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestBehaviorDecorator:
|
|
100
|
+
"""Test @behavior decorator"""
|
|
101
|
+
|
|
102
|
+
def test_behavior_marked(self):
|
|
103
|
+
"""Test that @behavior marks state transition"""
|
|
104
|
+
|
|
105
|
+
class TestStage(Stage):
|
|
106
|
+
NAME = "TestStage"
|
|
107
|
+
|
|
108
|
+
@behavior("next_state")
|
|
109
|
+
def current_state(self, inst):
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def next_state(self, inst):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
assert "current_state" in TestStage.__behaviors__
|
|
116
|
+
assert TestStage.__behaviors__["current_state"]["children"] == "next_state"
|
|
117
|
+
assert TestStage.__behaviors__["current_state"]["composite"] is None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestSequenceDecorator:
|
|
121
|
+
"""Test @sequence decorator"""
|
|
122
|
+
|
|
123
|
+
def test_sequence_marked(self):
|
|
124
|
+
"""Test that @sequence marks composite"""
|
|
125
|
+
|
|
126
|
+
class TestStage(Stage):
|
|
127
|
+
NAME = "TestStage"
|
|
128
|
+
|
|
129
|
+
@sequence("b", "c")
|
|
130
|
+
def a(self, inst):
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
def b(self, inst):
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def c(self, inst):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
assert "a" in TestStage.__behaviors__
|
|
140
|
+
assert TestStage.__behaviors__["a"]["children"] == ["b", "c"]
|
|
141
|
+
assert TestStage.__behaviors__["a"]["composite"] == "sequence"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestSelectorDecorator:
|
|
145
|
+
"""Test @selector decorator"""
|
|
146
|
+
|
|
147
|
+
def test_selector_marked(self):
|
|
148
|
+
"""Test that @selector marks composite"""
|
|
149
|
+
|
|
150
|
+
class TestStage(Stage):
|
|
151
|
+
NAME = "TestStage"
|
|
152
|
+
|
|
153
|
+
@selector("b", "c")
|
|
154
|
+
def a(self, inst):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
def b(self, inst):
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def c(self, inst):
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
assert "a" in TestStage.__behaviors__
|
|
164
|
+
assert TestStage.__behaviors__["a"]["children"] == ["b", "c"]
|
|
165
|
+
assert TestStage.__behaviors__["a"]["composite"] == "selector"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class TestParallelDecorator:
|
|
169
|
+
"""Test @parallel decorator"""
|
|
170
|
+
|
|
171
|
+
def test_parallel_marked(self):
|
|
172
|
+
"""Test that @parallel marks composite"""
|
|
173
|
+
|
|
174
|
+
class TestStage(Stage):
|
|
175
|
+
NAME = "TestStage"
|
|
176
|
+
|
|
177
|
+
@parallel("b", "c")
|
|
178
|
+
def a(self, inst):
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def b(self, inst):
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
def c(self, inst):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
assert "a" in TestStage.__behaviors__
|
|
188
|
+
assert TestStage.__behaviors__["a"]["children"] == ["b", "c"]
|
|
189
|
+
assert TestStage.__behaviors__["a"]["composite"] == "parallel"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestDecoratorCombinations:
|
|
193
|
+
"""Test combining decorators"""
|
|
194
|
+
|
|
195
|
+
def test_recursive_and_behavior(self):
|
|
196
|
+
"""Test @recursive with @behavior"""
|
|
197
|
+
|
|
198
|
+
class TestStage(Stage):
|
|
199
|
+
NAME = "TestStage"
|
|
200
|
+
|
|
201
|
+
@recursive
|
|
202
|
+
@behavior("next")
|
|
203
|
+
def current(self, inst):
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
def next(self, inst):
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
assert "current" in TestStage.__recursive__
|
|
210
|
+
assert "current" in TestStage.__behaviors__
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Tests for EventSystem"""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from chartflow.fsm.utils import EventSystem
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestEventSystemBasic:
|
|
8
|
+
"""Test basic EventSystem functionality"""
|
|
9
|
+
|
|
10
|
+
def test_listen_for_event(self):
|
|
11
|
+
"""Test listenFor registers callback"""
|
|
12
|
+
es = EventSystem()
|
|
13
|
+
|
|
14
|
+
called = False
|
|
15
|
+
|
|
16
|
+
def handler(data):
|
|
17
|
+
nonlocal called
|
|
18
|
+
called = True
|
|
19
|
+
|
|
20
|
+
es.listenFor("test_event", handler)
|
|
21
|
+
es.pushEvent("test_event", {})
|
|
22
|
+
|
|
23
|
+
assert called
|
|
24
|
+
|
|
25
|
+
def test_multiple_handlers(self):
|
|
26
|
+
"""Test multiple handlers for same event"""
|
|
27
|
+
es = EventSystem()
|
|
28
|
+
|
|
29
|
+
calls = []
|
|
30
|
+
|
|
31
|
+
def handler1(data):
|
|
32
|
+
calls.append(1)
|
|
33
|
+
|
|
34
|
+
def handler2(data):
|
|
35
|
+
calls.append(2)
|
|
36
|
+
|
|
37
|
+
es.listenFor("test_event", handler1)
|
|
38
|
+
es.listenFor("test_event", handler2)
|
|
39
|
+
es.pushEvent("test_event", {})
|
|
40
|
+
|
|
41
|
+
assert 1 in calls
|
|
42
|
+
assert 2 in calls
|
|
43
|
+
|
|
44
|
+
def test_cancel_listen(self):
|
|
45
|
+
"""Test cancelListen removes callback"""
|
|
46
|
+
es = EventSystem()
|
|
47
|
+
|
|
48
|
+
called = False
|
|
49
|
+
|
|
50
|
+
def handler(data):
|
|
51
|
+
nonlocal called
|
|
52
|
+
called = True
|
|
53
|
+
|
|
54
|
+
es.listenFor("test_event", handler)
|
|
55
|
+
es.cancelListen("test_event", handler)
|
|
56
|
+
es.pushEvent("test_event", {})
|
|
57
|
+
|
|
58
|
+
assert not called
|
|
59
|
+
|
|
60
|
+
def test_cancel_all_listeners(self):
|
|
61
|
+
"""Test cancelListen without handler removes all"""
|
|
62
|
+
es = EventSystem()
|
|
63
|
+
|
|
64
|
+
calls = []
|
|
65
|
+
|
|
66
|
+
def handler1(data):
|
|
67
|
+
calls.append(1)
|
|
68
|
+
|
|
69
|
+
def handler2(data):
|
|
70
|
+
calls.append(2)
|
|
71
|
+
|
|
72
|
+
es.listenFor("test_event", handler1)
|
|
73
|
+
es.listenFor("test_event", handler2)
|
|
74
|
+
es.cancelListen("test_event")
|
|
75
|
+
es.pushEvent("test_event", {})
|
|
76
|
+
|
|
77
|
+
assert len(calls) == 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestEventSystemSourceFiltering:
|
|
81
|
+
"""Test source filtering"""
|
|
82
|
+
|
|
83
|
+
def test_source_filter_matching(self):
|
|
84
|
+
"""Test that events from matching sources are received"""
|
|
85
|
+
es = EventSystem()
|
|
86
|
+
|
|
87
|
+
calls = []
|
|
88
|
+
|
|
89
|
+
def handler(data):
|
|
90
|
+
calls.append(data)
|
|
91
|
+
|
|
92
|
+
es.listenFor("test_event", handler, "source1", "source2")
|
|
93
|
+
es.pushEvent("test_event", {"msg": "hello"}, source="source1")
|
|
94
|
+
|
|
95
|
+
assert len(calls) == 1
|
|
96
|
+
assert calls[0]["msg"] == "hello"
|
|
97
|
+
|
|
98
|
+
def test_source_filter_non_matching(self):
|
|
99
|
+
"""Test that events from non-matching sources are filtered"""
|
|
100
|
+
es = EventSystem()
|
|
101
|
+
|
|
102
|
+
calls = []
|
|
103
|
+
|
|
104
|
+
def handler(data):
|
|
105
|
+
calls.append(data)
|
|
106
|
+
|
|
107
|
+
es.listenFor("test_event", handler, "source1")
|
|
108
|
+
es.pushEvent("test_event", {"msg": "hello"}, source="source2")
|
|
109
|
+
|
|
110
|
+
assert len(calls) == 0
|
|
111
|
+
|
|
112
|
+
def test_no_source_filter_receives_all(self):
|
|
113
|
+
"""Test that listeners without source filter receive all"""
|
|
114
|
+
es = EventSystem()
|
|
115
|
+
|
|
116
|
+
calls = []
|
|
117
|
+
|
|
118
|
+
def handler(data):
|
|
119
|
+
calls.append(data)
|
|
120
|
+
|
|
121
|
+
es.listenFor("test_event", handler)
|
|
122
|
+
es.pushEvent("test_event", {"msg": "hello"}, source="any_source")
|
|
123
|
+
|
|
124
|
+
assert len(calls) == 1
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestEventSystemTargetFiltering:
|
|
128
|
+
"""Test target filtering"""
|
|
129
|
+
|
|
130
|
+
def test_target_filter_matching(self):
|
|
131
|
+
"""Test that events with matching targets are received"""
|
|
132
|
+
es = EventSystem()
|
|
133
|
+
|
|
134
|
+
calls = []
|
|
135
|
+
|
|
136
|
+
def handler(data):
|
|
137
|
+
calls.append(data)
|
|
138
|
+
|
|
139
|
+
es.listenFor("test_event", handler)
|
|
140
|
+
# No targets specified means all listeners receive
|
|
141
|
+
es.pushEvent("test_event", {"msg": "hello"})
|
|
142
|
+
|
|
143
|
+
assert len(calls) == 1
|
|
144
|
+
|
|
145
|
+
def test_data_passed_to_handler(self):
|
|
146
|
+
"""Test that data is passed to handler"""
|
|
147
|
+
es = EventSystem()
|
|
148
|
+
|
|
149
|
+
received_data = None
|
|
150
|
+
|
|
151
|
+
def handler(data):
|
|
152
|
+
nonlocal received_data
|
|
153
|
+
received_data = data
|
|
154
|
+
|
|
155
|
+
es.listenFor("test_event", handler)
|
|
156
|
+
es.pushEvent("test_event", {"key": "value", "num": 42})
|
|
157
|
+
|
|
158
|
+
assert received_data["key"] == "value"
|
|
159
|
+
assert received_data["num"] == 42
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TestEventSystemEdgeCases:
|
|
163
|
+
"""Test edge cases"""
|
|
164
|
+
|
|
165
|
+
def test_no_listeners(self):
|
|
166
|
+
"""Test that pushEvent with no listeners doesn't error"""
|
|
167
|
+
es = EventSystem()
|
|
168
|
+
|
|
169
|
+
es.pushEvent("test_event", {}) # Should not raise
|
|
170
|
+
|
|
171
|
+
def test_unknown_event(self):
|
|
172
|
+
"""Test that pushEvent for unknown event doesn't error"""
|
|
173
|
+
es = EventSystem()
|
|
174
|
+
|
|
175
|
+
def handler(data):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
es.listenFor("other_event", handler)
|
|
179
|
+
es.pushEvent("test_event", {}) # Should not raise
|
|
180
|
+
|
|
181
|
+
def test_handler_exception(self):
|
|
182
|
+
"""Test that handler exception doesn't break system"""
|
|
183
|
+
es = EventSystem()
|
|
184
|
+
|
|
185
|
+
calls = []
|
|
186
|
+
|
|
187
|
+
def bad_handler(data):
|
|
188
|
+
raise ValueError("Test error")
|
|
189
|
+
|
|
190
|
+
def good_handler(data):
|
|
191
|
+
calls.append(data)
|
|
192
|
+
|
|
193
|
+
es.listenFor("test_event", bad_handler)
|
|
194
|
+
es.listenFor("test_event", good_handler)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
es.pushEvent("test_event", {})
|
|
198
|
+
except:
|
|
199
|
+
pass # Exception might propagate
|
|
200
|
+
|
|
201
|
+
# Good handler should still be called
|
|
202
|
+
assert len(calls) == 1
|