diff --git a/dimos/core/test_native_module.py b/dimos/core/test_native_module.py index e77b8f9a5..5811da4b0 100644 --- a/dimos/core/test_native_module.py +++ b/dimos/core/test_native_module.py @@ -18,8 +18,10 @@ The echo script writes received CLI args to a temp file for assertions. """ +from collections.abc import Generator import json from pathlib import Path +import threading import time import pytest @@ -90,22 +92,35 @@ def start(self) -> None: pass -def test_process_crash_triggers_stop() -> None: - """When the native process dies unexpectedly, the watchdog calls stop().""" +@pytest.fixture +def crash_module() -> Generator[StubNativeModule, None, None]: + """Create a StubNativeModule that dies after 0.2s, ensuring cleanup.""" + before = {t.ident for t in threading.enumerate()} mod = StubNativeModule(die_after=0.2) - mod.pointcloud.transport = LCMTransport("/pc", PointCloud2) - mod.start() + yield mod + # The watchdog calls stop() from its own thread, which sets + # _module_closed=True. A second stop() from here is then a no-op, + # so we explicitly join any threads the test created. + for t in threading.enumerate(): + if t.ident not in before and t is not threading.current_thread(): + t.join(timeout=5) + + +def test_process_crash_triggers_stop(crash_module: StubNativeModule) -> None: + """When the native process dies unexpectedly, the watchdog calls stop().""" + crash_module.pointcloud.transport = LCMTransport("/pc", PointCloud2) + crash_module.start() - assert mod._process is not None - pid = mod._process.pid + assert crash_module._process is not None + pid = crash_module._process.pid # Wait for the process to die and the watchdog to call stop() for _ in range(30): time.sleep(0.1) - if mod._process is None: + if crash_module._process is None: break - assert mod._process is None, f"Watchdog did not clean up after process {pid} died" + assert crash_module._process is None, f"Watchdog did not clean up after process {pid} died" @pytest.mark.slow