"""How long an overlay-base failure lasts: one frame, or the whole session. R2 of ``docs/test-repair-rework.zh.md``. The second round made every failed projection sticky, so one bad finger frame cost that result its overlay view for the session — a behaviour change beyond the box case it was written for. The scope is now the projector's own declaration (``overlay_base.OverlayBaseProjector``), never a guess made from a ``None`` or from an exception's text: * a **finger** unroll that loses a frame is transient — the next frame is tried and the session is *not* marked unavailable; * a **box** stitch that cannot rebuild the lenslet grid on this footage is permanent — the session records it and stops asking, so scrubbing does not rebuild the same failure on every cursor move; * a projector that cannot be **opened** at all is permanent, because no frame can work. No SDK and no recording are involved: the projector is given a stand-in ``project``, and the session is handed that projector directly, so these run on any machine. """ from __future__ import annotations from pathlib import Path from types import SimpleNamespace import numpy as np import pytest from orisys_app_shared.analysis.model import Provenance from orisys_app_shared.analysis.overlay_base import ( OverlayBaseProjector, projection_failures_are_permanent, ) from orisys_studio.replay_analysis import AnalysisSession FRAME = np.zeros((6, 6, 3), dtype=np.uint8) BASE = np.full((6, 6, 3), 7, dtype=np.uint8) class _Reader: """The smallest reader ``AnalysisSession`` accepts, with no result behind it.""" complete = True key = "overlay-scope" directory = Path(".") frame_ids = np.array([1, 2], dtype=np.int64) times = np.array([0.0, 0.1]) keyframes: dict = {} provenance = Provenance() def mask(self): return None def frame(self, frame_id): return SimpleNamespace(depth=np.zeros((6, 6), dtype=np.float32)) def grid(self, grid_id): raise AssertionError("no grid in this stub") def _session(projector) -> AnalysisSession: """A session whose projector is supplied directly (its provenance is not used).""" session = AnalysisSession(_Reader()) session._overlay = projector return session def test_one_bad_finger_frame_does_not_cost_the_session_its_overlay(): """A finger unroll that fails on one frame is retried on the next.""" answers = [None, BASE] projector = OverlayBaseProjector( sensor_type="finger", project=lambda image: answers.pop(0), projection_failures_are_permanent=False, ) session = _session(projector) assert session.overlay_base(FRAME, frame_id=1) is None assert session.overlay_unavailable is False, "one frame made the whole result unavailable" base = session.overlay_base(FRAME, frame_id=2) assert base is not None, "the second frame was never projected" assert np.array_equal(base, BASE) assert session.overlay_unavailable is False def test_a_finger_projection_that_raises_is_also_only_that_frame(): """The exception text is not consulted: a raising finger projection is transient.""" answers = [RuntimeError("unroll miss"), BASE] def project(image): answer = answers.pop(0) if isinstance(answer, Exception): raise answer return answer session = _session( OverlayBaseProjector( sensor_type="finger", project=project, projection_failures_are_permanent=False, ) ) assert session.overlay_base(FRAME, frame_id=1) is None assert session.overlay_unavailable is False assert session.overlay_base(FRAME, frame_id=2) is not None def test_a_box_projection_that_fails_is_permanent_and_is_not_retried(): """A box stitch that fails will fail again; the session stops asking.""" calls: list[int] = [] def project(image): calls.append(1) return None session = _session(OverlayBaseProjector(sensor_type="box", project=project)) assert session.overlay_base(FRAME, frame_id=1) is None assert session.overlay_unavailable is True assert session.overlay_base(FRAME, frame_id=2) is None assert len(calls) == 1, "a permanent failure was retried on the next frame" def test_a_projector_that_cannot_open_is_permanent(monkeypatch): """No projector at all is the other permanent case: no frame could ever work.""" calls: list[int] = [] @classmethod def _cannot_open(cls, provenance): calls.append(1) return None monkeypatch.setattr(OverlayBaseProjector, "open_from_provenance", _cannot_open) session = AnalysisSession(_Reader()) assert session.overlay_base(FRAME, frame_id=1) is None assert session.overlay_unavailable is True assert session.overlay_base(FRAME, frame_id=2) is None assert len(calls) == 1, "opening was retried after it had already failed" @pytest.mark.parametrize( ("sensor_type", "permanent"), (("finger", False), ("box", True)), ) def test_the_sensor_decides_which_kind_of_failure_it_has(sensor_type, permanent): """The policy lives with the sensor's opener, not in each caller's judgement.""" assert projection_failures_are_permanent(sensor_type) is permanent