"""T08a: single-stream entry — real-time latest, ownership, combined zero. The facade reuses StreamPipeline for threading/version resets and the accepted ForceStreamSession for force processing. These tests cover the T08a scope only: real-time candidate overwrite semantics, borrowed vs owned source lifetime, and the combined "image reset → same-version unloaded baseline → force ready" entry. File/debug complete processing and source-time provenance are T08b. """ from __future__ import annotations import threading import time import numpy as np import pytest from orisys.sensor import Sensor from orisys.sensor_stream import SensorStream, SensorStreamError SIZE = 365 def _sensor() -> Sensor: return Sensor( "external", config_name="box", verbose=False, backend="cpu", cuda=False, mirror=False, ) def _frame(value: float = 40.0) -> np.ndarray: return np.full((SIZE, SIZE), value, dtype=np.uint8) def _stream(sensor: Sensor, **kwargs) -> SensorStream: options = {"sensor_id": "BOX-1", "session_id": "run-1"} options.update(kwargs) return SensorStream(sensor, **options) def test_realtime_candidate_is_latest_and_overwrites_are_counted() -> None: sensor = _sensor() stream = _stream(sensor) try: for index in range(10): stream.push_frame( _frame(40.0 + index), source_frame_id=index, capture_time_ns=1_000_000_000 + index * 33_000_000, ) assert stream.overwritten_frames == 9, "unclaimed candidates are replaced" stream.start() output = stream.wait_output(timeout=5.0) assert output is not None # The producer claimed the newest candidate, not the first pushed frame. assert output.frame.stamp.source_frame_id == 9 assert output.frame.stamp.frame_id == 0, "output frame ids stay contiguous" assert output.frame.reference_generation == 0 finally: stream.stop() def test_borrowed_sensor_is_not_closed_by_stop() -> None: sensor = _sensor() disconnected = [] original = sensor.disconnect def tracked_disconnect() -> None: disconnected.append(True) original() sensor.disconnect = tracked_disconnect # type: ignore[method-assign] stream = _stream(sensor) stream.start() stream.push_frame(_frame(), source_frame_id=0) assert stream.wait_output(timeout=5.0) is not None stream.stop() assert disconnected == [], "a borrowed source belongs to the caller" assert stream.running is False def test_owned_sensor_is_closed_by_stop_and_threads_finish() -> None: sensor = _sensor() disconnected = [] original = sensor.disconnect def tracked_disconnect() -> None: disconnected.append(True) original() sensor.disconnect = tracked_disconnect # type: ignore[method-assign] stream = _stream(sensor, owns_sensor=True) stream.start() stream.push_frame(_frame(), source_frame_id=0) assert stream.wait_output(timeout=5.0) is not None threads = list(stream.pipeline._threads) stream.stop() assert disconnected == [True], "the open() owner closes its own Sensor" assert all(not thread.is_alive() for thread in threads) def test_reset_and_zero_combines_image_reset_and_same_version_baseline() -> None: sensor = _sensor() stream = _stream(sensor) stream.start() stop = threading.Event() def feeder() -> None: frame_id = 0 while not stop.is_set() and frame_id < 400: stream.push_frame( _frame(40.0 + (frame_id % 20)), source_frame_id=frame_id, capture_time_ns=1_000_000_000 + frame_id * 33_000_000, ) frame_id += 1 time.sleep(0.001) feeder_thread = threading.Thread(target=feeder) feeder_thread.start() try: time.sleep(0.1) # let some pre-reset frames flow zero = stream.reset_and_zero(reference_id="zero-1", timeout=20.0) assert zero.phase == "ready" assert zero.collected_frames == 30 output = stream.wait_output(timeout=5.0) assert output is not None assert output.zero.phase == "ready" assert output.reference_generation == stream.pipeline.tracker_reset_seq assert output.frame.stamp.session_id == "run-1" assert all(np.isfinite(value) or np.isnan(value) for value in output.newton) finally: stop.set() feeder_thread.join(timeout=5.0) stream.stop() def test_reset_and_zero_requires_a_running_stream() -> None: sensor = _sensor() stream = _stream(sensor) with pytest.raises(SensorStreamError, match="start the stream"): stream.reset_and_zero(reference_id="zero-1") def test_frames_already_started_are_not_interrupted_by_newer_input() -> None: sensor = _sensor() stream = _stream(sensor) try: stream.start() stream.push_frame( _frame(50.0), source_frame_id=0, capture_time_ns=1_000_000_000 ) first = stream.wait_output(timeout=5.0) assert first is not None and first.frame.stamp.source_frame_id == 0 stream.push_frame( _frame(80.0), source_frame_id=1, capture_time_ns=1_033_000_000 ) second = stream.wait_output(timeout=5.0) assert second is not None and second.frame.stamp.source_frame_id == 1 assert second.frame.stamp.frame_id == first.frame.stamp.frame_id + 1 # Both finished frames are present; a claimed frame was not replaced. assert second.frame.stamp.frame_id == 1 finally: stream.stop() # --------------------------------------------- Z1: one command, its own result def _start_feeder(stream: SensorStream, stop: threading.Event) -> threading.Thread: def feeder() -> None: frame_id = 0 while not stop.is_set(): stream.push_frame( _frame(40.0 + (frame_id % 20)), source_frame_id=frame_id, capture_time_ns=1_000_000_000 + frame_id * 33_000_000, ) frame_id += 1 time.sleep(0.002) thread = threading.Thread(target=feeder, daemon=True) thread.start() return thread def test_two_reset_and_zero_calls_each_return_their_own_result() -> None: sensor = _sensor() stream = _stream(sensor) stream.start() stop = threading.Event() feeder = _start_feeder(stream, stop) try: first = stream.reset_and_zero(reference_id="first", timeout=20.0) assert first.phase == "ready" and first.reference_id == "first" first_version = stream.session.reference_generation second = stream.reset_and_zero(reference_id="second", timeout=20.0) assert second.phase == "ready", "the second call must wait for its own zero" assert second.reference_id == "second" assert stream.session.zero.reference_id == "second" assert stream.session.reference_generation > first_version # A frame published after the second command carries its target version. stream.push_frame( _frame(70.0), source_frame_id=10_000, capture_time_ns=9_000_000_000 ) output = stream.wait_output(timeout=5.0) assert output is not None assert output.reference_generation == stream.session.reference_generation assert output.zero.phase == "ready" assert output.zero.reference_id == "second" finally: stop.set() feeder.join(timeout=5.0) stream.stop() def test_second_reset_and_zero_succeeds_after_a_failed_first() -> None: sensor = _sensor() stream = _stream(sensor) stream.start() stop = threading.Event() feeder = _start_feeder(stream, stop) try: with pytest.raises(SensorStreamError): stream.reset_and_zero(reference_id="bad", timeout=10.0, max_window_s=0.001) second = stream.reset_and_zero(reference_id="second", timeout=20.0) assert second.phase == "ready" assert second.reference_id == "second" finally: stop.set() feeder.join(timeout=5.0) stream.stop() # ------------------------------------- Z2: borrowed Sensor session mode restored def test_borrowed_sensor_measurement_session_is_restored_on_stop() -> None: sensor = _sensor() stream = _stream(sensor) try: assert sensor._measurement_session is False stream.start() assert sensor._measurement_session is True, ( "the stream enables the session seam" ) stream.stop() assert sensor._measurement_session is False, ( "borrowed Sensor mode must be restored" ) finally: if stream.running: stream.stop() def test_borrowed_sensor_already_in_session_mode_stays_enabled() -> None: sensor = _sensor() sensor.enable_measurement_session() stream = _stream(sensor) try: stream.start() stream.stop() assert sensor._measurement_session is True, "the pre-stream mode is preserved" finally: sensor.disable_measurement_session() if stream.running: stream.stop() def test_failed_start_restores_the_borrowed_sensor_session(monkeypatch) -> None: sensor = _sensor() stream = _stream(sensor) assert sensor._measurement_session is False def broken_start() -> None: raise RuntimeError("pipeline start failed") monkeypatch.setattr(stream.pipeline, "start", broken_start) with pytest.raises(RuntimeError, match="pipeline start failed"): stream.start() assert sensor._measurement_session is False, "a failed start must restore the mode"