Lint the commits being pushed and scope overlay failures per sensor
The pre-push gate linted only the dirty tree, so the commits about to be pushed went unchecked; and every failed overlay projection was sticky, which cost a finger result its overlay over one bad frame while box results were retried nowhere. gate.ps1 now takes -LintBase (default origin/main, exit 2 on an unknown ref), the projector declares whether its own failures are permanent, and serve_stored_view decides before it sets the view. Verified with gate.ps1 (PASS: 569 studio + 485 shared tests, 632 ui_check checks) and the two round-2 red samples kept in scratch.
This commit is contained in:
@@ -11,15 +11,22 @@ generated pixels, and a synthetic 896² clip sent the chain into minutes of work
|
||||
| | |
|
||||
|---|---|
|
||||
| Sensor | `box` (shipped bundle) |
|
||||
| Source | `dist/OrisysStudio/outputs/recordings/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011`, recorded 2026-09-30, `session_id` 2 |
|
||||
| Source | [`.scratch/result-visible/recording/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011/`](../../../../.scratch/result-visible/recording/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011/) — recorded 2026-09-30, `session_id` 2. The directory name is the recording's own identity, so it matches the `dist/` path below. |
|
||||
| Length | 33 frames, frame ids **20…52**, 1.92 s of signal time |
|
||||
| Files | `raw_0001.mp4`, `frames.csv`, `forces.csv`, `events.csv`, `session.json` (3.8 MB) |
|
||||
| Not included | `preroll_0001.mp4` (3.5 MB, optional for every reader: `RecordingReader.preroll_segment()` answers `None` when the file is absent) |
|
||||
|
||||
`dist/` is build output and is not tracked, so the copy under it disappeared on
|
||||
2026-10-14 when the studio was repackaged. That took 11 tests to `ERROR` and 4 to
|
||||
`failed` without anyone noticing — see
|
||||
[`docs/test-repair-plan.zh.md`](../../../docs/test-repair-plan.zh.md) §1 and §3 #1–#2.
|
||||
`dist/` is build output and is not tracked, so the copy under `dist/OrisysStudio/outputs/recordings/` disappeared on 2026-10-14 when the studio was repackaged. That took 11 tests to `ERROR` and 4 to `failed` without anyone noticing — see [`docs/test-repair-plan.zh.md`](../../../docs/test-repair-plan.zh.md) §1 and §3 #1–#2.
|
||||
|
||||
The `dist/` original is **gone**, so the files here cannot be compared against it byte for byte. They are byte-identical to the copy preserved under `.scratch/result-visible/` (the source named above), which is the strongest provenance available. The hashes are recorded here so a future copy can be checked against this one instead of argued about:
|
||||
|
||||
| File | SHA256 |
|
||||
|---|---|
|
||||
| `raw_0001.mp4` | `907867d0fe82966dbaacf97a8bb0a8d5647de25458ad5fd4640535d83d417207` |
|
||||
| `frames.csv` | `fab25e6780908aef8f7616504b43cf34062ab7ca3ba317b6ff4c0631969e0601` |
|
||||
| `forces.csv` | `972d3c951ff0be847a6a224b53339401f115cd3728039f1acb24a33bf215ecb1` |
|
||||
| `events.csv` | `6fe7121b1c621ebed472ee42376babfd76a32a364ab9f11660da077fb365a946` |
|
||||
| `session.json` | `f3c9afc4502180d2a88cd260cb70f1f4313e203f2c3c8441704de6ac53a1db4c` |
|
||||
|
||||
The frame ids are deliberately not 0…32: a test that used the frame *index* would pass on
|
||||
a 0-based recording, and real recordings start wherever the device's counter was.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""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
|
||||
@@ -772,3 +772,61 @@ def test_a_bare_video_with_a_declined_prompt_promises_no_pass(
|
||||
)
|
||||
assert t(RAW_NOTE_KEY) not in window.arrow_panel.note_label.text()
|
||||
assert plan_image(captured[-1]).note_key == "panels.replay_analysis_failed"
|
||||
|
||||
|
||||
def test_adopting_a_box_result_never_flashes_the_overlay_refusal(
|
||||
window, stored_recording, monkeypatch
|
||||
):
|
||||
"""R3: pick the drawable view once, draw once, and never show the refusal on the way.
|
||||
|
||||
The box result has no stored unroll map and its stitch rebuild fails on this footage,
|
||||
so the overlay base cannot be built. The pre-rework order — set the overlay view,
|
||||
render, discover the failure, switch, render again — put
|
||||
``panels.arrow_capture_unavailable`` on screen while adopting the result, and drew the
|
||||
same frame twice. The decision is now made before the view is set, so the panel is set
|
||||
once and rendered once, and the refusal is still what the operator gets when they ask
|
||||
for the overlay themselves.
|
||||
"""
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
controller = window.replay_ctl
|
||||
refusal = t("panels.arrow_capture_unavailable")
|
||||
seen: list[str] = []
|
||||
renders: list[int] = []
|
||||
inside = {"serve": False}
|
||||
|
||||
original_render = controller.render_frame
|
||||
|
||||
def render_spy():
|
||||
if inside["serve"]:
|
||||
renders.append(1)
|
||||
original_render()
|
||||
seen.append(window.arrow_panel.image_label.text())
|
||||
else:
|
||||
original_render()
|
||||
|
||||
original_serve = controller.serve_stored_view
|
||||
|
||||
def serve_spy():
|
||||
inside["serve"] = True
|
||||
try:
|
||||
return original_serve()
|
||||
finally:
|
||||
inside["serve"] = False
|
||||
|
||||
monkeypatch.setattr(controller, "render_frame", render_spy)
|
||||
monkeypatch.setattr(controller, "serve_stored_view", serve_spy)
|
||||
|
||||
_open(window, stored_recording, monkeypatch)
|
||||
|
||||
assert inside["serve"] is False, "adoption never reached the view it serves"
|
||||
assert len(renders) == 1, f"adoption rendered the frame {len(renders)} times, not once"
|
||||
assert refusal not in seen, "the overlay refusal was shown while adopting the result"
|
||||
assert controller._image_panel.view_mode() == ARROW_PURE, (
|
||||
"a result whose overlay cannot be drawn was adopted on the overlay view"
|
||||
)
|
||||
|
||||
# The overlay is still on offer, and choosing it still says why it cannot be drawn.
|
||||
controller._image_panel.set_view_mode(ARROW_STITCHED)
|
||||
controller.render_frame()
|
||||
assert window.arrow_panel.image_label.text() == refusal
|
||||
|
||||
Reference in New Issue
Block a user