97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""
|
|
Tests for SimulationManager lifecycle hooks (on_ready / on_completed).
|
|
|
|
NOTE ON SHAPE DIVERGENCE vs. original plan spec:
|
|
- SimulationState uses `simulation_id` (not `sim_id`)
|
|
- `status` is a SimulationStatus enum, not a plain string
|
|
- The COMPLETED transition lives in simulation_runner.py (SimulationRunner._monitor_simulation),
|
|
not in simulation_manager.py. The _notify_on_completed hook is registered on SimulationManager
|
|
and the production insertion point for COMPLETED is documented in DONE_WITH_CONCERNS.
|
|
|
|
Hooks are stored on the class (C3 fix), so each test snapshots/restores the
|
|
registries via the autouse fixture to keep test isolation.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.services.simulation_manager import SimulationManager, SimulationState, SimulationStatus
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_class_hooks():
|
|
saved_ready = list(SimulationManager._on_ready_hooks)
|
|
saved_completed = list(SimulationManager._on_completed_hooks)
|
|
try:
|
|
yield
|
|
finally:
|
|
SimulationManager._on_ready_hooks[:] = saved_ready
|
|
SimulationManager._on_completed_hooks[:] = saved_completed
|
|
|
|
|
|
def test_register_post_ready_hook_invoked():
|
|
called = []
|
|
mgr = SimulationManager()
|
|
mgr.register_on_ready(lambda state: called.append(("ready", state.simulation_id)))
|
|
state = SimulationState(
|
|
simulation_id="abc",
|
|
project_id="proj1",
|
|
graph_id="graph1",
|
|
status=SimulationStatus.READY,
|
|
)
|
|
mgr._notify_on_ready(state)
|
|
assert called == [("ready", "abc")]
|
|
|
|
|
|
def test_register_post_completed_hook_invoked():
|
|
called = []
|
|
mgr = SimulationManager()
|
|
mgr.register_on_completed(lambda state: called.append(("done", state.simulation_id)))
|
|
state = SimulationState(
|
|
simulation_id="abc",
|
|
project_id="proj1",
|
|
graph_id="graph1",
|
|
status=SimulationStatus.COMPLETED,
|
|
)
|
|
mgr._notify_on_completed(state)
|
|
assert called == [("done", "abc")]
|
|
|
|
|
|
def test_hooks_survive_across_instances():
|
|
"""C3: hook registries are class-level, so callbacks registered through the
|
|
classmethod must still fire on a freshly constructed instance. This is
|
|
what makes the Flask per-request ``SimulationManager()`` pattern work
|
|
after ``install_hooks(SimulationManager)`` runs at app startup.
|
|
"""
|
|
called: list[str] = []
|
|
|
|
# Register via the class — the production install_hooks(cls) path.
|
|
SimulationManager.register_on_ready(lambda s: called.append(f"ready:{s.simulation_id}"))
|
|
SimulationManager.register_on_completed(lambda s: called.append(f"done:{s.simulation_id}"))
|
|
|
|
# New, independently-constructed instance must still see the hooks.
|
|
fresh = SimulationManager()
|
|
state = SimulationState(
|
|
simulation_id="cross_instance",
|
|
project_id="p",
|
|
graph_id="g",
|
|
status=SimulationStatus.READY,
|
|
)
|
|
fresh._notify_on_ready(state)
|
|
state.status = SimulationStatus.COMPLETED
|
|
fresh._notify_on_completed(state)
|
|
|
|
assert "ready:cross_instance" in called
|
|
assert "done:cross_instance" in called
|
|
|
|
|
|
def test_register_via_instance_also_lands_on_class():
|
|
"""Registering through an instance must populate the class registry too —
|
|
backward-compatibility with code that calls ``manager.register_on_*``.
|
|
"""
|
|
mgr1 = SimulationManager()
|
|
mgr1.register_on_ready(lambda s: None)
|
|
# A second, unrelated instance must see the hook.
|
|
mgr2 = SimulationManager()
|
|
assert len(SimulationManager._on_ready_hooks) >= 1
|
|
assert SimulationManager._on_ready_hooks is mgr2.__class__._on_ready_hooks
|