-
-
Notifications
You must be signed in to change notification settings - Fork 103
Expand file tree
/
Copy pathtest_statechart_history.py
More file actions
224 lines (169 loc) · 8.07 KB
/
test_statechart_history.py
File metadata and controls
224 lines (169 loc) · 8.07 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
"""History state behavior with shallow and deep history.
Tests exercise shallow history (remembers last direct child), deep history
(remembers exact leaf in nested compounds), default transitions on first visit,
multiple exit/reentry cycles, and the history_values dict.
Theme: Gollum's dual personality — remembers which was active.
"""
import pytest
from statemachine import HistoryState
from statemachine import State
from statemachine import StateChart
@pytest.mark.timeout(5)
class TestHistoryStates:
async def test_shallow_history_remembers_last_child(self, sm_runner):
"""Exit compound, re-enter via history -> restores last active child."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
light_side = gollum.to(smeagol)
outside = State()
leave = personality.to(outside)
return_via_history = outside.to(personality.h)
sm = await sm_runner.start(GollumPersonality)
await sm_runner.send(sm, "dark_side")
assert "gollum" in sm.configuration_values
await sm_runner.send(sm, "leave")
assert {"outside"} == set(sm.configuration_values)
await sm_runner.send(sm, "return_via_history")
assert "gollum" in sm.configuration_values
assert "personality" in sm.configuration_values
async def test_shallow_history_default_on_first_visit(self, sm_runner):
"""No prior visit -> history uses default transition target."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
_ = h.to(smeagol) # default: smeagol
outside = State(initial=True)
enter_via_history = outside.to(personality.h)
leave = personality.to(outside)
sm = await sm_runner.start(GollumPersonality)
assert {"outside"} == set(sm.configuration_values)
await sm_runner.send(sm, "enter_via_history")
assert "smeagol" in sm.configuration_values
async def test_deep_history_remembers_full_descendant(self, sm_runner):
"""Deep history restores the exact leaf in a nested compound."""
class DeepMemoryOfMoria(StateChart):
class moria(State.Compound):
class halls(State.Compound):
entrance = State(initial=True)
chamber = State()
explore = entrance.to(chamber)
assert isinstance(halls, State)
h = HistoryState(type="deep")
bridge = State(final=True)
flee = halls.to(bridge)
outside = State()
escape = moria.to(outside)
return_deep = outside.to(moria.h)
sm = await sm_runner.start(DeepMemoryOfMoria)
await sm_runner.send(sm, "explore")
assert "chamber" in sm.configuration_values
await sm_runner.send(sm, "escape")
assert {"outside"} == set(sm.configuration_values)
await sm_runner.send(sm, "return_deep")
assert "chamber" in sm.configuration_values
assert "halls" in sm.configuration_values
assert "moria" in sm.configuration_values
async def test_multiple_exits_and_reentries(self, sm_runner):
"""History updates each time we exit the compound."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
light_side = gollum.to(smeagol)
outside = State()
leave = personality.to(outside)
return_via_history = outside.to(personality.h)
sm = await sm_runner.start(GollumPersonality)
await sm_runner.send(sm, "leave")
await sm_runner.send(sm, "return_via_history")
assert "smeagol" in sm.configuration_values
await sm_runner.send(sm, "dark_side")
await sm_runner.send(sm, "leave")
await sm_runner.send(sm, "return_via_history")
assert "gollum" in sm.configuration_values
await sm_runner.send(sm, "light_side")
await sm_runner.send(sm, "leave")
await sm_runner.send(sm, "return_via_history")
assert "smeagol" in sm.configuration_values
async def test_history_after_state_change(self, sm_runner):
"""Change state within compound, exit, re-enter -> new state restored."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
outside = State()
leave = personality.to(outside)
return_via_history = outside.to(personality.h)
sm = await sm_runner.start(GollumPersonality)
await sm_runner.send(sm, "dark_side")
await sm_runner.send(sm, "leave")
await sm_runner.send(sm, "return_via_history")
assert "gollum" in sm.configuration_values
async def test_shallow_only_remembers_immediate_child(self, sm_runner):
"""Shallow history in nested compound restores direct child, not grandchild."""
class ShallowMoria(StateChart):
class moria(State.Compound):
class halls(State.Compound):
entrance = State(initial=True)
chamber = State()
explore = entrance.to(chamber)
assert isinstance(halls, State)
h = HistoryState()
bridge = State(final=True)
flee = halls.to(bridge)
outside = State()
escape = moria.to(outside)
return_shallow = outside.to(moria.h)
sm = await sm_runner.start(ShallowMoria)
await sm_runner.send(sm, "explore")
assert "chamber" in sm.configuration_values
await sm_runner.send(sm, "escape")
await sm_runner.send(sm, "return_shallow")
# Shallow history restores 'halls' as the direct child,
# but re-enters halls at its initial state (entrance), not chamber
assert "halls" in sm.configuration_values
assert "entrance" in sm.configuration_values
async def test_history_values_dict_populated(self, sm_runner):
"""sm.history_values[history_id] has saved states after exit."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
outside = State()
leave = personality.to(outside)
return_via_history = outside.to(personality.h)
sm = await sm_runner.start(GollumPersonality)
await sm_runner.send(sm, "dark_side")
await sm_runner.send(sm, "leave")
assert "h" in sm.history_values
saved = sm.history_values["h"]
assert len(saved) == 1
assert saved[0].id == "gollum"
async def test_history_with_default_transition(self, sm_runner):
"""HistoryState with explicit default .to() transition."""
class GollumPersonality(StateChart):
class personality(State.Compound):
smeagol = State(initial=True)
gollum = State()
h = HistoryState()
dark_side = smeagol.to(gollum)
_ = h.to(gollum) # default: gollum (not the initial smeagol)
outside = State(initial=True)
enter_via_history = outside.to(personality.h)
leave = personality.to(outside)
sm = await sm_runner.start(GollumPersonality)
await sm_runner.send(sm, "enter_via_history")
assert "gollum" in sm.configuration_values