Present replay 3D with the cursor and open Analyze on a remembered video
Replay left the 3D pane on the rest mesh or a sparse overlay, and Analyze skipped the setup dialog unless the file was re-picked. Live Run no longer starts a chain on a video path. Verified with Studio tests for flush, overlay mask, Analyze open, live-video refusal, and locales.
This commit is contained in:
@@ -12,6 +12,7 @@ broken application (the same rule ``apps/autocal`` follows).
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -91,8 +92,29 @@ def make_window(qt_app, tmp_path, monkeypatch):
|
||||
for window in created:
|
||||
try:
|
||||
window.close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception: # noqa: BLE001 - teardown must not fail a test that passed
|
||||
# Reported rather than swallowed: a window that cannot close is a Qt lifetime
|
||||
# problem worth seeing, but it must not turn a green test red.
|
||||
logging.getLogger(__name__).debug("could not close a test window", exc_info=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def decline_video_prompt(monkeypatch):
|
||||
"""Answer the bare-video setup prompt with "no", so it cannot block a test.
|
||||
|
||||
Opening a video that is not a recording now asks *how* to analyse it, and the dialog is
|
||||
modal: a test that drives ``load_video`` on a bare video without answering waits for a
|
||||
person who is not there. Declining is the answer that leaves the old behaviour intact —
|
||||
the video plays, nothing is analysed — which is what the tests predating the prompt are
|
||||
about.
|
||||
"""
|
||||
from PySide6.QtWidgets import QDialog
|
||||
|
||||
from orisys_studio.ui.video_analysis_dialog import VideoAnalysisDialog
|
||||
|
||||
monkeypatch.setattr(
|
||||
VideoAnalysisDialog, "exec", lambda self: QDialog.DialogCode.Rejected
|
||||
)
|
||||
|
||||
|
||||
def visible_descendants(widget):
|
||||
|
||||
@@ -674,3 +674,52 @@ def test_the_first_frame_is_the_prompt_s_default_reference(
|
||||
assert dialog.reference_spin.value() == 4
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_the_prompt_restores_a_prior_reference_and_no_calibration(
|
||||
bare_clip, window, qt_app
|
||||
):
|
||||
"""Reopening Analysis setup must show the answers from the last pass."""
|
||||
facts = bare_video.probe(bare_video.resolve_video(bare_clip))
|
||||
dialog = VideoAnalysisDialog(
|
||||
video=bare_clip,
|
||||
facts=facts,
|
||||
catalog=scan_sensor_catalog(),
|
||||
window=window,
|
||||
initial_profile="shipped/box",
|
||||
prefer_no_calibration=True,
|
||||
initial_reference_frame=4,
|
||||
)
|
||||
try:
|
||||
assert dialog.reference_spin.value() == 4
|
||||
assert dialog.choice().reference_frame == 4
|
||||
assert dialog.calibration_combo.currentData() == "none"
|
||||
assert dialog.choice().calibration is None
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_analysis_setup_reopens_the_prompt_after_the_first_pass(
|
||||
bare_clip, window, monkeypatch
|
||||
):
|
||||
"""View ▸ Analysis setup… asks again; a new reference frame starts a new pass."""
|
||||
open_bare_video(window, bare_clip, monkeypatch, reference=None)
|
||||
settle(window)
|
||||
first = window.replay_ctl.recording_directory()
|
||||
assert first is not None
|
||||
assert bare_video.is_synthetic(first)
|
||||
|
||||
# The box stitch probe consumes the first decoded frames; a reference that names
|
||||
# one of them is refused. Frame 10 is past that offset on this clip.
|
||||
seen: list = []
|
||||
answer_prompt(monkeypatch, profile="shipped/box", reference=10, seen=seen)
|
||||
window.replay_ctl.on_analysis_setup()
|
||||
settle(window)
|
||||
|
||||
assert seen, "Analysis setup did not open the dialog"
|
||||
second = window.replay_ctl.recording_directory()
|
||||
assert second is not None
|
||||
block = bare_video.read_synthetic(second)
|
||||
assert block is not None
|
||||
assert int(block["reference_frame"]) == 10
|
||||
assert window.replay_ctl.available_result() is not None
|
||||
|
||||
@@ -494,9 +494,12 @@ class _Host:
|
||||
raise AssertionError("the test did not provide an analysis service")
|
||||
return self.service_factory(recording, cancelled, on_progress)
|
||||
|
||||
def bare_video_workspace(self, video):
|
||||
def bare_video_workspace(self, video, **kwargs):
|
||||
return None
|
||||
|
||||
def recording_analysis_overrides(self, recording):
|
||||
return False
|
||||
|
||||
|
||||
class _RecordingReader:
|
||||
"""The recording reader as the engine and the controller use it."""
|
||||
@@ -816,7 +819,7 @@ def test_a_seek_equals_playing_forward_to_the_same_frame(session):
|
||||
|
||||
|
||||
def test_the_3d_pane_is_fed_the_same_frame_the_canvas_draws(session):
|
||||
"""One cursor, two figures: the packet's id, flow and mask are the frame's own."""
|
||||
"""One cursor, two figures: the packet's id, flow, depth and mask are the frame's own."""
|
||||
frame = session.frame_at(session.time_of(LAST_FRAME_ID - 2))
|
||||
assert frame is not None
|
||||
packet = session.packet(frame)
|
||||
@@ -828,10 +831,49 @@ def test_the_3d_pane_is_fed_the_same_frame_the_canvas_draws(session):
|
||||
)
|
||||
assert np.array_equal(np.asarray(packet.valid_mask), np.asarray(frame.valid_mask))
|
||||
assert packet.flow_valid is True and packet.flow_frame_id == int(frame.frame_id)
|
||||
assert bool(packet.depth_valid) is bool(frame.has_depth)
|
||||
if frame.has_depth:
|
||||
assert np.array_equal(np.asarray(packet.depth), np.asarray(frame.depth))
|
||||
assert packet.depth_frame_id == int(frame.frame_id)
|
||||
else:
|
||||
assert packet.depth is None and packet.depth_frame_id is None
|
||||
assert packet.forces_valid is False, "a result stores no force, so none may be claimed"
|
||||
assert float(packet.mono_t) == pytest.approx(frame.t_rel_s)
|
||||
|
||||
|
||||
def test_paint_policy_matches_the_2d_painter_threshold(session, box_session):
|
||||
"""3D overlays use ``paint_policy``: finger keeps the display floor, box keeps 2 px."""
|
||||
from orisys_app_shared.acquisition.arrows import sdk_box_policy
|
||||
from orisys_app_shared.scene3d.visual_policy import VisualPolicy
|
||||
|
||||
finger = session.paint_policy()
|
||||
assert finger.flow_on_px == pytest.approx(session.display_policy.flow_on_px)
|
||||
assert int(finger.flow_grid_spacing) == int(
|
||||
session.stored_policy().get("flow_grid_spacing", session.policy.flow_grid_spacing)
|
||||
)
|
||||
|
||||
# Keep the stored grid; only the activity threshold moves.
|
||||
adopted = session.display_policy
|
||||
assert session.serve_policy(
|
||||
VisualPolicy(
|
||||
flow_on_px=0.9,
|
||||
flow_off_px=0.6,
|
||||
flow_grid_spacing=adopted.flow_grid_spacing,
|
||||
flow_arrow_scale=adopted.flow_arrow_scale,
|
||||
flow_edge_margin=adopted.flow_edge_margin,
|
||||
flow_length_margin=adopted.flow_length_margin,
|
||||
max_arrows=adopted.max_arrows,
|
||||
)
|
||||
)
|
||||
assert session.paint_policy().flow_on_px == pytest.approx(0.9)
|
||||
|
||||
painter = sdk_box_policy()
|
||||
box = box_session.paint_policy()
|
||||
assert box.flow_on_px == pytest.approx(painter.flow_on_px)
|
||||
assert box.flow_off_px == pytest.approx(painter.flow_off_px)
|
||||
assert int(box.flow_grid_spacing) == int(painter.flow_grid_spacing)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- display settings
|
||||
def test_a_display_change_is_a_read_and_writes_no_result(pass_root, session):
|
||||
"""The result key is blind to display settings, so a change must not write.
|
||||
@@ -1450,7 +1492,7 @@ def test_the_nothing_to_draw_line_names_the_action_that_produces_a_result():
|
||||
seen[locale] = text
|
||||
# The action that produces a result is named: the pass runs on open, and the
|
||||
# toolbar action re-runs it.
|
||||
assert "由视频重算" in text or "Recompute from the video" in text, text
|
||||
assert "分析视频" in text or "Analyze video" in text, text
|
||||
assert (
|
||||
"分析" in text or "analysis result" in text
|
||||
), f"{locale} does not say a result is what is missing: {text}"
|
||||
|
||||
@@ -184,6 +184,8 @@ class _Host:
|
||||
#: has no dialog, so the answer is the field a test sets.
|
||||
self.workspace = None
|
||||
self.workspace_requests: list = []
|
||||
self.workspace_kwargs: dict = {}
|
||||
self.override_requests: list = []
|
||||
|
||||
# AcquireGate reads (legacy private names kept as writeable fields for tests)
|
||||
@property
|
||||
@@ -238,10 +240,15 @@ class _Host:
|
||||
def current_source_kind(self) -> str:
|
||||
return "recompute"
|
||||
|
||||
def bare_video_workspace(self, video):
|
||||
def bare_video_workspace(self, video, **kwargs):
|
||||
self.workspace_requests.append(video)
|
||||
self.workspace_kwargs = dict(kwargs)
|
||||
return self.workspace
|
||||
|
||||
def recording_analysis_overrides(self, recording):
|
||||
self.override_requests.append(recording)
|
||||
return False
|
||||
|
||||
def analysis_service(self, recording, cancelled=None, *, on_progress=None):
|
||||
raise AssertionError("the test did not provide an analysis service")
|
||||
|
||||
@@ -577,3 +584,22 @@ def test_the_replay_entries_exist_only_in_the_replay_view(qt_app):
|
||||
assert actions["open_video"].enabled is True
|
||||
assert actions["open_video"].tooltips[-1] == ""
|
||||
assert parts[0].recompute_enabled[-1] is True
|
||||
|
||||
|
||||
def test_analyze_opens_a_remembered_video_that_is_not_loaded_yet(qt_app, tmp_path):
|
||||
"""A toolbar path alone used to notify ``playback.no_video`` and skip the dialog.
|
||||
|
||||
The rail can show a file name from a previous session while the engine has not opened
|
||||
anything. 「分析视频」 must call ``load_video`` so the setup prompt appears — the same
|
||||
path as picking the file again in the explorer.
|
||||
"""
|
||||
bare = tmp_path / "1.mp4"
|
||||
bare.write_bytes(b"not really a video")
|
||||
engine = _Engine(has_video=False, video_only=False)
|
||||
toolbar = _Toolbar(video_path=str(bare))
|
||||
controller, host, engine, parts = _controller(engine=engine, toolbar=toolbar)
|
||||
|
||||
controller.on_recompute_toggled(True)
|
||||
assert engine.opened == [("video", bare)], "the remembered file was not opened"
|
||||
assert host.workspace_requests == [bare], "the setup dialog was not shown"
|
||||
assert t("playback.no_video") not in host.notes
|
||||
|
||||
@@ -180,7 +180,7 @@ def _replay_window(make_window):
|
||||
QApplication.processEvents()
|
||||
window.brand_bar.btn_playback.click()
|
||||
QApplication.processEvents()
|
||||
assert window._in_replay_view(), "the window did not switch to the replay view"
|
||||
assert window.layout_ctl.in_replay_view(), "the window did not switch to the replay view"
|
||||
return window
|
||||
|
||||
|
||||
@@ -716,11 +716,20 @@ def test_the_panel_states_a_failed_pass_with_its_reason(
|
||||
)
|
||||
|
||||
|
||||
def test_the_panel_says_a_bare_video_cannot_be_analysed(tmp_path, own_window, monkeypatch):
|
||||
"""No session metadata: no pass exists to offer, and the line must not name one."""
|
||||
def test_a_bare_video_with_a_declined_prompt_promises_no_pass(
|
||||
tmp_path, own_window, monkeypatch, decline_video_prompt
|
||||
):
|
||||
"""A bare video whose setup prompt was dismissed: playback, and one honest line.
|
||||
|
||||
A bare video *can* be analysed now — that is what ``test_bare_video_analysis.py`` is
|
||||
about, and it asks the prompt and answers it. This test is the other answer: the operator
|
||||
opened the video and dismissed the question, so no pass is coming and the panel must not
|
||||
say one is. The line names the action instead (the setup prompt), which is what the
|
||||
"failed or refused, with its reason" state is for.
|
||||
"""
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
RAW_NOTE_KEY,
|
||||
RESULT_IMPOSSIBLE,
|
||||
RESULT_FAILED,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -734,9 +743,13 @@ def test_the_panel_says_a_bare_video_cannot_be_analysed(tmp_path, own_window, mo
|
||||
QApplication.processEvents()
|
||||
|
||||
assert window.replay.is_video_only(), "the clip did not open as a bare video"
|
||||
assert window.replay_ctl.result_state() == (RESULT_IMPOSSIBLE, "")
|
||||
assert not window.replay_ctl.analysis_running(), "a pass ran after a dismissed prompt"
|
||||
assert window.replay_ctl.result_state() == (
|
||||
RESULT_FAILED,
|
||||
t("playback.analysis_setup_declined"),
|
||||
)
|
||||
assert window.arrow_panel.note_label.text() == t(
|
||||
"panels.replay_analysis_unavailable"
|
||||
"panels.replay_analysis_failed", reason=t("playback.analysis_setup_declined")
|
||||
)
|
||||
assert t(RAW_NOTE_KEY) not in window.arrow_panel.note_label.text()
|
||||
assert plan_image(captured[-1]).note_key == "panels.replay_analysis_unavailable"
|
||||
assert plan_image(captured[-1]).note_key == "panels.replay_analysis_failed"
|
||||
|
||||
@@ -173,6 +173,36 @@ def test_the_finger_adapter_publishes_a_field_already_in_the_domain(scene):
|
||||
)
|
||||
|
||||
|
||||
def test_a_recovered_flow_keeps_the_island_as_the_overlay_mask(scene):
|
||||
"""Replay flow is finite only at stored cells; the overlay mask must stay the island.
|
||||
|
||||
The selector already drops ``NaN`` samples. If the mask is those finite cells too,
|
||||
an arrow tip one neighbour over is rejected and 3D keeps a handful of rim dots
|
||||
while 2D draws the full grid.
|
||||
"""
|
||||
adapter = FingerFrameAdapter(
|
||||
geometry_id=scene.geometry_id,
|
||||
field_domain_id=scene.field_domain_id,
|
||||
shape_hw=scene.shape_hw,
|
||||
)
|
||||
h, w = scene.shape_hw
|
||||
flow = np.full((h, w, 2), np.nan, dtype=np.float32)
|
||||
flow[1, 1, 0] = 7.0
|
||||
island = np.ones((h, w), dtype=bool)
|
||||
depth = np.zeros((h, w), dtype=np.float32)
|
||||
frame = adapter.snapshot(
|
||||
FakePacket(flow=flow, depth=depth, valid_mask=island),
|
||||
runtime=None,
|
||||
identity=_identity(),
|
||||
)
|
||||
assert frame is not None
|
||||
assert frame.flow_uv is not None and frame.flow_valid_mask is not None
|
||||
mask = np.asarray(frame.flow_valid_mask)
|
||||
assert mask.all(), "the overlay mask shrank to the recovered sample cells"
|
||||
finite = np.isfinite(np.asarray(frame.flow_uv)[..., 0])
|
||||
assert int(finite.sum()) == 1
|
||||
|
||||
|
||||
def test_the_finger_adapter_remaps_through_the_unroll_map():
|
||||
map_x = np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2]], dtype=np.float32)
|
||||
map_y = np.array([[2, 2, 2], [1, 1, 1], [0, 0, 0]], dtype=np.float32)
|
||||
|
||||
@@ -599,6 +599,54 @@ def test_repeated_start_and_stop_never_leaves_an_old_measurement(scene, pool):
|
||||
coordinator.close()
|
||||
|
||||
|
||||
def test_flush_presents_a_submitted_field_without_a_run_timer(scene, pool):
|
||||
"""Replay stops the run timer; flush must wait for compose and present the field."""
|
||||
widget = FakeWidget()
|
||||
composer = CountingComposer(delay_s=0.02)
|
||||
coordinator = ThreeDCoordinator(widget, composer, pool, max_fps=1)
|
||||
coordinator.set_visible(True)
|
||||
coordinator.activate(scene, "live-A")
|
||||
assert coordinator.submit(_frame(scene, frame_id=3, value=4.0)) == "accepted"
|
||||
# Two blind ticks used to leave the compose in flight when the pool is slow.
|
||||
coordinator.tick()
|
||||
coordinator.tick()
|
||||
assert not widget.frames or not np.allclose(
|
||||
np.asarray(widget.frames[-1].field.scalar), 4.0
|
||||
), "two ticks already presented a slow compose; the flush regression is moot"
|
||||
|
||||
coordinator.flush()
|
||||
assert widget.frames, "flush left the rest model on screen"
|
||||
assert np.allclose(np.asarray(widget.frames[-1].field.scalar), 4.0)
|
||||
assert coordinator._inflight is None # noqa: SLF001
|
||||
assert not coordinator.has_pending_frame()
|
||||
coordinator.close()
|
||||
|
||||
|
||||
def test_flush_presents_the_newer_frame_after_an_overlay_policy_change(scene):
|
||||
"""Replay sets overlay policy then submits; the cursor frame must reach the widget.
|
||||
|
||||
``set_overlay_policy`` dirties settings so a slider change can recompose. If that
|
||||
recompose of the *held* field wins over the mailbox, scrubbing leaves 3D on the
|
||||
first (often empty) depth while 2D moves.
|
||||
"""
|
||||
widget = FakeWidget()
|
||||
coordinator = ThreeDCoordinator(widget, SceneComposer(), ImmediateExecutor())
|
||||
coordinator.set_visible(True)
|
||||
coordinator.activate(scene, "live-A")
|
||||
assert coordinator.submit(_frame(scene, frame_id=1, value=0.0)) == "accepted"
|
||||
coordinator.flush()
|
||||
assert np.allclose(np.asarray(widget.frames[-1].field.scalar), 0.0)
|
||||
|
||||
coordinator.set_overlay_policy(object())
|
||||
assert coordinator.submit(_frame(scene, frame_id=80, value=7.0)) == "accepted"
|
||||
coordinator.flush()
|
||||
assert np.allclose(np.asarray(widget.frames[-1].field.scalar), 7.0), (
|
||||
"the pane stayed on the first field after overlay policy dirtied settings"
|
||||
)
|
||||
assert int(widget.frames[-1].identity.frame_id) == 80
|
||||
coordinator.close()
|
||||
|
||||
|
||||
def test_an_older_request_id_cannot_move_the_picture_backwards(scene):
|
||||
"""R7: request ids are monotonic within a session, and the pane must obey."""
|
||||
widget = FakeWidget()
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.scene3d.composer import SceneComposer
|
||||
from orisys_app_shared.scene3d.contracts import (
|
||||
@@ -152,3 +154,36 @@ def test_both_views_build_their_policy_from_the_same_settings() -> None:
|
||||
# Normalised, so a hand-edited preference cannot invert the hysteresis band.
|
||||
inverted = policy_for_settings(DisplaySettings(flow_on_px=0.1, flow_off_px=0.9))
|
||||
assert inverted.flow_on_px >= inverted.flow_off_px
|
||||
|
||||
|
||||
def test_a_sparse_recovered_field_still_draws_3d_arrows() -> None:
|
||||
"""A recovered analysis field is ``NaN`` off the stored grid; tips land there.
|
||||
|
||||
Handing ``isfinite(flow)`` as the overlay mask required the tip to sit on another
|
||||
stored cell, so replay 3D kept a few rim dots while 2D drew the island grid.
|
||||
"""
|
||||
assets = poc_scene()
|
||||
h, w = assets.shape_hw
|
||||
flow = np.full((h, w, 2), np.nan, dtype=np.float32)
|
||||
flow[1, 1] = (0.9, 0.0)
|
||||
settings = DisplaySettings(
|
||||
flow_arrows_enabled=True,
|
||||
contact_arrow_enabled=False,
|
||||
flow_grid_spacing=1,
|
||||
flow_on_px=0.4,
|
||||
flow_off_px=0.3,
|
||||
max_arrows=50,
|
||||
)
|
||||
dropped = SceneComposer().compose(
|
||||
assets,
|
||||
replace(
|
||||
_depth_frame(assets, flow=flow),
|
||||
flow_valid_mask=freeze(np.isfinite(flow[..., 0]), dtype=np.bool_),
|
||||
),
|
||||
settings,
|
||||
)
|
||||
n_dropped = 0 if dropped.flow_arrows is None else int(dropped.flow_arrows.origins.shape[0])
|
||||
drawn = SceneComposer().compose(assets, _depth_frame(assets, flow=flow), settings)
|
||||
n_drawn = 0 if drawn.flow_arrows is None else int(drawn.flow_arrows.origins.shape[0])
|
||||
assert n_drawn >= 1, "the island mask still produced no 3D arrows"
|
||||
assert n_drawn > n_dropped, (n_drawn, n_dropped)
|
||||
|
||||
@@ -208,6 +208,36 @@ def test_a_platform_without_a_context_says_so_instead_of_showing_an_empty_view(
|
||||
|
||||
|
||||
# --------------------------------------------------------- one state, two controls
|
||||
def test_an_analysis_packet_ticks_the_3d_pane(make_window, monkeypatch):
|
||||
"""Replay has no run timer: submit must flush the compose so the frame is presented."""
|
||||
window = make_window(sensor_type="finger")
|
||||
try:
|
||||
flushes: list[int] = []
|
||||
submitted: list[object] = []
|
||||
policies: list[object] = []
|
||||
|
||||
class _View:
|
||||
def set_overlay_policy(self, policy) -> None:
|
||||
policies.append(policy)
|
||||
|
||||
def submit_packet(self, packet) -> None:
|
||||
submitted.append(packet)
|
||||
|
||||
def flush(self) -> None:
|
||||
flushes.append(1)
|
||||
|
||||
# A full Scene3DView needs GL; only the submit/flush seam is under test here.
|
||||
window.mesh3d_panel._view = _View() # noqa: SLF001
|
||||
monkeypatch.setattr(window, "_connect_analysis_display", lambda panel: True)
|
||||
window.show_analysis_packet(object())
|
||||
assert submitted, "the analysis packet never reached the 3D producer"
|
||||
assert policies, "the overlay policy was not handed to the 3D view"
|
||||
assert flushes, "the pane was not flushed after submit"
|
||||
finally:
|
||||
window.mesh3d_panel._view = None # noqa: SLF001
|
||||
window.close()
|
||||
|
||||
|
||||
def test_the_pane_and_the_view_menu_are_the_same_flag(make_window):
|
||||
window = make_window(sensor_type="finger")
|
||||
try:
|
||||
|
||||
@@ -1509,6 +1509,33 @@ def test_the_source_picker_follows_the_view(make_window):
|
||||
_finish(window, stuck)
|
||||
|
||||
|
||||
def test_live_run_refuses_a_video_source(make_window, monkeypatch):
|
||||
"""Live acquires from a camera; a leftover video mode must not start a chain."""
|
||||
from orisys_app_shared.i18n import t
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
window = make_window(sensor_type="finger", source_mode="camera")
|
||||
try:
|
||||
window.brand_bar.btn_live.click()
|
||||
QApplication.processEvents()
|
||||
window.toolbar.set_video_path(r"C:\tmp\demo.mp4")
|
||||
window._set_source_mode("video")
|
||||
QApplication.processEvents()
|
||||
assert not window.toolbar.btn_run.isEnabled(), "Run is offered for a video file in live"
|
||||
called: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
"orisys_app_shared.acquisition.sdk_worker.orisys_importable",
|
||||
lambda: called.append("import") or (True, ""),
|
||||
)
|
||||
window.toolbar.set_run_checked(True)
|
||||
window._start_run()
|
||||
assert called == [], "a video source reached the acquisition path"
|
||||
assert window.toolbar.btn_run.isChecked() is False
|
||||
assert t("toolbar.run_video_in_replay") in window.statusBar().currentMessage()
|
||||
finally:
|
||||
window.close()
|
||||
|
||||
|
||||
def test_the_nav_items_switch_the_main_window_layout(make_window):
|
||||
window, stuck = _running_window(make_window)
|
||||
for index in range(60):
|
||||
|
||||
Reference in New Issue
Block a user