Files
orisys-app/apps/studio/tests/test_session_controller.py
2026-09-27 19:20:43 +08:00

222 lines
8.1 KiB
Python

"""Acceptance for the acquisition session's state and its UI-free mechanics.
``SessionController`` owns which worker runs, in which phase, and what the session
reports (frame rate, cost, capture rate and drops, how many readouts failed). It touches
no widget, so it can be driven directly — which is what these tests do, with a fake
recording coordinator and fake workers.
"""
from __future__ import annotations
import pytest
from orisys_studio import session_controller
from orisys_studio.session_controller import SessionController
class _Recording:
"""The record channel as the controller uses it (no files, no Qt)."""
def __init__(self) -> None:
self.packets: list = []
self.calibrations: list = []
self.resets: list = []
self.invalid = 0
def on_packet(self, packet):
self.packets.append(packet)
return type("Report", (), {"valid": packet != "invalid"})()
def note_calibration_applied(self, path, version, frame_id, mono_t, session) -> None:
self.calibrations.append((path, version, frame_id, mono_t, session))
def note_reset(self, frame_id, mono_t, session) -> None:
self.resets.append((frame_id, mono_t, session))
class _Worker:
def __init__(self, **kwargs) -> None:
self.kwargs = dict(kwargs)
self.sinks: list = []
self.running = False
def add_sink(self, sink) -> None:
if sink not in self.sinks:
self.sinks.append(sink)
def remove_sink(self, sink) -> None:
self.sinks.remove(sink)
@pytest.fixture
def workers(monkeypatch):
"""Replace both worker classes with recording fakes."""
import orisys_app_shared.acquisition.finger_worker as finger_module
import orisys_app_shared.acquisition.sdk_worker as sdk_module
created: list[_Worker] = []
def factory(**kwargs):
worker = _Worker(**kwargs)
created.append(worker)
return worker
monkeypatch.setattr(sdk_module, "SdkAcquisitionWorker", factory)
monkeypatch.setattr(finger_module, "FingerAcquisitionWorker", factory)
return created
def _controller(recording=None, **kwargs):
recording = recording or _Recording()
controller = SessionController(
recording=recording, packet_for_3d=lambda packet: None, **kwargs
)
return controller, recording
# --------------------------------------------------------------------- tests
def test_a_confirmed_zeroing_reaches_the_record(qt_app, workers):
"""The device's own "the zeroing took effect" must not be dropped on the floor.
Nothing was wired to it before: the reset request went out, the confirmation came
back, and the session recorded no event for it.
"""
controller, recording = _controller()
controller.ensure_workers()
assert len(workers) == 2, "both workers are created (the sensor can change)"
for worker in workers:
callback = worker.kwargs.get("on_reset_applied")
assert callable(callback), "the worker was not told where to report a zeroing"
callback(7, 1.5, 3)
assert recording.resets == [(7, 1.5, 3), (7, 1.5, 3)]
def test_a_confirmed_calibration_reaches_the_record(qt_app, workers):
controller, recording = _controller()
controller.ensure_workers()
for worker in workers:
worker.kwargs["on_calibration_applied"]("/tmp/cal.json", "abc123", 4, 0.2, 1)
assert recording.calibrations == [
("/tmp/cal.json", "abc123", 4, 0.2, 1),
("/tmp/cal.json", "abc123", 4, 0.2, 1),
]
def test_the_record_channel_is_a_sink_on_both_workers(qt_app, workers):
controller, recording = _controller()
controller.ensure_workers()
for worker in workers:
# Without a window-provided sink, the controller's own method is registered.
assert controller.note_packet_recorded in worker.sinks
assert len(worker.sinks) == 2, "the 3D pane is a second sink on the same thread"
# A failed readout is counted; a good one is not.
controller.note_packet_recorded("invalid")
controller.note_packet_recorded("fine")
assert controller.record_error_count == 1
assert recording.packets == ["invalid", "fine"]
controller.detach_sinks()
for worker in workers:
assert worker.sinks == []
def test_a_run_connects_and_then_reports_its_own_telemetry(qt_app, workers):
controller, recording = _controller()
controller.ensure_workers()
controller.reset_for_run(source="/dev/video0", sensor_type="box")
first = controller.session_counter
assert controller.phase == "idle" and not controller.is_running
controller.mark_running()
assert controller.is_running and controller.phase == "connecting"
assert controller.run_timer.isActive(), "the run timer is the session's cadence"
packet = type(
"Packet",
(),
{"fps": 16.7, "process_ms": 58.0, "capture_fps": 30.0, "capture_dropped": 2},
)()
controller.note_sample_telemetry(packet)
assert (controller.latest_fps, controller.latest_process_ms) == (16.7, 58.0)
assert controller.latest_capture_fps == 30.0
assert controller.latest_capture_dropped == 2
assert controller.note_first_frame() is True
assert controller.phase == "running"
assert controller.note_first_frame() is False, "the phase change happens once"
assert controller.mark_stopped("user_stop") is False
assert not controller.is_running and controller.phase == "idle"
assert not controller.run_timer.isActive()
# The telemetry of a session that is over must not be readable as this one's.
assert controller.latest_fps is None and controller.latest_capture_fps == 0.0
assert controller.last_sample_valid is None
controller.reset_for_run(source="/dev/video0", sensor_type="box")
assert controller.session_counter == first + 1, "a new run is a new session id"
def test_a_stop_while_connecting_says_so(qt_app, workers):
"""The caller uses this to tell "still releasing the device" from "already idle"."""
controller, recording = _controller()
controller.ensure_workers()
controller.mark_running()
assert controller.mark_stopped("user_stop") is True
controller.mark_running()
assert controller.note_first_frame() is True
assert controller.mark_stopped("failed") is False
assert controller.phase == "error"
def test_the_slow_connect_hint_is_said_once(qt_app, monkeypatch, workers):
controller, recording = _controller()
controller.ensure_workers()
clock = {"now": 100.0}
monkeypatch.setattr(session_controller.time, "perf_counter", lambda: clock["now"])
controller.mark_running()
clock["now"] = 100.0 + session_controller.CONNECT_SLOW_HINT_S - 0.1
assert controller.note_connect_slow() is False
clock["now"] = 100.0 + session_controller.CONNECT_SLOW_HINT_S
assert controller.note_connect_slow() is True
assert controller.note_connect_slow() is False, "the hint was repeated"
def test_a_dropped_batch_is_reported_once(qt_app, workers):
controller, recording = _controller()
assert controller.take_dropped_message(0) is None
first = controller.take_dropped_message(3)
assert first is not None and "3" in first
assert controller.take_dropped_message(3) is None, "the same batch was reported twice"
assert controller.take_dropped_message(5) is not None
def test_a_capture_shortfall_is_judged_only_with_a_measured_rate(
qt_app, monkeypatch, workers
):
"""The measured rate is the evidence; the driver's own report is an echo."""
monkeypatch.setattr(
session_controller.session_metadata,
"capture_metadata",
lambda worker: {"requested_fps": 30.0, "negotiated_fps": 30.0},
)
controller, recording = _controller()
controller.ensure_workers()
assert controller.check_capture_negotiation() is None, "nothing measured yet"
assert controller.capture_checked is False, "it decided without evidence"
controller.note_sample_telemetry(
type("Packet", (), {"capture_fps": 15.0, "capture_dropped": 0})()
)
controller.check_capture_negotiation()
assert controller.capture_checked is True, "the check never latched with evidence"
assert controller.check_capture_negotiation() is None, "it is said once per session"