"""Cancellation semantics for the acquisition workers. The GUI must never block on a device open that hangs: ``stop()`` returns immediately, the worker thread owns its own teardown, and a new session waits for the previous one *in its own thread* instead of opening the device twice. These tests drive the real worker loop with a fake ``orisys`` module, so they need no camera and no sensor bundle beyond a ``sensor.json``. """ from __future__ import annotations import sys import threading import time import types from pathlib import Path APP_ROOT = Path(__file__).resolve().parents[2] BOX_CONFIG = APP_ROOT / "apps" / "studio" / "config" / "sensors" / "box" / "sensor.json" STOP_BUDGET_S = 0.5 class FakeSensor: """Blocks in the constructor until ``release`` is set.""" def __init__(self, release: threading.Event, tracker: "OpenTracker", **kwargs): tracker.opened(self) if not release.wait(timeout=10.0): raise TimeoutError("fake sensor was never released") self.configured: dict = {} self.connected = True def configure(self, **kwargs): self.configured = kwargs def get_img(self): return None def disconnect(self): self.connected = False class OpenTracker: """Records how many devices are open at the same time.""" def __init__(self) -> None: self.lock = threading.Lock() self.open_count = 0 self.peak_open = 0 self.instances: list[FakeSensor] = [] def opened(self, sensor: FakeSensor) -> None: with self.lock: self.instances.append(sensor) self.open_count += 1 self.peak_open = max(self.peak_open, self.open_count) def closed(self, sensor: FakeSensor) -> None: with self.lock: if sensor.connected: sensor.connected = False self.open_count -= 1 def _install_fake_orisys(tracker: OpenTracker, release: threading.Event) -> object: """Replace ``orisys`` (and ``orisys.finger``) with blocking fakes.""" saved = {name: sys.modules.get(name) for name in ("orisys", "orisys.finger")} class _Sensor(FakeSensor): def __init__(self, *args, **kwargs): super().__init__(release, tracker, **kwargs) def disconnect(self): tracker.closed(self) super().disconnect() module = types.ModuleType("orisys") module.__version__ = "fake" module.Sensor = _Sensor # type: ignore[attr-defined] class _Runtime: def __init__(self, *args, **kwargs): self._door = _Sensor() self.tactile = types.SimpleNamespace(raw_sensor=None) @classmethod def open(cls, **kwargs): return cls(**kwargs) def warmup(self, **_kwargs): return None def close(self): self._door.disconnect() finger_module = types.ModuleType("orisys.finger") finger_module.FingerRuntime = _Runtime # type: ignore[attr-defined] module.finger = finger_module # type: ignore[attr-defined] sys.modules["orisys"] = module sys.modules["orisys.finger"] = finger_module return saved def _restore(saved) -> None: for name, original in saved.items(): if original is None: sys.modules.pop(name, None) else: sys.modules[name] = original def _new_worker(kind: str): from orisys_app_shared.acquisition.finger_worker import FingerAcquisitionWorker from orisys_app_shared.acquisition.sdk_worker import SdkAcquisitionWorker if kind == "finger": return FingerAcquisitionWorker() return SdkAcquisitionWorker() def _wait_until(predicate, timeout=5.0, interval=0.01) -> bool: deadline = time.perf_counter() + timeout while time.perf_counter() < deadline: if predicate(): return True time.sleep(interval) return predicate() def _stop_is_immediate_and_keeps_ownership(kind: str) -> None: tracker = OpenTracker() release = threading.Event() saved = _install_fake_orisys(tracker, release) try: worker = _new_worker(kind) worker.start("0", config_ref=BOX_CONFIG) assert _wait_until(lambda: tracker.peak_open >= 1), "session never opened the device" started = time.perf_counter() worker.stop() elapsed = time.perf_counter() - started assert elapsed < STOP_BUDGET_S, f"stop() blocked the caller for {elapsed:.2f}s" assert worker._thread is not None, "stop() abandoned a still-running thread" assert worker.consume_end_reason() == "stopped" release.set() assert _wait_until(lambda: worker._thread is None), "thread never released ownership" assert _wait_until(lambda: tracker.open_count == 0), "device was not closed" finally: release.set() _restore(saved) def test_sdk_stop_is_immediate_and_keeps_ownership(): _stop_is_immediate_and_keeps_ownership("sdk") def test_finger_stop_is_immediate_and_keeps_ownership(): _stop_is_immediate_and_keeps_ownership("finger") def test_second_session_waits_instead_of_double_opening(): tracker = OpenTracker() release = threading.Event() saved = _install_fake_orisys(tracker, release) try: worker = _new_worker("sdk") worker.start("0", config_ref=BOX_CONFIG) assert _wait_until(lambda: tracker.peak_open == 1) worker.stop() worker.start("0", config_ref=BOX_CONFIG) # user hits Run again while stuck time.sleep(0.3) assert tracker.peak_open == 1, "two sessions opened the same device at once" release.set() assert _wait_until(lambda: len(tracker.instances) == 2, timeout=5.0), ( "the second session never opened after the stale one released the device" ) assert tracker.peak_open == 1, "the device was open twice concurrently" assert tracker.open_count <= 1, "stale session kept its device open" worker.stop(wait=True) assert _wait_until(lambda: tracker.open_count == 0), "device leaked" finally: release.set() _restore(saved) def test_stop_wait_is_bounded_and_closes_the_device(): tracker = OpenTracker() release = threading.Event() saved = _install_fake_orisys(tracker, release) try: worker = _new_worker("sdk") worker.start("0", config_ref=BOX_CONFIG) assert _wait_until(lambda: tracker.peak_open == 1) release.set() started = time.perf_counter() worker.stop(wait=True) elapsed = time.perf_counter() - started assert elapsed < 5.0, f"stop(wait=True) took {elapsed:.2f}s" assert _wait_until(lambda: tracker.open_count == 0), "device was not closed" assert not worker.is_running finally: release.set() _restore(saved) if __name__ == "__main__": # pragma: no cover - manual runner failures = 0 for name, function in sorted(globals().items()): if name.startswith("test_") and callable(function): try: function() print(f" ok {name}") except AssertionError as exc: failures += 1 print(f" FAIL {name}: {exc}") except Exception as exc: # noqa: BLE001 failures += 1 print(f" ERR {name}: {type(exc).__name__}: {exc}") print(f"failures: {failures}") raise SystemExit(1 if failures else 0)