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.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
"""Locating a reviewed hold's frames in the existing recording."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from evidence_helpers import (
|
||||
BASE_NS,
|
||||
FRAMES_FIELDS,
|
||||
decoded_frame_number,
|
||||
force_row,
|
||||
frame_row,
|
||||
multipoint_session,
|
||||
write_capture,
|
||||
write_csv,
|
||||
write_json_file,
|
||||
)
|
||||
from orisys_autocal.session_review import review_session
|
||||
from orisys_autocal.video_review import (
|
||||
NO_FRAME_SOURCE,
|
||||
NO_VIDEO_FRAME,
|
||||
Location,
|
||||
in_window_frames,
|
||||
locate_window,
|
||||
open_window_video,
|
||||
)
|
||||
|
||||
|
||||
def _review(tmp_path) -> dict:
|
||||
root = multipoint_session(
|
||||
tmp_path / "rec", sites=("P1",), repeats=1, levels=(5.0,))
|
||||
return review_session(root)
|
||||
|
||||
|
||||
def _capture(review: dict):
|
||||
trial_id = review["trials"][0]["trial_id"]
|
||||
step = review["sites"][0]["windows"][1]["step_index"]
|
||||
return trial_id, step
|
||||
|
||||
|
||||
def test_a_reviewed_window_locates_its_own_first_video_frame(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
|
||||
location = locate_window(review, trial_id=trial_id, step_index=step)
|
||||
|
||||
assert isinstance(location, Location)
|
||||
assert location.frame_id > 0
|
||||
assert location.segment == 1 and location.segment_frame == location.frame_id
|
||||
# The located picture is the one the frame index names.
|
||||
root = tmp_path / "rec"
|
||||
video = root / location.capture_directory / "raw_0001.mp4"
|
||||
assert decoded_frame_number(video, segment_frame=location.segment_frame) == location.segment_frame - 1
|
||||
|
||||
with open_window_video(review, trial_id=trial_id, step_index=step) as player:
|
||||
assert player.frame_count >= 1
|
||||
assert player.frame(0) is not None
|
||||
assert player.location.frame_id == location.frame_id
|
||||
assert player.frame(0).shape[:2] == (48, 64)
|
||||
|
||||
|
||||
def test_a_frame_outside_the_video_is_skipped_for_the_next_one(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
root = tmp_path / "rec"
|
||||
trial_id = review["trials"][0]["trial_id"]
|
||||
window = review["sites"][0]["windows"][1]
|
||||
capture = root / "sites" / "P1" / "repeat-001" / "capture"
|
||||
from evidence_helpers import read_csv
|
||||
|
||||
rows = read_csv(capture / "frames.csv")
|
||||
# The second hold starts at frame 4; take its first frame out of the video.
|
||||
first_of_hold = int(window["hold_start_host_mono_ns"])
|
||||
row = next(item for item in rows if int(item["t_host_mono_ns"]) >= first_of_hold)
|
||||
row["in_video"] = 0
|
||||
row["segment"] = 0
|
||||
row["segment_frame"] = 0
|
||||
skipped = int(row["frame_id"])
|
||||
write_csv(capture / "frames.csv", FRAMES_FIELDS, rows)
|
||||
|
||||
location = locate_window(review, trial_id=trial_id, step_index=window["step_index"])
|
||||
|
||||
assert isinstance(location, Location)
|
||||
assert location.frame_id == skipped + 1
|
||||
|
||||
|
||||
def test_no_capture_directory_is_a_reason_not_a_picture(tmp_path):
|
||||
root = multipoint_session(tmp_path / "rec", sites=("P1",), repeats=1,
|
||||
levels=(5.0,), with_capture=False)
|
||||
review = review_session(root)
|
||||
trial_id = review["trials"][0]["trial_id"]
|
||||
step = review["sites"][0]["windows"][1]["step_index"]
|
||||
|
||||
location = locate_window(review, trial_id=trial_id, step_index=step)
|
||||
|
||||
assert location == {"reason": NO_FRAME_SOURCE}
|
||||
with pytest.raises(ValueError, match=NO_FRAME_SOURCE):
|
||||
open_window_video(review, trial_id=trial_id, step_index=step)
|
||||
|
||||
|
||||
def test_a_hold_with_no_picture_in_the_video_has_no_frame(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
root = tmp_path / "rec"
|
||||
capture = root / "sites" / "P1" / "repeat-001" / "capture"
|
||||
from evidence_helpers import read_csv
|
||||
|
||||
rows = read_csv(capture / "frames.csv")
|
||||
for row in rows:
|
||||
row["in_video"] = 0
|
||||
row["segment"] = 0
|
||||
row["segment_frame"] = 0
|
||||
write_csv(capture / "frames.csv", FRAMES_FIELDS, rows)
|
||||
trial_id, step = _capture(review)
|
||||
|
||||
assert locate_window(review, trial_id=trial_id, step_index=step) == {
|
||||
"reason": NO_VIDEO_FRAME}
|
||||
|
||||
|
||||
def test_a_missing_segment_file_is_reported(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
root = tmp_path / "rec"
|
||||
for video in root.glob("sites/*/*/capture/raw_0001.mp4"):
|
||||
video.unlink()
|
||||
trial_id, step = _capture(review)
|
||||
|
||||
assert locate_window(review, trial_id=trial_id, step_index=step) == {
|
||||
"reason": NO_FRAME_SOURCE}
|
||||
|
||||
|
||||
def test_a_hold_split_across_segments_opens_the_right_one(tmp_path):
|
||||
"""Segment 2 restarts its own frame count; seeking must follow the index."""
|
||||
root = tmp_path / "rec"
|
||||
trial = root / "trial"
|
||||
start = BASE_NS + 10
|
||||
frames = [
|
||||
frame_row(1, start, segment=1, segment_frame=1),
|
||||
frame_row(2, start + 10, segment=1, segment_frame=2),
|
||||
frame_row(3, start + 20, segment=2, segment_frame=1),
|
||||
]
|
||||
forces = [force_row(1, fn=1.0), force_row(2, fn=2.0), force_row(3, fn=3.0)]
|
||||
write_capture(trial, frames, forces, video_segments=2)
|
||||
write_json_file(root / "session.json", {"format_version": "autocal-session-1.0"})
|
||||
review = {
|
||||
"source_directory": str(root),
|
||||
"session_id": "s1",
|
||||
"trials": [{"trial_id": "t1", "directory": "trial"}],
|
||||
"sites": [{
|
||||
"site_id": "P1", "uv_xy_px": [1, 2],
|
||||
"windows": [{
|
||||
"trial_id": "t1", "step_index": 1,
|
||||
"hold_start_host_mono_ns": start, "hold_end_host_mono_ns": start + 100,
|
||||
"frame_source": "trial",
|
||||
"trace": [(start, 1, 1.0), (start + 10, 2, 2.0), (start + 20, 3, 3.0)],
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
location = locate_window(review, trial_id="t1", step_index=1)
|
||||
|
||||
assert isinstance(location, Location) and location.segment == 1
|
||||
with open_window_video(review, trial_id="t1", step_index=1) as player:
|
||||
assert player.frame_count == 3
|
||||
assert player.frame(0) is not None
|
||||
# Frame 3 lives in the second container, at its own position 1.
|
||||
assert player.frame(2) is not None
|
||||
assert player.frames[2].segment == 2 and player.frames[2].segment_frame == 1
|
||||
|
||||
|
||||
def test_the_decoder_can_be_closed_and_reopened(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
|
||||
first = open_window_video(review, trial_id=trial_id, step_index=step)
|
||||
assert first.frame(0) is not None
|
||||
first.close()
|
||||
second = open_window_video(review, trial_id=trial_id, step_index=step)
|
||||
try:
|
||||
assert second.frame(0) is not None
|
||||
finally:
|
||||
second.release()
|
||||
|
||||
|
||||
def test_an_unknown_trial_or_window_is_a_reason(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
|
||||
assert locate_window(review, trial_id="missing", step_index=step) == {"reason": "trial_not_found"}
|
||||
assert locate_window(review, trial_id=trial_id, step_index=999) == {"reason": "window_not_found"}
|
||||
|
||||
|
||||
def test_the_frame_list_is_bounded_by_the_hold(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
window = next(
|
||||
item for site in review["sites"] for item in site["windows"]
|
||||
if item["trial_id"] == trial_id and item["step_index"] == step
|
||||
)
|
||||
|
||||
frames = in_window_frames(review, trial_id=trial_id, step_index=step)
|
||||
|
||||
assert isinstance(frames, list) and frames
|
||||
assert all(
|
||||
window["hold_start_host_mono_ns"] <= frame.t_host_mono_ns < window["hold_end_host_mono_ns"]
|
||||
for frame in frames
|
||||
)
|
||||
assert all(frame.in_video for frame in frames)
|
||||
|
||||
|
||||
def test_decode_failure_is_reported_rather_than_showing_a_neighbour(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
root = tmp_path / "rec"
|
||||
trial_id, step = _capture(review)
|
||||
location = locate_window(review, trial_id=trial_id, step_index=step)
|
||||
assert isinstance(location, Location)
|
||||
(root / location.capture_directory / "raw_0001.mp4").write_bytes(b"not a video")
|
||||
window = review["sites"][0]["windows"][1]
|
||||
player = open_window_video(review, trial_id=trial_id, step_index=window["step_index"])
|
||||
try:
|
||||
assert player.frame(0) is None
|
||||
assert player.position == -1
|
||||
finally:
|
||||
player.close()
|
||||
|
||||
def test_a_discarded_hold_attempt_cannot_be_played(tmp_path):
|
||||
"""A remeasured hold keeps its row id; its window must not be shown as evidence."""
|
||||
from evidence_helpers import HOLD_STRIDE_NS, Hold, multipoint_session
|
||||
|
||||
replaced = Hold(
|
||||
step_index=1, branch="load", target_load_n=5.0, reference_n=5.0,
|
||||
start_ns=BASE_NS + 200 * HOLD_STRIDE_NS,
|
||||
end_ns=BASE_NS + 200 * HOLD_STRIDE_NS + 4_000_000_000,
|
||||
complete=0,
|
||||
)
|
||||
root = multipoint_session(
|
||||
tmp_path / "rec", sites=("P1",), repeats=1, levels=(5.0,), extra_holds=[replaced])
|
||||
review = review_session(root)
|
||||
trial_id = review["trials"][0]["trial_id"]
|
||||
step = next(item for site in review["sites"] for item in site["windows"]
|
||||
if item["trial_id"] == trial_id)["step_index"]
|
||||
|
||||
assert locate_window(review, trial_id=trial_id, step_index=step, hold_id=4) == {
|
||||
"reason": "hold_not_complete"}
|
||||
assert locate_window(review, trial_id=trial_id, step_index=step, hold_id=99) == {
|
||||
"reason": "hold_not_found"}
|
||||
# The completed attempt still opens, and names the attempt it shows.
|
||||
location = locate_window(review, trial_id=trial_id, step_index=step, hold_id=1)
|
||||
assert isinstance(location, Location) and location.hold_id == 1
|
||||
Reference in New Issue
Block a user