Files
ykcheungab 19426c3c4a Fix repeat ownership and close out capture evidence
Start each repeat after its own manifest entry, refuse packets from a restarted
or stopped camera, keep a simulated repeat from reading as recorded evidence,
and decide completeness from the writer report. Add the operator summary (four
files plus an atomic pointer) and read-only video review of a hold window.

Verified with the full Autocal suite (567 passed), the shared recorder/playback
files (57 passed), the lint gate and the documentation gate; the two pre-existing
panel_check layout failures also fail on a clean HEAD worktree.
2026-10-17 10:44:30 +08:00

171 lines
5.6 KiB
Python

"""A force row belongs to a frame by identity, never by its line number."""
from __future__ import annotations
import pytest
from evidence_helpers import (
BASE_NS,
force_row,
frame_row,
write_capture,
)
from orisys_autocal.frames_index import (
DUPLICATE_REASON,
joined_samples,
read_frame_index,
window_samples,
)
def _capture(tmp_path, frames, forces, **kwargs):
return write_capture(tmp_path / "capture", frames, forces, video_segments=0, **kwargs)
def test_samples_are_paired_by_frame_id_not_by_row_order(tmp_path):
frames = [
frame_row(1, BASE_NS + 10),
frame_row(2, BASE_NS + 20),
frame_row(3, BASE_NS + 30),
]
# The force rows are reversed on disk, as an out-of-order write would leave them.
forces = [force_row(3, fn=30.0), force_row(2, fn=20.0), force_row(1, fn=10.0)]
_capture(tmp_path, frames, forces)
samples, report = joined_samples(tmp_path / "capture")
assert [sample.frame_id for sample in samples] == [1, 2, 3]
assert [sample.t_host_mono_ns for sample in samples] == [
BASE_NS + 10, BASE_NS + 20, BASE_NS + 30,
]
assert [sample.value("FN") for sample in samples] == [10.0, 20.0, 30.0]
assert report.unjoined_force_rows == 0
window = window_samples(samples, BASE_NS + 15, BASE_NS + 30)
assert [sample.frame_id for sample in window] == [2]
def test_a_repeated_frame_id_excludes_the_samples_it_would_pair(tmp_path):
"""A camera that restarted reuses frame numbers; neither copy is the measurement."""
frames = [
frame_row(1, BASE_NS + 10),
frame_row(2, BASE_NS + 20),
frame_row(1, BASE_NS + 1_000),
frame_row(2, BASE_NS + 1_010),
]
forces = [
force_row(1, fn=10.0),
force_row(2, fn=20.0),
force_row(1, fn=11.0),
force_row(2, fn=21.0),
]
_capture(tmp_path, frames, forces)
samples, report = joined_samples(tmp_path / "capture")
assert samples == []
assert report.duplicate_frame_ids == 2
assert DUPLICATE_REASON in report.reasons
def test_frames_outside_the_video_are_kept_only_when_asked_for(tmp_path):
frames = [
frame_row(1, BASE_NS + 10, in_video=1),
frame_row(2, BASE_NS + 20, in_video=0),
frame_row(3, BASE_NS + 30, in_video=1),
]
forces = [force_row(1, fn=1.0), force_row(2, fn=2.0), force_row(3, fn=3.0)]
_capture(tmp_path, frames, forces)
with_video, report = joined_samples(tmp_path / "capture", require_video=True)
everything, _ = joined_samples(tmp_path / "capture")
assert [sample.frame_id for sample in with_video] == [1, 3]
assert [sample.frame_id for sample in everything] == [1, 2, 3]
assert report.frames_not_in_video == 1
def test_a_dropped_frame_row_has_no_time_to_pair_with(tmp_path):
frames = [
frame_row(1, BASE_NS + 10),
frame_row(2, 0, dropped=1),
frame_row(3, BASE_NS + 30),
]
forces = [force_row(1, fn=1.0), force_row(2, fn=2.0), force_row(3, fn=3.0)]
_capture(tmp_path, frames, forces)
samples, report = joined_samples(tmp_path / "capture")
assert [sample.frame_id for sample in samples] == [1, 3]
assert report.unjoined_force_rows == 1
assert report.frames_dropped == 1
def test_a_recording_without_an_in_video_column_still_joins(tmp_path):
"""Older recordings have no membership column; unknown is not "outside"."""
fields = [
name for name in (
"frame_id", "source_frame_id", "t_rel_s", "t_host_mono_ns", "t_host_utc_ns",
"acquired_at", "time_basis", "media_time_ms", "segment", "segment_frame",
"dropped", "capture_dropped",
)
]
frames = [
{"frame_id": 1, "t_host_mono_ns": BASE_NS + 10, "dropped": 0, "t_rel_s": "0.0"},
{"frame_id": 2, "t_host_mono_ns": BASE_NS + 20, "dropped": 0, "t_rel_s": "0.0"},
]
_capture(tmp_path, frames, [force_row(1, fn=1.0), force_row(2, fn=2.0)], fields=fields)
samples, report = joined_samples(tmp_path / "capture", require_video=True)
assert [sample.frame_id for sample in samples] == [1, 2]
assert report.frames_not_in_video == 0
def test_a_missing_axis_is_empty_not_zero(tmp_path):
frames = [frame_row(1, BASE_NS + 10), frame_row(2, BASE_NS + 20)]
forces = [
force_row(1, fn=10.0, fx=0.5, fy=-0.25),
force_row(2, fn=None, fx=None, fy=None, valid=0),
]
_capture(tmp_path, frames, forces)
samples, _ = joined_samples(tmp_path / "capture")
assert samples[0].values == (10.0, 0.5, -0.25)
assert samples[1].value("FN") is None
assert samples[1].valid is False
assert samples[1].usable() is False
assert samples[0].usable() is True
def test_the_frame_index_keeps_repeated_ids_and_unknown_video_membership(tmp_path):
frames = [
frame_row(1, BASE_NS + 10, in_video=1),
frame_row(1, BASE_NS + 20, in_video=0),
]
_capture(tmp_path, frames, [force_row(1, fn=1.0)])
index = read_frame_index(tmp_path / "capture")
assert [row.frame_id for row in index] == [1, 1]
assert [row.in_video for row in index] == [True, False]
def test_a_missing_capture_directory_reads_as_empty(tmp_path):
samples, report = joined_samples(tmp_path / "absent")
assert samples == []
assert report.frames_total == 0 and report.force_rows == 0
@pytest.mark.parametrize("frame_id", [0, 120])
def test_frame_ids_are_used_verbatim(tmp_path, frame_id):
"""Zero and large ids are still identities; nothing is renumbered."""
frames = [frame_row(frame_id, BASE_NS + 10)]
_capture(tmp_path, frames, [force_row(frame_id, fn=2.0)])
samples, _ = joined_samples(tmp_path / "capture")
assert samples[0].frame_id == frame_id