把 Studio 主机协议改成单向端口,并让「从视频重算」走分析入口

控制器经 AcquireGate / ImageSink / ShellNotes 与信号接线,采集 tick 用 acquire_presenter 上屏;三维测试证明分析结果已喂 3D 后,重算并入 on_analysis_requested。
验证:apps/studio 下相关 pytest 与 tools/ui_check.py(634 checks)通过。
This commit is contained in:
ykcheungab
2026-10-05 14:00:24 +08:00
parent 25ccffd814
commit 28e955a817
27 changed files with 1764 additions and 745 deletions
+56 -38
View File
@@ -2003,10 +2003,10 @@ def test_the_replay_transport_plays_and_pauses(make_window, replay_recording):
assert window.replay.position() == parked, "the playhead kept moving while paused"
def test_the_recompute_mode_reruns_the_pipeline_over_the_video(
def test_the_recompute_switch_asks_for_analysis_not_an_acquire_chain(
make_window, replay_recording
):
"""Off: the recorded CSV. On: the recorded video through the vision chain."""
"""「从视频重算」 shares ``on_analysis_requested`` with opening a recording."""
from PySide6.QtWidgets import QApplication
window = _show(make_window(), (1440, 880))
@@ -2019,27 +2019,30 @@ def test_the_recompute_mode_reruns_the_pipeline_over_the_video(
assert window.act_recompute.isEnabled(), "the recompute switch is not offered"
started: list = []
asked: list = []
def fake_run() -> None:
started.append(True)
window.session.set_running(True)
original = window.replay_ctl.on_analysis_requested
def spy_analysis() -> None:
asked.append(True)
return original()
window._start_run = fake_run
window.replay_ctl.on_analysis_requested = spy_analysis
window.act_recompute.setChecked(True)
QApplication.processEvents()
assert started == [True], f"the pass started {len(started)} times"
assert not panel.is_recording_source(), "the charts still read the CSV"
assert window.toolbar.source_mode() == "video"
assert window.toolbar.video_path().endswith(".mp4"), window.toolbar.video_path()
assert window.toolbar.btn_run.isChecked(), (
"the transport does not show the pass as running"
)
assert asked == [True], "the switch did not ask for analysis"
assert started == [], "the switch must not open an acquisition chain"
# A recording's force measurement stays on forces.csv; analysis feeds 2D/3D.
assert panel.is_recording_source(), "analysis must not replace the recorded CSV"
window._recompute_running = True
window.act_recompute.setChecked(False)
QApplication.processEvents()
assert panel.is_recording_source(), "turning the recompute off did not restore the CSV"
assert not window.toolbar.btn_run.isChecked(), "the transport stayed checked"
assert panel.is_recording_source()
def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_recording):
@@ -2083,25 +2086,23 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
window.statusBar().currentMessage()
)
# A recompute pass: the same button stops it, without stopping the recording.
stopped: list = []
window._stop_run = lambda **kwargs: stopped.append(kwargs.get("reason"))
window._recompute_running = True
window._update_run_enabled()
assert window.toolbar.btn_stop.isEnabled(), "no stop offered while a pass runs"
window.toolbar.btn_stop.click()
# An analysis pass left over from opening the recording: Stop cancels it.
# Opening may already have finished the pass; start one so the escape hatch is real.
window.replay_ctl.cancel_analysis()
window.replay_ctl.wait_for_analysis()
window.replay_ctl.on_analysis_requested()
QApplication.processEvents()
assert stopped == ["user_stop"], stopped
assert t("toolbar.stopped_pass") in window.statusBar().currentMessage(), (
window.statusBar().currentMessage()
)
if window.replay_ctl.analysis_running():
window._update_run_enabled()
assert window.toolbar.btn_stop.isEnabled(), "no stop offered while a pass runs"
window.toolbar.btn_stop.click()
QApplication.processEvents()
assert not window.replay_ctl.analysis_running()
assert t("toolbar.stopped_pass") in window.statusBar().currentMessage(), (
window.statusBar().currentMessage()
)
# Nothing running: it says so instead of doing nothing at all.
# (The pass is over, so the stop button is correctly disabled — the escape hatch is
# therefore exercised the way Esc reaches it, through the controller.)
assert not window.toolbar.btn_stop.isEnabled(), (
"a stop is still offered after the pass was stopped"
)
window.replay_ctl.stop_activity()
QApplication.processEvents()
assert t("toolbar.nothing_to_stop") in window.statusBar().currentMessage(), (
@@ -2344,13 +2345,20 @@ def test_the_two_figures_are_drawn_from_one_arrow_policy(make_window, monkeypatc
window.close()
def test_a_bare_video_plays_even_though_it_is_not_a_recording(make_window, record_root):
def test_a_bare_video_plays_even_though_it_is_not_a_recording(
make_window, record_root, decline_video_prompt
):
"""A clip with no forces.csv beside it is still something to watch.
An operator who points at a video expects the transport to play it. Opening the
video's own folder as a *recording* used to "succeed" against the tolerant reader
and then play a zero-length timeline ("pressing 回放 does nothing"), so the fix is
not to refuse the file but to play it as what it is: a video, with empty charts.
The setup prompt is declined here (``decline_video_prompt``): this test is about the
playback, and a dismissed prompt is the answer that leaves it exactly as it was. What
accepting the prompt does instead is its own module
(``test_bare_video_analysis.py``).
"""
import time as _time
@@ -2401,14 +2409,16 @@ def test_a_bare_video_plays_even_though_it_is_not_a_recording(make_window, recor
assert window._play_timer.isActive()
image = window.replay.video_image()
assert image is not None and not image.isNull(), "no frame was decoded"
window._stop_replay_activity()
window.replay_ctl.stop_activity()
QApplication.processEvents()
assert not window._play_timer.isActive()
finally:
window.close()
def test_the_replay_and_the_live_paths_share_one_image_backend(make_window, record_root):
def test_the_replay_and_the_live_paths_share_one_image_backend(
make_window, record_root, decline_video_prompt
):
"""Stage 7: one renderer for every 2D producer, and modes from the frame itself.
A recording stores the raw camera image and nothing else, so its plan offers exactly
@@ -2444,7 +2454,7 @@ def test_the_replay_and_the_live_paths_share_one_image_backend(make_window, reco
app_module.render_image = spy
try:
window._apply_view_preset("replay")
window.layout_ctl.apply_preset("replay")
assert window._load_replay_video(bare) is True
window._render_replay_frame()
QApplication.processEvents()
@@ -2503,7 +2513,9 @@ def test_the_application_converts_bgr_to_qimage_in_one_place():
assert others == [], f"another BGR→QImage conversion exists: {others}"
def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_root):
def test_the_replay_entries_exist_only_in_the_replay_view(
make_window, record_root, decline_video_prompt
):
"""Stage 7: the live view has no replay entry, and says where they live.
Opening a video from the live view used to set the acquisition source to a file
@@ -2523,7 +2535,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
window = _show(make_window(), (1440, 880))
try:
window._apply_view_preset("live")
window.layout_ctl.apply_preset("live")
QApplication.processEvents()
for action in (window.act_open_video, window.act_playback, window.act_playback_latest):
assert not action.isEnabled(), "a replay entry is offered in the live view"
@@ -2542,7 +2554,7 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
# In the replay view they are offered again, and the recompute entry follows
# whether there is anything to recompute from.
window._apply_view_preset("replay")
window.layout_ctl.apply_preset("replay")
QApplication.processEvents()
for action in (window.act_open_video, window.act_playback, window.act_playback_latest):
assert action.isEnabled()
@@ -2552,10 +2564,16 @@ def test_the_replay_entries_exist_only_in_the_replay_view(make_window, record_ro
assert window.toolbar.btn_recompute.isEnabled(), (
"a video with no recording beside it can still be recomputed"
)
# Declined setup (fixture) means play-only: the switch re-asks, then clears. It
# must not open an acquisition chain over the clip.
started: list = []
window._start_run = lambda: started.append(True)
window.toolbar.btn_recompute.click()
QApplication.processEvents()
assert window.act_recompute.isChecked(), "the rail's recompute entry did not toggle"
window.act_recompute.setChecked(False)
assert started == [], "recompute must not open an acquisition chain"
assert not window.act_recompute.isChecked(), (
"declining analysis setup must leave the switch off"
)
finally:
window.close()
@@ -2613,7 +2631,7 @@ def test_a_video_inside_a_recording_opens_the_recording_and_plays(
window.toolbar.btn_run.setChecked(True)
QApplication.processEvents()
assert window._play_timer.isActive(), "pressing 回放 did not start playback"
window._pause_playback()
window.replay_ctl.pause_playback()
window.close()