修复 Autocal 录像故障处理、复核回放与汇总统计
This commit is contained in:
@@ -193,6 +193,40 @@ def test_a_stopped_source_keeps_the_repeat_incomplete(tmp_path):
|
||||
assert session["status"] == "incomplete"
|
||||
|
||||
|
||||
def test_a_failed_writer_aborts_while_the_camera_keeps_running(tmp_path, monkeypatch):
|
||||
from orisys_app_shared.record.video_writer import VideoSegments
|
||||
|
||||
def fail_write(self, frame):
|
||||
raise OSError("encoder or disk failed")
|
||||
|
||||
monkeypatch.setattr(VideoSegments, "write", fail_write)
|
||||
campaign = _campaign(repeats=1)
|
||||
root = _start(campaign, tmp_path)
|
||||
worker = FakeWorker()
|
||||
campaign.attach_capture(worker, fps=10)
|
||||
recorder = campaign._recorder
|
||||
try:
|
||||
for _ in range(8):
|
||||
worker.advance()
|
||||
worker.emit()
|
||||
time.sleep(0.02)
|
||||
deadline = time.monotonic() + 3
|
||||
while recorder.is_recording and time.monotonic() < deadline:
|
||||
time.sleep(0.005)
|
||||
assert recorder.stats().failed
|
||||
assert worker.is_running
|
||||
assert not campaign.capture_ready
|
||||
campaign.poll()
|
||||
assert campaign.phase in {"closing", "aborted"}
|
||||
_drain(campaign)
|
||||
trial = _session(root)["trials"][0]
|
||||
assert trial["capture_status"] == "incomplete"
|
||||
assert trial["capture_reason"] == "write_error"
|
||||
finally:
|
||||
campaign.stop("test finished")
|
||||
campaign.flush_close()
|
||||
|
||||
|
||||
def test_a_simulated_repeat_is_steps_complete_but_not_recorded_evidence(tmp_path):
|
||||
campaign = _campaign(repeats=1)
|
||||
root = campaign.start(
|
||||
|
||||
@@ -164,3 +164,19 @@ def test_confirmed_and_candidate_windows_are_never_fitted_together():
|
||||
assert by_review["candidate"]["slope_raw_per_N"] == pytest.approx(40.0)
|
||||
assert by_review["confirmed"]["window_count"] == 2
|
||||
assert by_review["confirmed"]["slope_raw_per_N"] == pytest.approx(40.0)
|
||||
|
||||
|
||||
def test_site_aggregation_keeps_review_sets_and_trial_counts_separate():
|
||||
from orisys_autocal.summary import aggregate_sensitivity, build_sensitivity
|
||||
|
||||
windows = [
|
||||
_window(0, 0, branch="baseline"), _window(5, 500),
|
||||
_window(10, 400, verified=True), _window(15, 600, verified=True),
|
||||
]
|
||||
rows = [row for row in aggregate_sensitivity(build_sensitivity(windows))
|
||||
if row["component"] == "FN"]
|
||||
assert len(rows) == 2
|
||||
by_review = {row["review_status"]: row for row in rows}
|
||||
assert by_review["candidate"]["slope_mean_raw_per_N"] == pytest.approx(100)
|
||||
assert by_review["confirmed"]["slope_mean_raw_per_N"] == pytest.approx(40)
|
||||
assert all(row["trials"] == 1 and row["slope_std_raw_per_N"] is None for row in rows)
|
||||
|
||||
@@ -205,6 +205,39 @@ def test_the_frame_list_is_bounded_by_the_hold(tmp_path):
|
||||
assert all(frame.in_video for frame in frames)
|
||||
|
||||
|
||||
def test_confirmed_playback_uses_the_reviewers_selected_interval(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
window = review["sites"][0]["windows"][1]
|
||||
selected_start = window["trace"][1][0]
|
||||
selected_end = window["hold_end_host_mono_ns"]
|
||||
review = review_session(tmp_path / "rec", selections={f"{trial_id}:{step}": {
|
||||
"t_start_host_mono_ns": selected_start,
|
||||
"t_end_host_mono_ns": selected_end,
|
||||
"stability_verified": True,
|
||||
}})
|
||||
with open_window_video(review, trial_id=trial_id, step_index=step) as player:
|
||||
assert len(player.frames) == 2
|
||||
assert all(selected_start <= frame.t_host_mono_ns < selected_end for frame in player.frames)
|
||||
|
||||
|
||||
def test_playback_does_not_retain_previously_decoded_images(tmp_path):
|
||||
import gc
|
||||
import weakref
|
||||
|
||||
review = _review(tmp_path)
|
||||
trial_id, step = _capture(review)
|
||||
refs = []
|
||||
with open_window_video(review, trial_id=trial_id, step_index=step) as player:
|
||||
for index in range(player.frame_count):
|
||||
image = player.frame(index)
|
||||
assert image is not None
|
||||
refs.append(weakref.ref(image))
|
||||
del image
|
||||
gc.collect()
|
||||
assert sum(ref() is not None for ref in refs) <= 1
|
||||
|
||||
|
||||
def test_decode_failure_is_reported_rather_than_showing_a_neighbour(tmp_path):
|
||||
review = _review(tmp_path)
|
||||
root = tmp_path / "rec"
|
||||
|
||||
@@ -147,3 +147,33 @@ def test_an_unclosed_trial_does_not_offer_the_video(qt_app, tmp_path):
|
||||
assert dialog.message.text()
|
||||
finally:
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_a_closed_aborted_trial_can_be_replayed_for_diagnosis(qt_app, tmp_path):
|
||||
import json
|
||||
|
||||
from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog
|
||||
from orisys_autocal.ui.session_review_dialog import SessionReviewDialog
|
||||
|
||||
root = multipoint_session(tmp_path / "rec", trial_status="incomplete")
|
||||
path = root / "session.json"
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
manifest["trials"][0].update(
|
||||
capture_status="incomplete", capture_report={"completed": True})
|
||||
path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
review = review_session(root)
|
||||
for kind in (SessionReviewDialog, RecordOverviewDialog):
|
||||
dialog = kind(root, review=review)
|
||||
opened = None
|
||||
try:
|
||||
if isinstance(dialog, RecordOverviewDialog):
|
||||
dialog.table.selectRow(0)
|
||||
opened = dialog._video()
|
||||
assert opened is not None
|
||||
wait_review(opened)
|
||||
assert opened.video is not None
|
||||
assert opened.image.pixmap() is not None
|
||||
finally:
|
||||
if opened is not None:
|
||||
opened.close()
|
||||
dialog.close()
|
||||
|
||||
Reference in New Issue
Block a user