Files
orisys-app/apps/autocal/tests/test_summary_lifecycle.py
T
ykcheungab 06e06126ca 重构 Autocal 采集页:单一队列与控制器驱动的主按钮
选点、对准、加载与录像分散在两个面板,状态靠隐藏相位判断。现改由不依赖 Qt 的采集控制器驱动,并加入录像选项、汇总平铺副本、保持到时自动结束、会话结束回到空闲,以及无设备时的模拟采集。已运行 Autocal 非 Qt 测试并通过范围内门禁;界面测试待 Windows 复核,未做实机验收。
2026-10-18 16:04:32 +08:00

159 lines
5.9 KiB
Python

"""The candidate export, the reviewed export, and what happens when it fails."""
from __future__ import annotations
import hashlib
import time
from PySide6.QtCore import QCoreApplication
from review_test_helpers import wait_review
from evidence_helpers import multipoint_session
from orisys_autocal.summary_export import POINTER_NAME, load_summary
def _pump(panel, timeout: float = 30.0) -> None:
deadline = time.monotonic() + timeout
while panel.summary_publishing and time.monotonic() < deadline:
QCoreApplication.processEvents()
time.sleep(0.005)
assert not panel.summary_publishing, "the summary export never finished"
def _finish_simulated_campaign(campaign) -> None:
while campaign.phase in {"load", "hold"}:
if campaign.phase == "load":
branch, target = campaign._steps[campaign._step_index]
campaign.confirm_reference(target)
campaign.complete_hold(
now_ns=campaign._hold_started_ns + int(campaign._hold_s * 1e9)
)
def _acquired_hashes(root) -> dict[str, str]:
"""Hashes of the recorded evidence only: export artifacts are not evidence.
The published summary lives in ``derived/`` and its readable mirror in
``summary/``; both are written by the export, so neither is part of the
acquisition and neither may appear in this comparison.
"""
return {
str(path.relative_to(root)): hashlib.sha256(path.read_bytes()).hexdigest()
for path in root.rglob("*")
if path.is_file()
and "derived" not in path.parts
and "summary" not in path.parts
and path.name != POINTER_NAME
}
def test_closing_the_session_publishes_a_candidate_summary(make_window, tmp_path):
root = multipoint_session(
tmp_path / "rec", sites=("P1",), repeats=1, levels=(5.0, 10.0))
before = _acquired_hashes(root)
window = make_window()
panel = window.calibration_panel
assert panel.publish_summary_for(root)
_pump(panel)
assert (root / POINTER_NAME).is_file()
summary, pointer = load_summary(root)
assert summary["formal_conclusion"] is False
assert summary["windows"]
assert all(row["review_status"] == "candidate" for row in summary["windows"])
assert pointer["path"].startswith("derived/summary_")
# Raw evidence is never rewritten by an export.
assert _acquired_hashes(root) == before
window.close()
def test_a_failed_export_is_reported_and_can_be_retried(
make_window, tmp_path, monkeypatch
):
import orisys_autocal.ui.calibration_panel as panel_module
root = multipoint_session(tmp_path / "rec", sites=("P1",), repeats=1, levels=(5.0,))
window = make_window()
panel = window.calibration_panel
def explode(*args, **kwargs):
raise RuntimeError("disk full")
monkeypatch.setattr(panel_module, "publish_summary", explode)
assert panel.publish_summary_for(root)
_pump(panel)
assert not (root / POINTER_NAME).exists()
assert "disk full" in panel.message.text()
assert panel._directory() == "" # the acquisition result is untouched
monkeypatch.undo()
assert panel.publish_summary_for(root)
_pump(panel)
assert (root / POINTER_NAME).is_file()
window.close()
def test_a_published_candidate_is_republished_when_the_review_is_saved(qt_app, tmp_path):
from orisys_autocal.session_review import review_session
from orisys_autocal.ui.record_overview_dialog import RecordOverviewDialog
root = multipoint_session(
tmp_path / "rec", sites=("P1",), repeats=1, levels=(5.0, 10.0))
review = review_session(root)
window = review["sites"][0]["windows"][2]
selections = {f"{window['trial_id']}:{window['step_index']}": {
"t_start_host_mono_ns": window["hold_start_host_mono_ns"] + 1,
"t_end_host_mono_ns": window["hold_end_host_mono_ns"],
"stability_verified": True,
}}
reviewed = review_session(root, selections=selections)
dialog = RecordOverviewDialog(root, review=reviewed)
try:
dialog._save()
wait_review(dialog)
assert (root / POINTER_NAME).is_file()
summary, pointer = load_summary(root)
confirmed = [row for row in summary["windows"] if row["stability_verified"]]
assert confirmed and confirmed[0]["review_status"] == "confirmed"
assert pointer["summary_id"] == summary["summary_id"]
assert summary["review_relative_path"].startswith("derived/review_")
assert summary["sensitivity"]
# A second explicit publish (the retry action) makes a new version.
first_id = pointer["summary_id"]
dialog._publish_summary()
wait_review(dialog)
second, pointer_again = load_summary(root)
assert pointer_again["summary_id"] != first_id
assert second["summary_id"] == pointer_again["summary_id"]
finally:
dialog.close()
def test_the_multipoint_end_of_campaign_starts_the_export(make_window, tmp_path):
window = make_window()
panel = window.calibration_panel
campaign = panel.campaign
campaign.add_site("P1", 0, 0, 1, [5.0], 1, 1, max_force_n=20)
directory = campaign.start(
tmp_path, sensor_model="finger", sensor_sn="SN1",
coordinate_frame="sensor", max_force_n=20, simulate=True,
)
_finish_simulated_campaign(campaign)
assert campaign.phase == "done"
panel._poll()
_pump(panel)
assert (directory / POINTER_NAME).is_file()
summary, _pointer = load_summary(directory)
assert summary["session_id"] == directory.name
# A simulated campaign produces no frames, so its windows must not read as
# measurements: they are excluded with the reason the review recorded.
assert all(row["eligible"] is False for row in summary["windows"])
assert all(row["excluded_reason"] == "no_frame_source" for row in summary["windows"])
assert any(issue["reason"] == "no_frame_source" for issue in summary["issues"])
window.close()