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.
Files changed (37) hide show
  1. mycorrhizal/__init__.py +3 -0
  2. mycorrhizal/common/__init__.py +68 -0
  3. mycorrhizal/common/interface_builder.py +203 -0
  4. mycorrhizal/common/interfaces.py +412 -0
  5. mycorrhizal/common/timebase.py +99 -0
  6. mycorrhizal/common/wrappers.py +532 -0
  7. mycorrhizal/enoki/__init__.py +0 -0
  8. mycorrhizal/enoki/core.py +1545 -0
  9. mycorrhizal/enoki/testing_utils.py +529 -0
  10. mycorrhizal/enoki/util.py +220 -0
  11. mycorrhizal/hypha/__init__.py +0 -0
  12. mycorrhizal/hypha/core/__init__.py +107 -0
  13. mycorrhizal/hypha/core/builder.py +404 -0
  14. mycorrhizal/hypha/core/runtime.py +890 -0
  15. mycorrhizal/hypha/core/specs.py +234 -0
  16. mycorrhizal/hypha/util.py +38 -0
  17. mycorrhizal/rhizomorph/README.md +220 -0
  18. mycorrhizal/rhizomorph/__init__.py +0 -0
  19. mycorrhizal/rhizomorph/core.py +1729 -0
  20. mycorrhizal/rhizomorph/util.py +45 -0
  21. mycorrhizal/spores/__init__.py +124 -0
  22. mycorrhizal/spores/cache.py +208 -0
  23. mycorrhizal/spores/core.py +419 -0
  24. mycorrhizal/spores/dsl/__init__.py +48 -0
  25. mycorrhizal/spores/dsl/enoki.py +514 -0
  26. mycorrhizal/spores/dsl/hypha.py +399 -0
  27. mycorrhizal/spores/dsl/rhizomorph.py +351 -0
  28. mycorrhizal/spores/encoder/__init__.py +11 -0
  29. mycorrhizal/spores/encoder/base.py +42 -0
  30. mycorrhizal/spores/encoder/json.py +159 -0
  31. mycorrhizal/spores/extraction.py +484 -0
  32. mycorrhizal/spores/models.py +288 -0
  33. mycorrhizal/spores/transport/__init__.py +10 -0
  34. mycorrhizal/spores/transport/base.py +46 -0
  35. mycorrhizal-0.1.0.dist-info/METADATA +198 -0
  36. mycorrhizal-0.1.0.dist-info/RECORD +37 -0
  37. 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
+ }