mycorrhizal 0.1.0__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.
- mycorrhizal/__init__.py +3 -0
- mycorrhizal/common/__init__.py +68 -0
- mycorrhizal/common/interface_builder.py +203 -0
- mycorrhizal/common/interfaces.py +412 -0
- mycorrhizal/common/timebase.py +99 -0
- mycorrhizal/common/wrappers.py +532 -0
- mycorrhizal/enoki/__init__.py +0 -0
- mycorrhizal/enoki/core.py +1545 -0
- mycorrhizal/enoki/testing_utils.py +529 -0
- mycorrhizal/enoki/util.py +220 -0
- mycorrhizal/hypha/__init__.py +0 -0
- mycorrhizal/hypha/core/__init__.py +107 -0
- mycorrhizal/hypha/core/builder.py +404 -0
- mycorrhizal/hypha/core/runtime.py +890 -0
- mycorrhizal/hypha/core/specs.py +234 -0
- mycorrhizal/hypha/util.py +38 -0
- mycorrhizal/rhizomorph/README.md +220 -0
- mycorrhizal/rhizomorph/__init__.py +0 -0
- mycorrhizal/rhizomorph/core.py +1729 -0
- mycorrhizal/rhizomorph/util.py +45 -0
- mycorrhizal/spores/__init__.py +124 -0
- mycorrhizal/spores/cache.py +208 -0
- mycorrhizal/spores/core.py +419 -0
- mycorrhizal/spores/dsl/__init__.py +48 -0
- mycorrhizal/spores/dsl/enoki.py +514 -0
- mycorrhizal/spores/dsl/hypha.py +399 -0
- mycorrhizal/spores/dsl/rhizomorph.py +351 -0
- mycorrhizal/spores/encoder/__init__.py +11 -0
- mycorrhizal/spores/encoder/base.py +42 -0
- mycorrhizal/spores/encoder/json.py +159 -0
- mycorrhizal/spores/extraction.py +484 -0
- mycorrhizal/spores/models.py +288 -0
- mycorrhizal/spores/transport/__init__.py +10 -0
- mycorrhizal/spores/transport/base.py +46 -0
- mycorrhizal-0.1.0.dist-info/METADATA +198 -0
- mycorrhizal-0.1.0.dist-info/RECORD +37 -0
- mycorrhizal-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Enoki Testing Utilities
|
|
4
|
+
|
|
5
|
+
Testing infrastructure for the class-method-based Enoki architecture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
11
|
+
import time
|
|
12
|
+
import gevent
|
|
13
|
+
from gevent import sleep
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
# Import the Enoki modules
|
|
17
|
+
from mycorrhizal.enoki import (
|
|
18
|
+
State,
|
|
19
|
+
StateMachine,
|
|
20
|
+
StateConfiguration,
|
|
21
|
+
SharedContext,
|
|
22
|
+
TimeoutMessage,
|
|
23
|
+
Push,
|
|
24
|
+
Pop,
|
|
25
|
+
ValidationError,
|
|
26
|
+
BlockedInUntimedState,
|
|
27
|
+
StateMachineComplete,
|
|
28
|
+
Unhandled
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_mock_context(common_data=None, msg=None, **kwargs):
|
|
33
|
+
"""
|
|
34
|
+
Create a mock SharedContext for testing state methods.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
common_data: dict/object for shared state between states
|
|
38
|
+
msg: message to set as current message
|
|
39
|
+
**kwargs: additional attributes to set on the context
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
SharedContext configured for testing
|
|
43
|
+
"""
|
|
44
|
+
if common_data is None:
|
|
45
|
+
common_data = SimpleNamespace()
|
|
46
|
+
elif isinstance(common_data, dict):
|
|
47
|
+
common_data = SimpleNamespace(**common_data)
|
|
48
|
+
|
|
49
|
+
# Mock functions
|
|
50
|
+
send_message_mock = Mock(return_value=None)
|
|
51
|
+
log_mock = Mock(return_value=None)
|
|
52
|
+
|
|
53
|
+
context = SharedContext(
|
|
54
|
+
send_message=send_message_mock,
|
|
55
|
+
log=log_mock,
|
|
56
|
+
common=common_data,
|
|
57
|
+
msg=msg,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Apply any additional kwargs
|
|
61
|
+
for key, value in kwargs.items():
|
|
62
|
+
setattr(context, key, value)
|
|
63
|
+
|
|
64
|
+
return context
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_state_method(state_class, method_name, context=None, **context_kwargs):
|
|
68
|
+
"""
|
|
69
|
+
Run a specific state method (on_state, on_enter, etc.) with given context.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
state_class: The state class to test
|
|
73
|
+
method_name: Name of the method to call ('on_state', 'on_enter', etc.)
|
|
74
|
+
context: SharedContext to pass, or None to create one
|
|
75
|
+
**context_kwargs: Arguments for make_mock_context if context is None
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The result from the state method
|
|
79
|
+
"""
|
|
80
|
+
if context is None:
|
|
81
|
+
context = make_mock_context(**context_kwargs)
|
|
82
|
+
|
|
83
|
+
method = getattr(state_class, method_name)
|
|
84
|
+
return method(context)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_state_on_state(state_class, context=None, **context_kwargs):
|
|
88
|
+
"""
|
|
89
|
+
Convenience function to run a state's on_state method.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
state_class: The state class to test
|
|
93
|
+
context: SharedContext to pass, or None to create one
|
|
94
|
+
**context_kwargs: Arguments for make_mock_context if context is None
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The transition result from on_state
|
|
98
|
+
"""
|
|
99
|
+
return run_state_method(state_class, "on_state", context, **context_kwargs)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def simulate_state_lifecycle(
|
|
103
|
+
state_class, messages=None, max_iterations=10, **context_kwargs
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Simulate a complete state lifecycle with multiple messages.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
state_class: The state class to simulate
|
|
110
|
+
messages: List of messages to send, or None for no messages
|
|
111
|
+
max_iterations: Maximum iterations before giving up
|
|
112
|
+
**context_kwargs: Arguments for make_mock_context
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of (message, transition_result) tuples
|
|
116
|
+
"""
|
|
117
|
+
if messages is None:
|
|
118
|
+
messages = [None] # Single iteration with no message
|
|
119
|
+
|
|
120
|
+
context = make_mock_context(**context_kwargs)
|
|
121
|
+
results = []
|
|
122
|
+
|
|
123
|
+
# Call on_enter first
|
|
124
|
+
try:
|
|
125
|
+
state_class.on_enter(context)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
results.append(("on_enter", e))
|
|
128
|
+
|
|
129
|
+
# Process each message
|
|
130
|
+
for msg in messages:
|
|
131
|
+
context.msg = msg
|
|
132
|
+
try:
|
|
133
|
+
result = state_class.on_state(context)
|
|
134
|
+
results.append((msg, result))
|
|
135
|
+
|
|
136
|
+
# Break if we get a definitive transition
|
|
137
|
+
if result not in (None, Unhandled):
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
results.append((msg, e))
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Call on_leave
|
|
145
|
+
try:
|
|
146
|
+
state_class.on_leave(context)
|
|
147
|
+
results.append(("on_leave", None))
|
|
148
|
+
except Exception as e:
|
|
149
|
+
results.append(("on_leave", e))
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class EnokiTestCase(unittest.TestCase):
|
|
155
|
+
"""Base test case class with assertion methods for Enoki FSM testing."""
|
|
156
|
+
|
|
157
|
+
def assertStateTransition(
|
|
158
|
+
self, state_class, expected_transition, context=None, **context_kwargs
|
|
159
|
+
):
|
|
160
|
+
"""Assert that a state transitions to the expected transition type."""
|
|
161
|
+
result = run_state_on_state(state_class, context, **context_kwargs)
|
|
162
|
+
|
|
163
|
+
if isinstance(expected_transition, type) and issubclass(
|
|
164
|
+
expected_transition, State
|
|
165
|
+
):
|
|
166
|
+
# Convert state class to string for comparison
|
|
167
|
+
if isinstance(result, str):
|
|
168
|
+
expected_name = expected_transition.name
|
|
169
|
+
self.assertEqual(
|
|
170
|
+
result,
|
|
171
|
+
expected_name,
|
|
172
|
+
f"Expected transition to {expected_name}, got {result}",
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
self.fail(f"Expected string state name, got {type(result)}: {result}")
|
|
176
|
+
elif isinstance(expected_transition, TransitionName):
|
|
177
|
+
self.assertEqual(
|
|
178
|
+
result,
|
|
179
|
+
expected_transition,
|
|
180
|
+
f"Expected {expected_transition}, got {result}",
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
self.assertEqual(result, expected_transition)
|
|
184
|
+
|
|
185
|
+
def assertNoTransition(self, state_class, context=None, **context_kwargs):
|
|
186
|
+
"""Assert that a state does not transition (returns UNHANDLED)."""
|
|
187
|
+
result = run_state_on_state(state_class, context, **context_kwargs)
|
|
188
|
+
self.assertIn(
|
|
189
|
+
result, (None, Unhandled), f"Expected None/Unhandled, got {result}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def assertPushTransition(
|
|
193
|
+
self, state_class, expected_states, context=None, **context_kwargs
|
|
194
|
+
):
|
|
195
|
+
"""Assert that a state returns a Push transition with expected states."""
|
|
196
|
+
result = run_state_on_state(state_class, context, **context_kwargs)
|
|
197
|
+
self.assertIsInstance(result, Push, "Expected Push transition")
|
|
198
|
+
|
|
199
|
+
# Convert expected states to names if they're classes
|
|
200
|
+
expected_names = []
|
|
201
|
+
for state in expected_states:
|
|
202
|
+
if isinstance(state, type) and issubclass(state, State):
|
|
203
|
+
expected_names.append(state.name)
|
|
204
|
+
else:
|
|
205
|
+
expected_names.append(str(state))
|
|
206
|
+
|
|
207
|
+
actual_names = list(result.states)
|
|
208
|
+
self.assertEqual(
|
|
209
|
+
actual_names,
|
|
210
|
+
expected_names,
|
|
211
|
+
f"Expected push states {expected_names}, got {actual_names}",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def assertPopTransition(self, state_class, context=None, **context_kwargs):
|
|
215
|
+
"""Assert that a state returns a Pop transition."""
|
|
216
|
+
result = run_state_on_state(state_class, context, **context_kwargs)
|
|
217
|
+
self.assertIs(result, Pop, "Expected Pop transition")
|
|
218
|
+
|
|
219
|
+
def assertStateRaisesException(
|
|
220
|
+
self, state_class, expected_exception, context=None, **context_kwargs
|
|
221
|
+
):
|
|
222
|
+
"""Assert that a state raises a specific exception."""
|
|
223
|
+
with self.assertRaises(expected_exception):
|
|
224
|
+
run_state_on_state(state_class, context, **context_kwargs)
|
|
225
|
+
|
|
226
|
+
def assertLifecycleMethodCalled(
|
|
227
|
+
self, state_class, method_name, context=None, **context_kwargs
|
|
228
|
+
):
|
|
229
|
+
"""Assert that a specific lifecycle method can be called without error."""
|
|
230
|
+
if context is None:
|
|
231
|
+
context = make_mock_context(**context_kwargs)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
method = getattr(state_class, method_name)
|
|
235
|
+
if method_name == "on_state":
|
|
236
|
+
# on_state must return something
|
|
237
|
+
result = method(context)
|
|
238
|
+
self.assertIsNotNone(
|
|
239
|
+
result, f"{method_name} should return a transition"
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
# Other lifecycle methods can return None
|
|
243
|
+
result = method(context)
|
|
244
|
+
# Just verify it doesn't crash
|
|
245
|
+
except AttributeError:
|
|
246
|
+
self.fail(
|
|
247
|
+
f"State {state_class.__name__} does not have method {method_name}"
|
|
248
|
+
)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.fail(f"Method {method_name} raised unexpected exception: {e}")
|
|
251
|
+
|
|
252
|
+
def assertSharedStateModified(
|
|
253
|
+
self,
|
|
254
|
+
state_class,
|
|
255
|
+
attribute_name,
|
|
256
|
+
expected_value,
|
|
257
|
+
context=None,
|
|
258
|
+
**context_kwargs,
|
|
259
|
+
):
|
|
260
|
+
"""Assert that a state modifies shared state as expected."""
|
|
261
|
+
if context is None:
|
|
262
|
+
context = make_mock_context(**context_kwargs)
|
|
263
|
+
|
|
264
|
+
# Run the state method
|
|
265
|
+
run_state_on_state(state_class, context)
|
|
266
|
+
|
|
267
|
+
# Check if the attribute was set correctly
|
|
268
|
+
actual_value = getattr(context.common, attribute_name, None)
|
|
269
|
+
self.assertEqual(
|
|
270
|
+
actual_value,
|
|
271
|
+
expected_value,
|
|
272
|
+
f"Expected {attribute_name}={expected_value}, got {actual_value}",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def assertTransitionMapping(self, state_class, transition_enum, expected_target):
|
|
276
|
+
"""Assert that a state's transition mapping is correct."""
|
|
277
|
+
transitions = state_class.transitions()
|
|
278
|
+
|
|
279
|
+
self.assertIn(
|
|
280
|
+
transition_enum,
|
|
281
|
+
transitions,
|
|
282
|
+
f"State {state_class.__name__} missing transition for {transition_enum}",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
actual_target = transitions[transition_enum]
|
|
286
|
+
|
|
287
|
+
if isinstance(expected_target, type) and issubclass(expected_target, State):
|
|
288
|
+
expected_name = expected_target.name
|
|
289
|
+
self.assertEqual(
|
|
290
|
+
actual_target,
|
|
291
|
+
expected_name,
|
|
292
|
+
f"Expected transition to {expected_name}, got {actual_target}",
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
self.assertEqual(
|
|
296
|
+
actual_target,
|
|
297
|
+
expected_target,
|
|
298
|
+
f"Expected {expected_target}, got {actual_target}",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def assertStateConfiguration(self, state_class, **expected_config):
|
|
302
|
+
"""Assert that a state's configuration matches expected values."""
|
|
303
|
+
config = state_class.CONFIG
|
|
304
|
+
|
|
305
|
+
for attr_name, expected_value in expected_config.items():
|
|
306
|
+
actual_value = getattr(config, attr_name)
|
|
307
|
+
self.assertEqual(
|
|
308
|
+
actual_value,
|
|
309
|
+
expected_value,
|
|
310
|
+
f"Expected {attr_name}={expected_value}, got {actual_value}",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def assertTimeoutBehavior(self, state_class, context=None, **context_kwargs):
|
|
314
|
+
"""Assert that a state properly handles timeout messages."""
|
|
315
|
+
timeout_msg = TimeoutMessage(state_class.name, 1, 5.0)
|
|
316
|
+
|
|
317
|
+
if context is None:
|
|
318
|
+
context = make_mock_context(msg=timeout_msg, **context_kwargs)
|
|
319
|
+
else:
|
|
320
|
+
context.msg = timeout_msg
|
|
321
|
+
|
|
322
|
+
# Test on_state with timeout message
|
|
323
|
+
result = state_class.on_state(context)
|
|
324
|
+
self.assertIsNotNone(result, "State should handle timeout message")
|
|
325
|
+
|
|
326
|
+
# Test on_timeout directly
|
|
327
|
+
timeout_result = state_class.on_timeout(context)
|
|
328
|
+
self.assertIsNotNone(timeout_result, "on_timeout should return a transition")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class StateMachineTestCase(EnokiTestCase):
|
|
332
|
+
"""Extended test case for testing full StateMachine behavior."""
|
|
333
|
+
|
|
334
|
+
def assertFSMTransition(
|
|
335
|
+
self,
|
|
336
|
+
initial_state,
|
|
337
|
+
expected_state,
|
|
338
|
+
messages=None,
|
|
339
|
+
error_state=None,
|
|
340
|
+
**sm_kwargs,
|
|
341
|
+
):
|
|
342
|
+
"""Assert that an FSM transitions from initial to expected state."""
|
|
343
|
+
sm = StateMachine(initial_state, error_state=error_state, **sm_kwargs)
|
|
344
|
+
|
|
345
|
+
# Send messages if provided
|
|
346
|
+
if messages:
|
|
347
|
+
for msg in messages:
|
|
348
|
+
sm.send_message(msg)
|
|
349
|
+
|
|
350
|
+
# Run one tick
|
|
351
|
+
try:
|
|
352
|
+
sm.tick()
|
|
353
|
+
except BlockedInUntimedState:
|
|
354
|
+
# This might be expected for certain tests
|
|
355
|
+
pass
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# Log the error but continue with assertion
|
|
358
|
+
print(f"FSM tick error: {e}")
|
|
359
|
+
|
|
360
|
+
if isinstance(expected_state, type) and issubclass(expected_state, State):
|
|
361
|
+
expected_name = expected_state.name
|
|
362
|
+
actual_name = sm.current_state.name
|
|
363
|
+
self.assertEqual(
|
|
364
|
+
actual_name,
|
|
365
|
+
expected_name,
|
|
366
|
+
f"Expected FSM to be in {expected_name}, " f"but it's in {actual_name}",
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
self.assertEqual(sm.current_state, expected_state)
|
|
370
|
+
|
|
371
|
+
def assertFSMCompletes(self, initial_state, messages=None, **sm_kwargs):
|
|
372
|
+
"""Assert that an FSM reaches a terminal state."""
|
|
373
|
+
sm = StateMachine(initial_state, **sm_kwargs)
|
|
374
|
+
|
|
375
|
+
# Send messages if provided
|
|
376
|
+
if messages:
|
|
377
|
+
for msg in messages:
|
|
378
|
+
sm.send_message(msg)
|
|
379
|
+
|
|
380
|
+
# Run until terminal or max iterations
|
|
381
|
+
max_iterations = 100
|
|
382
|
+
with self.assertRaises(StateMachineComplete):
|
|
383
|
+
sm.run(max_iterations)
|
|
384
|
+
|
|
385
|
+
self.assertTrue(
|
|
386
|
+
sm.current_state.CONFIG.terminal, "FSM should reach terminal state"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def assertFSMBlocks(self, initial_state, messages=None, **sm_kwargs):
|
|
390
|
+
"""Assert that an FSM blocks in a non-dwelling, non-timeout state."""
|
|
391
|
+
sm = StateMachine(initial_state, **sm_kwargs)
|
|
392
|
+
|
|
393
|
+
# Send messages if provided
|
|
394
|
+
if messages:
|
|
395
|
+
for msg in messages:
|
|
396
|
+
sm.send_message(msg)
|
|
397
|
+
|
|
398
|
+
with self.assertRaises(BlockedInUntimedState):
|
|
399
|
+
sm.tick()
|
|
400
|
+
|
|
401
|
+
def assertFSMStackState(self, sm, expected_stack):
|
|
402
|
+
"""Assert that the FSM's state stack matches expected states."""
|
|
403
|
+
actual_stack = [
|
|
404
|
+
s.name if hasattr(s, "name") else str(s) for s in sm.state_stack
|
|
405
|
+
]
|
|
406
|
+
expected_names = [
|
|
407
|
+
s.name if hasattr(s, "name") else str(s) for s in expected_stack
|
|
408
|
+
]
|
|
409
|
+
self.assertEqual(
|
|
410
|
+
actual_stack,
|
|
411
|
+
expected_names,
|
|
412
|
+
f"Expected stack {expected_names}, got {actual_stack}",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def assertValidationError(self, initial_state, error_state=None, **sm_kwargs):
|
|
416
|
+
"""Assert that StateMachine construction raises ValidationError."""
|
|
417
|
+
with self.assertRaises(ValidationError):
|
|
418
|
+
StateMachine(initial_state, error_state=error_state, **sm_kwargs)
|
|
419
|
+
|
|
420
|
+
def assertFSMAnalysis(self, sm, expected_states=None, expected_groups=None):
|
|
421
|
+
"""Assert that FSM analysis produces expected results."""
|
|
422
|
+
if expected_states:
|
|
423
|
+
discovered = sm.discovered_states
|
|
424
|
+
expected_names = {
|
|
425
|
+
s.name if hasattr(s, "name") else str(s) for s in expected_states
|
|
426
|
+
}
|
|
427
|
+
self.assertEqual(
|
|
428
|
+
discovered,
|
|
429
|
+
expected_names,
|
|
430
|
+
f"Expected states {expected_names}, got {discovered}",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if expected_groups:
|
|
434
|
+
groups = sm.state_groups
|
|
435
|
+
self.assertEqual(
|
|
436
|
+
set(groups.keys()),
|
|
437
|
+
set(expected_groups),
|
|
438
|
+
f"Expected groups {expected_groups}, got {list(groups.keys())}",
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# Test Utilities for Complex Scenarios
|
|
443
|
+
class MockGeventQueue:
|
|
444
|
+
"""Mock gevent queue for testing without actual gevent operations."""
|
|
445
|
+
|
|
446
|
+
def __init__(self):
|
|
447
|
+
self._items = []
|
|
448
|
+
|
|
449
|
+
def put(self, item):
|
|
450
|
+
self._items.append(item)
|
|
451
|
+
|
|
452
|
+
def get(self, timeout=None):
|
|
453
|
+
if not self._items:
|
|
454
|
+
if timeout is None:
|
|
455
|
+
raise Exception("Queue empty")
|
|
456
|
+
else:
|
|
457
|
+
raise Exception("Timeout")
|
|
458
|
+
return self._items.pop(0)
|
|
459
|
+
|
|
460
|
+
def get_nowait(self):
|
|
461
|
+
if not self._items:
|
|
462
|
+
raise Exception("Queue empty")
|
|
463
|
+
return self._items.pop(0)
|
|
464
|
+
|
|
465
|
+
def empty(self):
|
|
466
|
+
return len(self._items) == 0
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def create_test_state_machine(initial_state, **kwargs):
|
|
470
|
+
"""
|
|
471
|
+
Create a StateMachine suitable for testing, with mocked gevent components if needed.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
initial_state: Initial state class
|
|
475
|
+
**kwargs: Additional arguments for StateMachine
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
StateMachine configured for testing
|
|
479
|
+
"""
|
|
480
|
+
# In real tests, we'll use the actual StateMachine
|
|
481
|
+
# This helper is for any special test configuration needed
|
|
482
|
+
return StateMachine(initial_state, **kwargs)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def run_fsm_scenario(
|
|
486
|
+
initial_state, messages=None, max_iterations=10, timeout=1, **sm_kwargs
|
|
487
|
+
):
|
|
488
|
+
"""
|
|
489
|
+
Run a complete FSM scenario and return execution trace.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
initial_state: Initial state class
|
|
493
|
+
messages: List of messages to send
|
|
494
|
+
max_iterations: Maximum iterations to run
|
|
495
|
+
**sm_kwargs: StateMachine constructor arguments
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
Dict with execution results: {
|
|
499
|
+
'final_state': final_state_class,
|
|
500
|
+
'states_visited': [state_names],
|
|
501
|
+
'completed': bool,
|
|
502
|
+
'error': exception_or_none
|
|
503
|
+
}
|
|
504
|
+
"""
|
|
505
|
+
sm = StateMachine(initial_state, **sm_kwargs)
|
|
506
|
+
states_visited = [sm.current_state.name]
|
|
507
|
+
error = None
|
|
508
|
+
completed = False
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
# Send all messages
|
|
512
|
+
if messages:
|
|
513
|
+
for msg in messages:
|
|
514
|
+
sm.send_message(msg)
|
|
515
|
+
|
|
516
|
+
sm.run(max_iterations=max_iterations, timeout=timeout)
|
|
517
|
+
except StateMachineComplete:
|
|
518
|
+
completed = True
|
|
519
|
+
except Exception as e:
|
|
520
|
+
error = e
|
|
521
|
+
print(traceback.format_exc())
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
"final_state": sm.current_state,
|
|
525
|
+
"completed": completed,
|
|
526
|
+
"error": error,
|
|
527
|
+
"stack_depth": len(sm.state_stack),
|
|
528
|
+
"ctx": sm.context,
|
|
529
|
+
}
|