Read the analysis cache before refusing a pass, and own the cache root
Opening a recording while the session that recorded it was still live never reached ``stored_result()``, so the panel reported no result over a recording whose result was on disk. The guard now follows the cache read, results move to the app's derived-data root (swept at startup, the old in-recording root ignored), and the panel states which of the four no-result states it is in, with the pass's own reason. Verified: apps/studio 462/0/18, shared 431/0/1, autocal 358, ui_check 628/0, both gates clean, real-GL snapshots per state.
This commit is contained in:
@@ -35,6 +35,10 @@ from orisys_app_shared.acquisition.image_backend import (
|
||||
DEPTH,
|
||||
DERIVED_MODES,
|
||||
RAW_NOTE_KEY,
|
||||
RESULT_FAILED,
|
||||
RESULT_IMPOSSIBLE,
|
||||
RESULT_NONE,
|
||||
RESULT_RUNNING,
|
||||
SOURCE_ANALYSIS,
|
||||
SOURCE_REPLAY,
|
||||
ImageFrame,
|
||||
@@ -498,7 +502,9 @@ class _Engine:
|
||||
return self._reader
|
||||
|
||||
def directory(self):
|
||||
return self._reader.directory
|
||||
# ``None`` for a bare video, exactly as ``ReplayEngine.directory`` answers: the
|
||||
# controller's "is there anything here a pass could read?" is this call.
|
||||
return None if self._reader is None else self._reader.directory
|
||||
|
||||
def video_path(self):
|
||||
return None
|
||||
@@ -743,6 +749,177 @@ def test_the_controller_redraws_on_a_display_change_and_runs_no_pass(pass_root):
|
||||
|
||||
|
||||
# ------------------------------------------------------------ analysis on open
|
||||
def test_a_running_session_does_not_hide_a_stored_result(pass_root):
|
||||
"""A result on disk is served while a session is open: reading it opens no chain.
|
||||
|
||||
The defect this pins: ``on_analysis_requested`` asked "is a session or a preview
|
||||
open?" *before* it asked "is a result already stored?". Serving a result is a file
|
||||
read — no SDK chain, no device, no working directory — so the one-chain-at-a-time
|
||||
rule never applied to it, but the guard refused the whole call anyway. The operator
|
||||
who recorded something and opened it while the acquisition session was still live
|
||||
(a paused session keeps the device, so it counts) was shown "this recording has no
|
||||
analysis result" over a recording whose arrows were sitting in the cache, and had to
|
||||
stop the session and reopen the recording to see them.
|
||||
"""
|
||||
_require_inputs()
|
||||
calls: list[str] = []
|
||||
|
||||
def factory(recording: Path, cancelled, on_progress):
|
||||
calls.append(str(recording))
|
||||
return _service(pass_root)
|
||||
|
||||
for live in ("running", "preview", "winding_down"):
|
||||
calls.clear()
|
||||
host = _Host(service_factory=factory)
|
||||
if live == "running":
|
||||
host._running = True
|
||||
elif live == "preview":
|
||||
host._preview_open = True
|
||||
else:
|
||||
host._session_active = True
|
||||
panel = _ImagePanel()
|
||||
controller, host, engine, parts = _controller(host=host, image_panel=panel)
|
||||
assert controller.load_recording(RECORDING) is True
|
||||
assert controller.available_result() is not None, (
|
||||
f"a cached result was not served while a session was {live}"
|
||||
)
|
||||
assert not controller.analysis_running(), "a pass ran over a stored result"
|
||||
assert calls == [str(RECORDING)], "the stored result was not even looked for"
|
||||
assert parts[4].states[-1] == "reused", "the surface did not report the reuse"
|
||||
assert controller.analysis_error() == ""
|
||||
# What reaches the 2D sink is the cached measurement, not "there is nothing".
|
||||
host.frames.clear()
|
||||
panel.set_view_mode(DEPTH)
|
||||
controller.render_frame()
|
||||
drawn = host.frames[-1]
|
||||
assert drawn.result_available is True
|
||||
assert drawn.depth is not None, "the depth view was not served from the cache"
|
||||
assert plan_image(drawn).note_key == "", "the panel still says it has no result"
|
||||
|
||||
|
||||
def test_a_pass_is_refused_while_a_session_is_open_and_says_so(tmp_path, qt_app):
|
||||
"""With nothing cached, the refusal is *state*: the panel states it, with the reason.
|
||||
|
||||
The pass really cannot run next to a session (one SDK chain and one working directory
|
||||
at a time), so the fix is not to run it anyway — it is to stop the refusal from being
|
||||
a silent no-op whose only trace is a status line that expires. Before this, the four
|
||||
states below were one line, and three of them were wrong.
|
||||
"""
|
||||
_require_inputs()
|
||||
empty = tmp_path / "cache"
|
||||
empty.mkdir()
|
||||
|
||||
def factory(recording: Path, cancelled, on_progress):
|
||||
return _service(empty, cancelled=cancelled, on_progress=on_progress)
|
||||
|
||||
host = _Host(service_factory=factory)
|
||||
host._running = True
|
||||
panel = _ImagePanel()
|
||||
controller, host, engine, parts = _controller(host=host, image_panel=panel)
|
||||
assert controller.load_recording(RECORDING) is True
|
||||
|
||||
assert not controller.analysis_running(), "a pass was started next to a session"
|
||||
assert parts[4].states == [], "the progress surface claimed a pass"
|
||||
state, reason = controller.result_state()
|
||||
assert state == RESULT_FAILED, f"the refusal was reported as {state!r}"
|
||||
assert reason == t("analysis.blocked") and reason, "the refusal lost its reason"
|
||||
assert controller.analysis_error() == reason
|
||||
|
||||
host.frames.clear()
|
||||
controller.render_frame()
|
||||
drawn = host.frames[-1]
|
||||
assert drawn.result_available is False
|
||||
plan = plan_image(drawn)
|
||||
assert plan.note_key == "panels.replay_analysis_failed"
|
||||
assert plan.note_reason == reason, "the line the panel draws lost the reason"
|
||||
assert plan.note_key != RAW_NOTE_KEY, (
|
||||
"the panel promised a pass that was refused before it started"
|
||||
)
|
||||
assert plan.available_modes == ("recorded",)
|
||||
|
||||
|
||||
def test_the_four_no_result_states_are_four_different_lines(tmp_path, qt_app):
|
||||
"""No result yet, running, failed (with its reason), impossible — all four reachable.
|
||||
|
||||
Each is a different fact and a different next action, so each is a different line:
|
||||
a pass is coming, a pass is going, a pass is not coming and here is why, or no pass
|
||||
can ever serve this frame. ``none`` is what the panel draws for a recording whose
|
||||
analysis has not been asked for yet — the redraw the engine's own ``recording_changed``
|
||||
makes inside ``load_recording``, before ``on_analysis_requested`` runs. ``failed`` is
|
||||
reached by a pass that really fails (a chain whose configuration file is absent),
|
||||
because the reason on the line has to be the pass's own, not one a test invented.
|
||||
"""
|
||||
_require_inputs()
|
||||
empty = tmp_path / "cache"
|
||||
empty.mkdir()
|
||||
|
||||
def working(recording: Path, cancelled, on_progress):
|
||||
return _service(empty, cancelled=cancelled, on_progress=on_progress)
|
||||
|
||||
def broken(recording: Path, cancelled, on_progress):
|
||||
# A chain the SDK cannot open: the configuration file does not exist. This is the
|
||||
# shape of every "the pass ran and failed" the operator can meet — a moved
|
||||
# install, a config edited away — and the reason it produces is the pass's own.
|
||||
return AnalysisService(
|
||||
RECORDING,
|
||||
chain=ChainSpec(sensor_type="box", config_ref=tmp_path / "absent-sensor.json"),
|
||||
cache=ResultCache(empty),
|
||||
cancelled=cancelled,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
|
||||
# none: the recording is open, and nothing has asked for a pass yet.
|
||||
host = _Host(service_factory=working)
|
||||
panel = _ImagePanel()
|
||||
controller, host, engine, parts = _controller(host=host, image_panel=panel)
|
||||
assert engine.open_recording(RECORDING) is True
|
||||
assert controller.result_state() == (RESULT_NONE, "")
|
||||
host.frames.clear()
|
||||
controller.render_frame()
|
||||
assert plan_image(host.frames[-1]).note_key == RAW_NOTE_KEY
|
||||
|
||||
# running: the pass is in flight and the panel says so instead of promising one.
|
||||
controller.on_analysis_requested()
|
||||
assert controller.analysis_running(), "no pass was started"
|
||||
assert controller.result_state() == (RESULT_RUNNING, "")
|
||||
host.frames.clear()
|
||||
controller.render_frame()
|
||||
assert plan_image(host.frames[-1]).note_key == "panels.replay_analysis_running"
|
||||
assert controller.wait_for_analysis(120)
|
||||
_flush()
|
||||
|
||||
# failed: a pass that ended without a result carries its own reason to the panel.
|
||||
failing = _Host(service_factory=broken)
|
||||
failing_panel = _ImagePanel()
|
||||
failed_ctl, failing, _, failed_parts = _controller(
|
||||
host=failing, image_panel=failing_panel
|
||||
)
|
||||
assert failed_ctl.load_recording(RECORDING) is True
|
||||
assert failed_ctl.wait_for_analysis(120), "the failing pass never finished"
|
||||
_flush()
|
||||
state, reason = failed_ctl.result_state()
|
||||
assert reason, "a failed pass left no reason to show"
|
||||
assert failed_parts[4].states[-1] == "failed", failed_parts[4].states
|
||||
if state != RESULT_FAILED: # a box chain that somehow opened: then it has a result
|
||||
pytest.fail(f"the failed pass is reported as {state!r}")
|
||||
failing.frames.clear()
|
||||
failed_ctl.render_frame()
|
||||
plan = plan_image(failing.frames[-1])
|
||||
assert plan.note_key == "panels.replay_analysis_failed"
|
||||
assert plan.note_reason == reason, "the line the panel draws lost the reason"
|
||||
|
||||
# impossible: a bare video has no session metadata, so no pass can serve it at all.
|
||||
bare = tmp_path / "clip.mp4"
|
||||
bare.write_bytes(b"\0" * 8)
|
||||
engine._reader = None
|
||||
controller._toolbar.set_video_path(str(bare))
|
||||
assert controller.recording_directory() is None
|
||||
assert controller.result_state() == (RESULT_IMPOSSIBLE, "")
|
||||
host.frames.clear()
|
||||
controller.render_frame()
|
||||
assert plan_image(host.frames[-1]).note_key == "panels.replay_analysis_unavailable"
|
||||
|
||||
|
||||
def test_opening_a_recording_with_a_stored_result_runs_no_pass(pass_root):
|
||||
"""Analysis on open reuses a compatible result: no progress, no pass."""
|
||||
_require_inputs()
|
||||
|
||||
@@ -36,6 +36,7 @@ from orisys_app_shared.acquisition.image_backend import (
|
||||
DEPTH,
|
||||
DERIVED_MODES,
|
||||
RECORDED,
|
||||
plan_image,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import DisplaySettings
|
||||
@@ -87,8 +88,14 @@ def _copy_recording(destination: Path) -> Path:
|
||||
return destination
|
||||
|
||||
|
||||
def _run_the_pass(recording: Path):
|
||||
"""One real pass, with the chain the *application* derives from the recording."""
|
||||
def _run_the_pass(recording: Path, root: Path | None = None):
|
||||
"""One real pass, with the chain the *application* derives from the recording.
|
||||
|
||||
``root`` names the cache explicitly because the app's own root is the user's
|
||||
derived-data directory: a test must never write there, and the window it then opens
|
||||
must find the result, so the root is patched to a temporary one for the whole test.
|
||||
"""
|
||||
from orisys_app_shared.analysis.cache import ResultCache
|
||||
from orisys_app_shared.analysis.service import AnalysisService, chain_spec_for
|
||||
from orisys_app_shared.record import RecordingReader
|
||||
|
||||
@@ -97,23 +104,76 @@ def _run_the_pass(recording: Path):
|
||||
chain = chain_spec_for(reader.session())
|
||||
finally:
|
||||
reader.close()
|
||||
return AnalysisService(recording, chain=chain).run()
|
||||
cache = None if root is None else ResultCache(root)
|
||||
return AnalysisService(recording, chain=chain, cache=cache).run()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def stored_recording(tmp_path_factory) -> Path:
|
||||
"""A copy of the evidence recording whose own cache already holds a result."""
|
||||
def cache_root(tmp_path_factory):
|
||||
"""The analysis cache for this module's tests, in place of the user's own.
|
||||
|
||||
The app resolves its cache root at construction from
|
||||
``paths.analysis_cache_dir()``, so pointing that at a temporary directory is enough
|
||||
for the *window* to write and read there — which is what keeps a test from sweeping
|
||||
or filling the real ``%LOCALAPPDATA%`` cache, and what makes "the window found the
|
||||
stored result" a real statement rather than a coincidence of two roots.
|
||||
"""
|
||||
from orisys_app_shared import paths as paths_module
|
||||
|
||||
root = tmp_path_factory.mktemp("replay-wiring-cache") / "analysis-cache"
|
||||
root.mkdir(parents=True)
|
||||
patch = pytest.MonkeyPatch()
|
||||
patch.setattr(paths_module, "analysis_cache_dir", lambda: root)
|
||||
yield root
|
||||
patch.undo()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def stored_recording(tmp_path_factory, cache_root) -> Path:
|
||||
"""A copy of the evidence recording whose cache already holds a result."""
|
||||
_require_inputs()
|
||||
recording = _copy_recording(tmp_path_factory.mktemp("replay-wiring") / "recording")
|
||||
outcome = _run_the_pass(recording)
|
||||
outcome = _run_the_pass(recording, cache_root)
|
||||
assert outcome.complete, outcome.reason
|
||||
assert outcome.frames_written > 0
|
||||
return recording
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def window(make_window, qt_app):
|
||||
"""The application, in the replay view, as the operator meets it."""
|
||||
def window(make_window, qt_app, cache_root):
|
||||
"""The application, in the replay view, as the operator meets it.
|
||||
|
||||
It shares the module's cache root, so a test may pre-place a result (or reuse the one
|
||||
``stored_recording`` wrote) and have the window find it — which is the whole point of
|
||||
a root that is not inside the recording: the same recording is the same result.
|
||||
"""
|
||||
return _replay_window(make_window)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fresh_cache(tmp_path, monkeypatch):
|
||||
"""A cache root of this test's own.
|
||||
|
||||
A copy of the recording *is* the same recording to the cache (the key carries the
|
||||
identity, not the path), so a test that wants a pass to run needs a root nothing has
|
||||
written to — otherwise it would be served another test's result, which is correct
|
||||
behaviour and the wrong fixture.
|
||||
"""
|
||||
from orisys_app_shared import paths as paths_module
|
||||
|
||||
root = tmp_path / "analysis-cache"
|
||||
root.mkdir(exist_ok=True)
|
||||
monkeypatch.setattr(paths_module, "analysis_cache_dir", lambda: root)
|
||||
return root
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def own_window(make_window, qt_app, fresh_cache):
|
||||
"""A window whose analysis cache belongs to this test alone."""
|
||||
return _replay_window(make_window)
|
||||
|
||||
|
||||
def _replay_window(make_window):
|
||||
window = make_window(sensor_type="box", source_mode="camera")
|
||||
window.resize(1440, 880)
|
||||
window.show()
|
||||
@@ -194,6 +254,20 @@ def _cells(rows, cols) -> set:
|
||||
return {(int(row), int(col)) for row, col in zip(np.asarray(rows), np.asarray(cols))}
|
||||
|
||||
|
||||
def _entry_set(directory: Path) -> set:
|
||||
"""Every entry under ``directory``, as (relative path, size).
|
||||
|
||||
The owner's constraint is about the *recording*, so it is asserted on the recording's
|
||||
own file set: a result written beside the footage changes it, and a size is included
|
||||
so an in-place rewrite cannot pass as "the same entries".
|
||||
"""
|
||||
found = set()
|
||||
for path in Path(directory).rglob("*"):
|
||||
relative = str(path.relative_to(directory))
|
||||
found.add((relative, -1 if path.is_dir() else path.stat().st_size))
|
||||
return found
|
||||
|
||||
|
||||
def _active_cells(samples) -> set:
|
||||
return {
|
||||
(int(row), int(col))
|
||||
@@ -388,14 +462,16 @@ def test_playback_draws_the_cached_frame_not_the_video(
|
||||
)
|
||||
|
||||
|
||||
def test_a_finished_pass_puts_the_cache_on_screen(tmp_path, window, monkeypatch):
|
||||
def test_a_finished_pass_puts_the_cache_on_screen(tmp_path, own_window, monkeypatch, fresh_cache):
|
||||
"""The other trigger: the pass completes and the panel serves what it stored.
|
||||
|
||||
Nothing is pre-placed here, so the window runs the pass over the copy (a box chain:
|
||||
a couple of seconds for this recording) and the *completion* is what has to leave the
|
||||
derived view on screen — with the cursor still on the recording.
|
||||
"""
|
||||
window = own_window
|
||||
recording = _copy_recording(tmp_path / "recording")
|
||||
before = _entry_set(recording)
|
||||
_sink_frames(window, monkeypatch)
|
||||
_open(window, recording, monkeypatch)
|
||||
|
||||
@@ -424,6 +500,65 @@ def test_a_finished_pass_puts_the_cache_on_screen(tmp_path, window, monkeypatch)
|
||||
"the finished result is not what the panel draws"
|
||||
)
|
||||
|
||||
# The owner's constraint, asserted where it can actually be broken: the recording
|
||||
# directory came back exactly as it was, and the result went to the app's own cache.
|
||||
assert _entry_set(recording) == before, (
|
||||
"opening and analysing a recording wrote inside the recording folder"
|
||||
)
|
||||
assert not (recording / "analysis").exists()
|
||||
assert Path(session.directory).parent == Path(fresh_cache), (
|
||||
f"the result was not written to the app's cache root: {session.directory}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_running_session_does_not_hide_a_stored_result(
|
||||
stored_recording, window, monkeypatch
|
||||
):
|
||||
"""A live session must not cost the operator the cached views.
|
||||
|
||||
The operator's own sequence: record something, and open the recording while the
|
||||
acquisition session is still live (paused sessions keep the device, so they count).
|
||||
Reading a stored result opens no SDK chain, so the session is no reason to refuse it —
|
||||
and refusing it left the panel saying "this recording has no analysis result" over a
|
||||
recording whose arrows were in the cache, which is the report this pins. Before the
|
||||
fix: the note was shown and the mode selector offered ``recorded`` alone.
|
||||
"""
|
||||
import ui_check
|
||||
|
||||
window._ensure_workers = lambda: None
|
||||
stuck = ui_check._StuckWorker()
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window._start_run()
|
||||
window._run_timer.stop()
|
||||
window._set_phase("running")
|
||||
QApplication.processEvents()
|
||||
assert window._running and window._session_active, "no session was started"
|
||||
|
||||
captured = _sink_frames(window, monkeypatch)
|
||||
_open(window, stored_recording, monkeypatch)
|
||||
|
||||
session = window.replay_ctl.available_result()
|
||||
assert session is not None, (
|
||||
"a running session hid a stored result the app did not have to compute"
|
||||
)
|
||||
assert window.analysis_progress.state() == "reused", "a pass ran beside a session"
|
||||
assert window.replay.mode() == MODE_RECORDING
|
||||
assert window.arrow_panel.note_label.text() == "", (
|
||||
"the panel says it has no result while it is drawing one"
|
||||
)
|
||||
_set_mode(window, DEPTH)
|
||||
frame_id = session.frame_id_at(window.replay.position())
|
||||
expected = _expected_depth(window, session.frame(frame_id))
|
||||
assert expected is not None
|
||||
assert np.array_equal(_panel_bgr(window), expected), (
|
||||
"the cache was not what the panel drew"
|
||||
)
|
||||
assert captured[-1].result_available is True
|
||||
|
||||
window.replay_ctl.stop_activity()
|
||||
stuck.release()
|
||||
|
||||
|
||||
def test_the_mode_selector_is_the_trigger_for_a_derived_view(
|
||||
stored_recording, window, monkeypatch
|
||||
@@ -455,7 +590,7 @@ def test_the_mode_selector_is_the_trigger_for_a_derived_view(
|
||||
|
||||
|
||||
def test_the_display_settings_still_redraw_from_the_cache(
|
||||
stored_recording, window, monkeypatch
|
||||
stored_recording, window, monkeypatch, cache_root
|
||||
):
|
||||
"""A threshold change is a read: it redraws the stored frame and writes nothing."""
|
||||
from orisys_app_shared.analysis.cache import scan_results
|
||||
@@ -465,7 +600,6 @@ def test_the_display_settings_still_redraw_from_the_cache(
|
||||
assert session is not None
|
||||
_set_mode(window, ARROW_PURE)
|
||||
|
||||
cache_root = Path(stored_recording) / "analysis"
|
||||
before = {entry.directory for entry in scan_results(cache_root)}
|
||||
captured = _sink_frames(window, monkeypatch)
|
||||
window.replay_ctl.on_display_settings(DisplaySettings(flow_on_px=0.9))
|
||||
@@ -482,7 +616,7 @@ def test_the_display_settings_still_redraw_from_the_cache(
|
||||
|
||||
|
||||
def test_a_setting_that_moves_the_grid_is_still_refused(
|
||||
stored_recording, window, monkeypatch
|
||||
stored_recording, window, monkeypatch, cache_root
|
||||
):
|
||||
"""The stored candidate locations cannot be re-gridded; the frame is drawn anyway."""
|
||||
from orisys_app_shared.analysis.cache import scan_results
|
||||
@@ -492,7 +626,6 @@ def test_a_setting_that_moves_the_grid_is_still_refused(
|
||||
assert session is not None
|
||||
_set_mode(window, ARROW_PURE)
|
||||
before_threshold = session.threshold()
|
||||
cache_root = Path(stored_recording) / "analysis"
|
||||
before = {entry.directory for entry in scan_results(cache_root)}
|
||||
|
||||
captured = _sink_frames(window, monkeypatch)
|
||||
@@ -505,3 +638,105 @@ def test_a_setting_that_moves_the_grid_is_still_refused(
|
||||
assert captured, "the frame was not redrawn from the result's own grid"
|
||||
assert captured[-1].frame_id == session.frame_id_at(window.replay.position())
|
||||
assert {entry.directory for entry in scan_results(cache_root)} == before
|
||||
|
||||
|
||||
# --------------------------------------------------- what the panel says without one
|
||||
def test_the_panel_says_the_pass_is_running(tmp_path, own_window, monkeypatch):
|
||||
"""While the pass runs, the line must not tell the operator to start one.
|
||||
|
||||
The old line promised "opening a recording runs a pass" *during* the pass, which is
|
||||
worse than useless on a ten-minute recording: it reads as "nothing is happening".
|
||||
"""
|
||||
from orisys_app_shared.acquisition.image_backend import RESULT_RUNNING
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window = own_window
|
||||
recording = _copy_recording(tmp_path / "recording")
|
||||
captured = _sink_frames(window, monkeypatch)
|
||||
_open(window, recording, monkeypatch)
|
||||
|
||||
assert window.replay_ctl.analysis_running(), "the pass finished before it was observed"
|
||||
assert window.replay_ctl.result_state() == (RESULT_RUNNING, "")
|
||||
assert captured[-1].result_state == RESULT_RUNNING
|
||||
assert window.arrow_panel.note_label.text() == t("panels.replay_analysis_running"), (
|
||||
"the panel did not say the pass is running"
|
||||
)
|
||||
assert window.replay_ctl.wait_for_analysis(180.0)
|
||||
QApplication.processEvents()
|
||||
assert window.arrow_panel.note_label.text() == "", (
|
||||
"the completed pass left a note behind"
|
||||
)
|
||||
|
||||
|
||||
def test_the_panel_states_a_failed_pass_with_its_reason(
|
||||
tmp_path, own_window, monkeypatch, fresh_cache
|
||||
):
|
||||
"""A pass that ends without a result says so, with the pass's own reason.
|
||||
|
||||
Reached through the window's own open action: only the chain is made unopenable (a
|
||||
configuration file that is not there — the shape of a moved install), so the worker
|
||||
thread, the completion slot and the panel are all the real ones. The check that
|
||||
mattered in the report is the last one: the panel and the progress surface agree.
|
||||
"""
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
RAW_NOTE_KEY,
|
||||
RESULT_FAILED,
|
||||
)
|
||||
from orisys_app_shared.analysis.cache import ResultCache
|
||||
from orisys_app_shared.analysis.service import AnalysisService, ChainSpec
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window = own_window
|
||||
|
||||
def broken_service(recording, cancelled=None, *, on_progress=None):
|
||||
return AnalysisService(
|
||||
recording,
|
||||
chain=ChainSpec(sensor_type="box", config_ref=tmp_path / "absent-sensor.json"),
|
||||
cache=ResultCache(fresh_cache),
|
||||
cancelled=cancelled,
|
||||
on_progress=on_progress,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(window, "_analysis_service", broken_service)
|
||||
recording = _copy_recording(tmp_path / "recording")
|
||||
_open(window, recording, monkeypatch)
|
||||
assert window.replay_ctl.wait_for_analysis(180.0), "the failing pass never finished"
|
||||
QApplication.processEvents()
|
||||
|
||||
state, reason = window.replay_ctl.result_state()
|
||||
assert reason, "a failed pass left no reason to show"
|
||||
assert state == RESULT_FAILED, f"the failed pass is reported as {state!r}"
|
||||
assert window.analysis_progress.state() == "failed"
|
||||
assert window.arrow_panel.note_label.text() == t(
|
||||
"panels.replay_analysis_failed", reason=reason
|
||||
), "the panel's line did not carry the pass's reason"
|
||||
assert reason in window.arrow_panel.note_label.text()
|
||||
assert t(RAW_NOTE_KEY) not in window.arrow_panel.note_label.text(), (
|
||||
"the panel promised the automatic pass that had just failed"
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
from orisys_app_shared.acquisition.image_backend import (
|
||||
RAW_NOTE_KEY,
|
||||
RESULT_IMPOSSIBLE,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window = own_window
|
||||
recording = _copy_recording(tmp_path / "recording")
|
||||
clip = tmp_path / "clip.mp4"
|
||||
shutil.copy2(recording / "raw_0001.mp4", clip)
|
||||
|
||||
captured = _sink_frames(window, monkeypatch)
|
||||
assert window.replay_ctl.load_video(clip) is True
|
||||
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 window.arrow_panel.note_label.text() == t(
|
||||
"panels.replay_analysis_unavailable"
|
||||
)
|
||||
assert t(RAW_NOTE_KEY) not in window.arrow_panel.note_label.text()
|
||||
assert plan_image(captured[-1]).note_key == "panels.replay_analysis_unavailable"
|
||||
|
||||
Reference in New Issue
Block a user