-
-
Notifications
You must be signed in to change notification settings - Fork 103
Expand file tree
/
Copy pathtest_statemachine_compat.py
More file actions
383 lines (269 loc) · 11.1 KB
/
test_statemachine_compat.py
File metadata and controls
383 lines (269 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
"""Backward-compatibility tests for the StateMachine (v2) API.
These tests verify that ``StateMachine`` (which inherits from ``StateChart``
with different defaults) continues to work as expected. Tests here exercise
behaviour that differs from ``StateChart`` defaults:
- ``allow_event_without_transition = False`` → ``TransitionNotAllowed``
- ``enable_self_transition_entries = False``
- ``atomic_configuration_update = True``
- ``catch_errors_as_events = False`` → exceptions propagate directly
- ``current_state`` deprecated property
"""
import warnings
import pytest
from statemachine import State
from statemachine import StateMachine
from statemachine import exceptions
# ---------------------------------------------------------------------------
# Flag defaults
# ---------------------------------------------------------------------------
class TestStateMachineDefaults:
"""Verify the four class-level flag defaults on StateMachine."""
def test_allow_event_without_transition(self):
assert StateMachine.allow_event_without_transition is False
def test_enable_self_transition_entries(self):
assert StateMachine.enable_self_transition_entries is False
def test_atomic_configuration_update(self):
assert StateMachine.atomic_configuration_update is True
def test_catch_errors_as_events(self):
assert StateMachine.catch_errors_as_events is False
# ---------------------------------------------------------------------------
# Smoke test
# ---------------------------------------------------------------------------
class TestStateMachineSmoke:
"""StateMachine as a subclass works for basic operations."""
def test_create_send_and_check_state(self):
class TrafficLight(StateMachine):
green = State(initial=True)
yellow = State()
red = State()
cycle = green.to(yellow) | yellow.to(red) | red.to(green)
sm = TrafficLight()
assert sm.green.is_active
sm.send("cycle")
assert sm.yellow.is_active
sm.send("cycle")
assert sm.red.is_active
def test_final_state_terminates(self):
class Simple(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
sm = Simple()
sm.send("go")
assert sm.is_terminated
# ---------------------------------------------------------------------------
# TransitionNotAllowed (allow_event_without_transition = False)
# ---------------------------------------------------------------------------
class TestTransitionNotAllowed:
"""StateMachine raises TransitionNotAllowed for invalid events."""
@pytest.fixture()
def sm(self):
class Workflow(StateMachine):
draft = State(initial=True)
published = State(final=True)
publish = draft.to(published)
return Workflow()
def test_invalid_event_raises(self, sm):
with pytest.raises(exceptions.TransitionNotAllowed):
sm.send("nonexistent")
def test_event_not_available_in_current_state(self, sm):
sm.send("publish")
with pytest.raises(exceptions.TransitionNotAllowed):
sm.send("publish")
def test_condition_blocks_transition(self):
class Gated(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2, cond="allowed")
allowed: bool = False
sm = Gated()
with pytest.raises(sm.TransitionNotAllowed):
sm.go()
def test_multiple_destinations_all_blocked(self):
def never(event_data):
return False
class Multi(StateMachine):
requested = State(initial=True)
accepted = State(final=True)
rejected = State(final=True)
validate = requested.to(accepted, cond=never) | requested.to(
rejected, cond="also_never"
)
@property
def also_never(self):
return False
sm = Multi()
with pytest.raises(exceptions.TransitionNotAllowed):
sm.validate()
assert sm.requested.is_active
def test_from_any_with_cond_blocked(self):
class Account(StateMachine):
active = State(initial=True)
closed = State(final=True)
close = closed.from_.any(cond="can_close")
can_close: bool = False
sm = Account()
with pytest.raises(sm.TransitionNotAllowed):
sm.close()
def test_condition_algebra_any_false(self):
class CondAlgebra(StateMachine):
start = State(initial=True)
end = State(final=True)
submit = start.to(end, cond="used_money or used_credit")
used_money: bool = False
used_credit: bool = False
sm = CondAlgebra()
with pytest.raises(sm.TransitionNotAllowed):
sm.submit()
# ---------------------------------------------------------------------------
# TransitionNotAllowed — async
# ---------------------------------------------------------------------------
class TestTransitionNotAllowedAsync:
"""TransitionNotAllowed in async machines."""
@pytest.fixture()
def async_sm_cls(self):
class AsyncWorkflow(StateMachine):
s1 = State(initial=True)
s2 = State()
s3 = State(final=True)
go = s1.to(s2, cond="is_ready")
finish = s2.to(s3)
is_ready: bool = False
async def on_go(self): ...
return AsyncWorkflow
async def test_async_transition_not_allowed(self, async_sm_cls):
sm = async_sm_cls()
await sm.activate_initial_state()
with pytest.raises(sm.TransitionNotAllowed):
await sm.send("go")
def test_sync_context_transition_not_allowed(self, async_sm_cls):
sm = async_sm_cls()
with pytest.raises(sm.TransitionNotAllowed):
sm.send("go")
async def test_async_condition_blocks(self):
class AsyncCond(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2, cond="check")
async def check(self):
return False
sm = AsyncCond()
await sm.activate_initial_state()
with pytest.raises(sm.TransitionNotAllowed):
await sm.go()
# ---------------------------------------------------------------------------
# catch_errors_as_events = False (exceptions propagate directly)
# ---------------------------------------------------------------------------
class TestErrorOnExecutionFalse:
"""With catch_errors_as_events=False, exceptions propagate without being caught."""
def test_runtime_error_in_action_propagates(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
def on_go(self):
raise RuntimeError("boom")
sm = SM()
with pytest.raises(RuntimeError, match="boom"):
sm.send("go")
def test_runtime_error_in_after_propagates(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
def after_go(self):
raise RuntimeError("after boom")
sm = SM()
with pytest.raises(RuntimeError, match="after boom"):
sm.send("go")
@pytest.mark.timeout(5)
async def test_async_runtime_error_in_after_propagates(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
async def after_go(self, **kwargs):
raise RuntimeError("async after boom")
sm = SM()
await sm.activate_initial_state()
with pytest.raises(RuntimeError, match="async after boom"):
await sm.send("go")
# ---------------------------------------------------------------------------
# enable_self_transition_entries = False
# ---------------------------------------------------------------------------
class TestSelfTransitionNoEntries:
"""With enable_self_transition_entries=False, internal self-transitions do NOT fire entry/exit.
Note: ``enable_self_transition_entries`` only applies to *internal* self-transitions
(``internal=True``). External self-transitions always fire entry/exit regardless.
"""
def test_internal_self_transition_does_not_fire_enter_exit(self):
log = []
class SM(StateMachine):
s1 = State(initial=True)
loop = s1.to.itself(internal=True)
def on_enter_s1(self):
log.append("enter_s1")
def on_exit_s1(self):
log.append("exit_s1")
sm = SM()
log.clear() # clear initial enter
sm.send("loop")
assert "enter_s1" not in log
assert "exit_s1" not in log
def test_external_self_transition_fires_enter_exit(self):
"""External self-transitions always fire, regardless of the flag."""
log = []
class SM(StateMachine):
s1 = State(initial=True)
loop = s1.to.itself()
def on_enter_s1(self):
log.append("enter_s1")
def on_exit_s1(self):
log.append("exit_s1")
sm = SM()
log.clear()
sm.send("loop")
assert "enter_s1" in log
assert "exit_s1" in log
# ---------------------------------------------------------------------------
# current_state deprecated property
# ---------------------------------------------------------------------------
class TestCurrentStateDeprecated:
"""The current_state property emits DeprecationWarning but still works."""
def test_current_state_returns_state(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
sm = SM()
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
cs = sm.current_state
assert cs == sm.s1
def test_current_state_emits_warning(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
sm = SM()
with pytest.warns(DeprecationWarning, match="current_state"):
_ = sm.current_state # noqa: F841
def test_current_state_setter_emits_warning(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
sm = SM()
with pytest.warns(DeprecationWarning, match="current_state"):
sm.current_state = sm.s2
assert sm.s2 in sm.configuration
def test_current_state_setter_changes_state(self):
class SM(StateMachine):
s1 = State(initial=True)
s2 = State(final=True)
go = s1.to(s2)
sm = SM()
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
sm.current_state = sm.s2
assert sm.s2 in sm.configuration