fixed label under 3d view
This commit is contained in:
@@ -7,7 +7,9 @@ whole session (Qt forbids creating more than one).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -77,3 +79,98 @@ def visible_descendants(widget):
|
||||
continue
|
||||
if child.isVisible():
|
||||
yield child
|
||||
|
||||
|
||||
def show_window(window, size):
|
||||
"""Show a window at ``size`` and flush pending Qt events."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
window.resize(*size)
|
||||
window.show()
|
||||
QApplication.processEvents()
|
||||
return window
|
||||
|
||||
|
||||
def running_window(make_window):
|
||||
"""Return a running window backed by a controllable test worker."""
|
||||
import ui_check
|
||||
|
||||
window = show_window(
|
||||
make_window(sensor_type="box", source_mode="camera"),
|
||||
(1440, 880),
|
||||
)
|
||||
stuck = ui_check._StuckWorker()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window._start_run()
|
||||
window._run_timer.stop()
|
||||
window._set_phase("running")
|
||||
return window, stuck
|
||||
|
||||
|
||||
def finish_run(window, stuck):
|
||||
"""Stop a test run and release its controllable worker."""
|
||||
window._stop_run(reason="user_stop")
|
||||
stuck.release()
|
||||
|
||||
|
||||
_RECORD_SCRATCH = (
|
||||
Path(__file__).resolve().parents[1] / "outputs" / "_test_record"
|
||||
)
|
||||
_RECORD_3D_SCRATCH = (
|
||||
Path(__file__).resolve().parents[1] / "outputs" / "_test_record_3d"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_root():
|
||||
"""A clean workspace-local directory for recording tests."""
|
||||
shutil.rmtree(_RECORD_SCRATCH, ignore_errors=True)
|
||||
_RECORD_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
yield _RECORD_SCRATCH
|
||||
shutil.rmtree(_RECORD_SCRATCH, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def next_record_root():
|
||||
"""Create isolated recording directories for comparisons within one test."""
|
||||
shutil.rmtree(_RECORD_3D_SCRATCH, ignore_errors=True)
|
||||
_RECORD_3D_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
counter = itertools.count()
|
||||
|
||||
def _next() -> Path:
|
||||
path = _RECORD_3D_SCRATCH / f"run{next(counter)}"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
yield _next
|
||||
shutil.rmtree(_RECORD_3D_SCRATCH, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def replay_recording(record_root):
|
||||
"""One short finished recording (video, forces, frames, and metadata)."""
|
||||
import ui_check
|
||||
from orisys_app_shared.record import ExperimentRecorder, RecordConfig
|
||||
|
||||
recorder = ExperimentRecorder(
|
||||
RecordConfig(video=True, fps=30.0, segment_seconds=30.0)
|
||||
)
|
||||
recording = recorder.start(
|
||||
root=record_root,
|
||||
session_id=1,
|
||||
metadata={
|
||||
"device": {
|
||||
"sensor_type": "box",
|
||||
"config_ref": str(record_root / "sensor.json"),
|
||||
"config_sha256": "recorded",
|
||||
},
|
||||
"versions": {"sdk": "0.5.1"},
|
||||
"depth": {"stored": False, "replay": "recompute"},
|
||||
},
|
||||
)
|
||||
for index in range(60):
|
||||
recorder.on_packet(ui_check._fake_packet(index))
|
||||
recorder.stop()
|
||||
yield Path(recording)
|
||||
|
||||
@@ -6,6 +6,8 @@ import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_DIR = APP_ROOT / "src"
|
||||
LOCALES_DIR = APP_ROOT / "config" / "locales"
|
||||
@@ -69,13 +71,6 @@ def test_preference_defaults_match_store_defaults():
|
||||
}
|
||||
|
||||
|
||||
def test_locale_files_are_valid_utf8_json():
|
||||
for name in ("zh_CN", "en_US"):
|
||||
data = _load_locale(name)
|
||||
assert isinstance(data, dict)
|
||||
assert data.get("app", {}).get("title")
|
||||
|
||||
|
||||
#: Sequences that only ever appear when UTF-8 text was decoded as a single-byte
|
||||
#: codepage and then re-encoded. A ``Get-Content -Raw | Set-Content`` round trip
|
||||
#: in Windows PowerShell does exactly that and silently mangles every non-ASCII
|
||||
@@ -92,19 +87,22 @@ _MOJIBAKE = (
|
||||
)
|
||||
|
||||
|
||||
def test_locale_files_are_utf8_without_bom_and_not_mojibake():
|
||||
for name in ("zh_CN", "en_US"):
|
||||
path = LOCALES_DIR / f"{name}.json"
|
||||
raw = path.read_bytes()
|
||||
assert not raw.startswith(b"\xef\xbb\xbf"), f"{name}.json starts with a BOM"
|
||||
text = raw.decode("utf-8") # raises if it is not valid UTF-8
|
||||
@pytest.mark.parametrize("name", ("zh_CN", "en_US"))
|
||||
def test_locale_file_is_valid_utf8_json_without_mojibake(name):
|
||||
path = LOCALES_DIR / f"{name}.json"
|
||||
raw = path.read_bytes()
|
||||
assert not raw.startswith(b"\xef\xbb\xbf"), f"{name}.json starts with a BOM"
|
||||
text = raw.decode("utf-8") # raises if it is not valid UTF-8
|
||||
data = json.loads(text)
|
||||
assert isinstance(data, dict)
|
||||
assert data.get("app", {}).get("title")
|
||||
|
||||
for sequence in _MOJIBAKE:
|
||||
assert sequence not in text, (
|
||||
f"{name}.json contains {sequence!r}: the file was decoded with the "
|
||||
"wrong encoding. Rewrite it with an explicit UTF-8 writer instead "
|
||||
"of a PowerShell Get-Content/Set-Content round trip."
|
||||
)
|
||||
for sequence in _MOJIBAKE:
|
||||
assert sequence not in text, (
|
||||
f"{name}.json contains {sequence!r}: the file was decoded with the "
|
||||
"wrong encoding. Rewrite it with an explicit UTF-8 writer instead "
|
||||
"of a PowerShell Get-Content/Set-Content round trip."
|
||||
)
|
||||
|
||||
|
||||
def test_chinese_locale_actually_contains_chinese():
|
||||
|
||||
@@ -40,12 +40,14 @@ def panel(qt_app):
|
||||
# --------------------------------------------------------------------- the pane
|
||||
|
||||
|
||||
def test_the_pane_holds_a_legend_and_no_settings(qt_app, panel):
|
||||
def test_the_pane_holds_a_legend_and_no_settings(panel):
|
||||
"""What is on the pane has to be readable, not adjustable."""
|
||||
# The readouts are part of the pane itself...
|
||||
assert panel.legend_label.parent() is panel
|
||||
assert panel.source_label.parent() is panel
|
||||
assert panel.scale_bar.parent() is panel
|
||||
assert panel.scale_lower_label.parent() is panel
|
||||
assert panel.scale_upper_label.parent() is panel
|
||||
# ...and every setting is in one block that the pane never shows.
|
||||
assert panel.controls_widget.parent() is panel
|
||||
assert not panel.controls_widget.isVisible()
|
||||
@@ -64,7 +66,7 @@ def test_the_pane_holds_a_legend_and_no_settings(qt_app, panel):
|
||||
assert panel.stats_label.parent() is panel.controls_widget
|
||||
|
||||
|
||||
def test_the_settings_dialog_hosts_the_panes_own_controls(qt_app, panel):
|
||||
def test_the_settings_dialog_hosts_the_panes_own_controls(panel):
|
||||
"""One set of widgets either way: the dialog moves them, it does not copy them."""
|
||||
from orisys_studio.scene3d.settings_dialog import Scene3dSettingsDialog
|
||||
|
||||
@@ -96,6 +98,10 @@ def test_the_pane_header_offers_the_two_actions(qt_app):
|
||||
"""A gear and a view reset: the two things that belong to *looking*."""
|
||||
from orisys_studio.ui.mesh3d_panel import Mesh3dPanel
|
||||
|
||||
assert "reset_view" in icons.NAMES
|
||||
for theme in ("dark", "light"):
|
||||
assert not icons.icon("reset_view", theme).isNull()
|
||||
|
||||
pane = Mesh3dPanel(theme="dark")
|
||||
try:
|
||||
assert pane.btn_settings.toolTip() == t("panels.mesh3d_settings_tip")
|
||||
@@ -138,17 +144,10 @@ def test_the_pane_header_offers_the_two_actions(qt_app):
|
||||
finally:
|
||||
pane.deleteLater()
|
||||
|
||||
|
||||
def test_the_reset_view_icon_is_painted_from_the_theme():
|
||||
assert "reset_view" in icons.NAMES
|
||||
for theme in ("dark", "light"):
|
||||
assert not icons.icon("reset_view", theme).isNull()
|
||||
|
||||
|
||||
# --------------------------------------------------------------- which quantity
|
||||
|
||||
|
||||
def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(qt_app, panel):
|
||||
def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(panel):
|
||||
"""The choice is the operator's; a frame that cannot honour it says so."""
|
||||
from orisys_app_shared.scene3d.contracts import FieldDescriptor
|
||||
|
||||
@@ -165,6 +164,7 @@ def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(qt_app, p
|
||||
|
||||
# The other one is drawn instead: the note names both, and the legend still shows
|
||||
# what is really on screen (the range is for the field that was drawn).
|
||||
panel.set_diagnostics_visible(True)
|
||||
panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False))
|
||||
note = panel.field_note.text()
|
||||
assert t("scene3d.field_depth") in note and t("scene3d.field_flow") in note, note
|
||||
@@ -184,12 +184,25 @@ def test_the_pane_offers_the_two_quantities_and_says_which_one_it_drew(qt_app, p
|
||||
assert panel.field() == "depth"
|
||||
|
||||
|
||||
def test_the_diagnostics_switch_governs_what_the_pane_explains(qt_app, panel):
|
||||
def test_the_diagnostics_switch_governs_what_the_pane_explains(panel):
|
||||
"""Warnings are claims about the measurement and always show; info is explanation."""
|
||||
from orisys_app_shared.scene3d.contracts import diagnostic
|
||||
from orisys_app_shared.scene3d.contracts import FieldDescriptor, RenderStats, diagnostic
|
||||
|
||||
panel.set_descriptor(None)
|
||||
panel.set_descriptor(FieldDescriptor("flow_magnitude", "px", "approximate", False))
|
||||
panel.set_render_stats(
|
||||
RenderStats(scale_lower=0.25, scale_upper=2.5, scale_mode="automatic")
|
||||
)
|
||||
panel.set_diagnostics_visible(False)
|
||||
assert not panel.scale_bar.isHidden()
|
||||
assert panel.scale_lower_label.text() == "0.25"
|
||||
assert panel.scale_upper_label.text() == "2.5"
|
||||
assert not panel.scale_lower_label.isHidden()
|
||||
assert not panel.scale_upper_label.isHidden()
|
||||
assert panel.legend_label.isHidden()
|
||||
assert panel.source_label.isHidden()
|
||||
assert panel.state_label.isHidden()
|
||||
assert panel.field_note.isHidden()
|
||||
|
||||
panel.set_diagnostic(diagnostic("field_stale", "the array is from another frame"))
|
||||
assert "another frame" in panel.legend_label.text(), (
|
||||
"a warning about the measurement was hidden"
|
||||
@@ -204,13 +217,17 @@ def test_the_diagnostics_switch_governs_what_the_pane_explains(qt_app, panel):
|
||||
assert "not current" not in panel.legend_label.text(), (
|
||||
"an informational line survived diagnostics being off"
|
||||
)
|
||||
assert panel.legend_label.isHidden()
|
||||
|
||||
panel.set_diagnostics_visible(True)
|
||||
assert not panel.legend_label.isHidden()
|
||||
assert not panel.source_label.isHidden()
|
||||
assert not panel.state_label.isHidden()
|
||||
panel.set_diagnostic(diagnostic("stale_frame", "the picture is not current"))
|
||||
assert "not current" in panel.legend_label.text()
|
||||
|
||||
|
||||
def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(qt_app, make_window):
|
||||
def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(make_window):
|
||||
"""One line under the canvas: what the picture is, not how it was made."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
@@ -243,7 +260,7 @@ def test_the_2d_footer_shows_the_picture_and_hides_the_pipeline(qt_app, make_win
|
||||
# --------------------------------------------------------------------- the menu
|
||||
|
||||
|
||||
def test_the_view_menu_opens_the_3d_settings(qt_app, make_window):
|
||||
def test_the_view_menu_opens_the_3d_settings(make_window):
|
||||
"""视图 ▸ 三维显示设置… is the way in that does not depend on the pane's header."""
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
|
||||
@@ -71,47 +71,40 @@ def _source(path: Path) -> SceneSource:
|
||||
return SceneSource(path=path, kind="package", directory=path.parent)
|
||||
|
||||
|
||||
def test_a_signed_package_opens_from_the_application(tmp_path):
|
||||
@pytest.mark.parametrize("sealed", (False, True), ids=("signed", "sealed"))
|
||||
def test_a_protected_package_opens_from_the_application(tmp_path, sealed):
|
||||
"""The review's R4: the library could read it; the application could not.
|
||||
|
||||
With the verification key resolved from the preferences, the pane's own
|
||||
``load_package`` accepts the package and the scene comes out intact.
|
||||
With keys resolved from preferences, the pane's own loader accepts both signed
|
||||
and sealed release packages and returns their scene intact.
|
||||
"""
|
||||
scene = synthetic.poc_scene()
|
||||
package, verify_path, _key_path = _write_signed(tmp_path, scene, sealed=False)
|
||||
package, verify_path, key_path = _write_signed(tmp_path, scene, sealed=sealed)
|
||||
prefs = {PREF_VERIFY_KEY: str(verify_path)}
|
||||
if sealed:
|
||||
prefs.update(
|
||||
{
|
||||
PREF_CONTENT_KEY_FILE: str(key_path),
|
||||
PREF_CONTENT_KEY_ID: KEY_ID,
|
||||
}
|
||||
)
|
||||
|
||||
keys = resolve_scene_keys({PREF_VERIFY_KEY: str(verify_path)})
|
||||
assert keys.verify_key_pem is not None
|
||||
assert keys.describe()["verify_key"] is True
|
||||
assets = load_package(_source(package), _expected(scene), keys)
|
||||
assert assets.geometry_id == scene.geometry_id
|
||||
# The manifest's own hash, recomputed when the package was written: the synthetic
|
||||
# scene's ``asset_hash`` is a placeholder until it goes through ``write_package``.
|
||||
assert len(assets.asset_hash) == 64
|
||||
|
||||
|
||||
def test_a_sealed_package_opens_with_the_content_key_from_the_preferences(tmp_path):
|
||||
scene = synthetic.poc_scene()
|
||||
package, verify_path, key_path = _write_signed(tmp_path, scene, sealed=True)
|
||||
|
||||
keys = resolve_scene_keys(
|
||||
{
|
||||
PREF_VERIFY_KEY: str(verify_path),
|
||||
PREF_CONTENT_KEY_FILE: str(key_path),
|
||||
PREF_CONTENT_KEY_ID: KEY_ID,
|
||||
}
|
||||
)
|
||||
assert keys.provider is not None
|
||||
keys = resolve_scene_keys(prefs)
|
||||
described = keys.describe()
|
||||
assert described["content_key"] is True
|
||||
assert described["content_key_id"] == KEY_ID
|
||||
# The description carries paths and ids, never key material.
|
||||
assert CONTENT_KEY_HEX not in json.dumps(described)
|
||||
assert keys.verify_key_pem is not None
|
||||
assert described["verify_key"] is True
|
||||
assert CONTENT_KEY_HEX.encode() not in keys.verify_key_pem
|
||||
if sealed:
|
||||
assert keys.provider is not None
|
||||
assert described["content_key"] is True
|
||||
assert described["content_key_id"] == KEY_ID
|
||||
# The description carries paths and ids, never key material.
|
||||
assert CONTENT_KEY_HEX not in json.dumps(described)
|
||||
|
||||
assets = load_package(_source(package), _expected(scene), keys)
|
||||
assert assets.geometry_id == scene.geometry_id
|
||||
# The writer replaces the synthetic scene's placeholder with the manifest hash.
|
||||
assert len(assets.asset_hash) == 64
|
||||
|
||||
|
||||
def test_without_the_verification_key_a_signed_package_is_refused(tmp_path):
|
||||
|
||||
@@ -20,8 +20,6 @@ mathematics (the camera, the sampler and the composer have their own files):
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import shutil
|
||||
import time
|
||||
from concurrent.futures import Future
|
||||
from dataclasses import replace
|
||||
@@ -51,33 +49,12 @@ from orisys_studio.scene3d.asset_source import (
|
||||
load_package,
|
||||
no_asset_diagnostic,
|
||||
)
|
||||
from tests.test_ui_smoke import ( # noqa: F401 (fixtures are imported by name)
|
||||
_finish,
|
||||
_running_window,
|
||||
record_root,
|
||||
replay_recording,
|
||||
from tests.conftest import (
|
||||
finish_run as _finish,
|
||||
running_window as _running_window,
|
||||
show_window as _show,
|
||||
)
|
||||
|
||||
#: Scratch root for the two-session comparison. One subdirectory per run, because
|
||||
#: the point of the test is that two recordings are identical, not that one file
|
||||
#: can be overwritten.
|
||||
RECORD_SCRATCH = Path(__file__).resolve().parents[1] / "outputs" / "_test_record_3d"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def next_record_root():
|
||||
shutil.rmtree(RECORD_SCRATCH, ignore_errors=True)
|
||||
RECORD_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
counter = itertools.count()
|
||||
|
||||
def _next() -> Path:
|
||||
path = RECORD_SCRATCH / f"run{next(counter)}"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
yield _next
|
||||
shutil.rmtree(RECORD_SCRATCH, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def write_poc(tmp_path):
|
||||
@@ -1074,8 +1051,6 @@ def test_the_replay_view_shows_the_pane_and_says_where_the_field_comes_from(
|
||||
what a recompute pass produces from the video. Those are two different claims and
|
||||
the label has to distinguish them.
|
||||
"""
|
||||
from tests.test_ui_smoke import _show
|
||||
|
||||
window = _show(make_window(sensor_type="finger"), (1440, 880))
|
||||
try:
|
||||
assert window._load_recording(Path(replay_recording))
|
||||
@@ -1138,7 +1113,6 @@ def test_every_run_gets_a_new_generation(make_window, qt_app):
|
||||
so the picture stays on the first run's last frame forever.
|
||||
"""
|
||||
from orisys_app_shared.scene3d.contracts import FrameIdentity
|
||||
from tests.test_ui_smoke import _show
|
||||
import ui_check
|
||||
|
||||
window = _show(make_window(sensor_type="finger"), (1440, 880))
|
||||
|
||||
@@ -9,20 +9,16 @@ import numpy as np
|
||||
import pytest
|
||||
|
||||
import ui_check
|
||||
from tests.conftest import visible_descendants
|
||||
from tests.conftest import (
|
||||
finish_run as _finish,
|
||||
running_window as _running_window,
|
||||
show_window as _show,
|
||||
visible_descendants,
|
||||
)
|
||||
|
||||
RESOLUTIONS = ((1280, 720), (1600, 900), (1920, 1080))
|
||||
|
||||
|
||||
def _show(window, size):
|
||||
window.resize(*size)
|
||||
window.show()
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
QApplication.processEvents()
|
||||
return window
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("theme", "sensor_type"), [
|
||||
("dark", "box"),
|
||||
("light", "box"),
|
||||
@@ -211,8 +207,9 @@ def test_toolbar_run_button_is_labelled(make_window):
|
||||
assert window.toolbar.btn_run.text() != label
|
||||
|
||||
|
||||
def test_stop_cancels_a_hung_connect(make_window):
|
||||
"""Stop must stay live and take effect instantly while connecting."""
|
||||
@pytest.mark.parametrize("cancel_via", ("toolbar", "escape"))
|
||||
def test_a_hung_connect_can_be_cancelled_immediately(make_window, cancel_via):
|
||||
"""Both cancellation paths stay responsive while a device is connecting."""
|
||||
import time
|
||||
|
||||
import ui_check
|
||||
@@ -226,35 +223,24 @@ def test_stop_cancels_a_hung_connect(make_window):
|
||||
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
assert window._session_phase == "connecting"
|
||||
assert window.toolbar.btn_run.isEnabled(), "Stop is disabled while connecting"
|
||||
assert window.toolbar.btn_run.text() == t("toolbar.cancel_label")
|
||||
assert window.act_stop.isEnabled()
|
||||
try:
|
||||
if cancel_via == "toolbar":
|
||||
assert window.toolbar.btn_run.isEnabled(), "Stop is disabled while connecting"
|
||||
assert window.toolbar.btn_run.text() == t("toolbar.cancel_label")
|
||||
assert window.act_stop.isEnabled()
|
||||
cancel = lambda: window.toolbar.btn_run.setChecked(False)
|
||||
else:
|
||||
assert window.act_stop.shortcut().toString() == "Esc"
|
||||
cancel = window._on_stop_shortcut
|
||||
|
||||
started = time.perf_counter()
|
||||
window.toolbar.btn_run.setChecked(False)
|
||||
elapsed = time.perf_counter() - started
|
||||
assert elapsed < 0.5, f"Stop blocked the GUI for {elapsed:.2f}s"
|
||||
assert not window._running
|
||||
assert stuck.thread_still_owned, "Stop abandoned the still-running worker thread"
|
||||
|
||||
|
||||
def test_escape_cancels_a_hung_connect(make_window):
|
||||
import time
|
||||
|
||||
import ui_check
|
||||
|
||||
window = _show(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
||||
stuck = ui_check._StuckWorker()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
|
||||
window.toolbar.btn_run.setChecked(True)
|
||||
assert window.act_stop.shortcut().toString() == "Esc"
|
||||
started = time.perf_counter()
|
||||
window._on_stop_shortcut()
|
||||
assert time.perf_counter() - started < 0.5
|
||||
assert not window._running
|
||||
started = time.perf_counter()
|
||||
cancel()
|
||||
elapsed = time.perf_counter() - started
|
||||
assert elapsed < 0.5, f"{cancel_via} cancellation blocked for {elapsed:.2f}s"
|
||||
assert not window._running
|
||||
assert stuck.thread_still_owned, "Stop abandoned the still-running worker thread"
|
||||
finally:
|
||||
stuck.release()
|
||||
|
||||
|
||||
def test_theme_switch_updates_panels(make_window):
|
||||
@@ -581,31 +567,6 @@ def test_force_plots_render_demo_data(make_window):
|
||||
|
||||
|
||||
# --------------------------------------------------------------- record channel
|
||||
|
||||
|
||||
def _running_window(make_window):
|
||||
"""A window in the ``running`` phase whose worker never opens a device.
|
||||
|
||||
The Qt timer is stopped deliberately: these tests drive the display path by
|
||||
calling ``_on_run_tick`` themselves, and a concurrent 16 ms timer would race
|
||||
the assertions (and outlive the window on teardown).
|
||||
"""
|
||||
window = _show(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
||||
stuck = ui_check._StuckWorker()
|
||||
window._ensure_workers = lambda: None
|
||||
window._box_worker = stuck
|
||||
window._finger_worker = stuck
|
||||
window._start_run()
|
||||
window._run_timer.stop()
|
||||
window._set_phase("running")
|
||||
return window, stuck
|
||||
|
||||
|
||||
def _finish(window, stuck):
|
||||
window._stop_run(reason="user_stop")
|
||||
stuck.release()
|
||||
|
||||
|
||||
def test_record_channel_does_not_depend_on_the_ui_timer(make_window):
|
||||
window, stuck = _running_window(make_window)
|
||||
for index in range(90):
|
||||
@@ -965,48 +926,6 @@ def test_depth_view_reports_missing_data_and_fixes_its_scale(make_window):
|
||||
|
||||
|
||||
# ------------------------------------------------------------- experiment record
|
||||
|
||||
#: Recording scratch goes under the app's gitignored ``outputs`` tree rather than
|
||||
#: the system temp directory, so the tests also run under a file sandbox that
|
||||
#: only permits writes inside the workspace.
|
||||
RECORD_SCRATCH = Path(__file__).resolve().parents[1] / "outputs" / "_test_record"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_root():
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(RECORD_SCRATCH, ignore_errors=True)
|
||||
RECORD_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
yield RECORD_SCRATCH
|
||||
shutil.rmtree(RECORD_SCRATCH, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def replay_recording(record_root):
|
||||
"""One short finished recording (video + forces + frames + session.json)."""
|
||||
from orisys_app_shared.record import ExperimentRecorder, RecordConfig
|
||||
|
||||
recorder = ExperimentRecorder(RecordConfig(video=True, fps=30.0, segment_seconds=30.0))
|
||||
recording = recorder.start(
|
||||
root=record_root,
|
||||
session_id=1,
|
||||
metadata={
|
||||
"device": {
|
||||
"sensor_type": "box",
|
||||
"config_ref": str(record_root / "sensor.json"),
|
||||
"config_sha256": "recorded",
|
||||
},
|
||||
"versions": {"sdk": "0.5.1"},
|
||||
"depth": {"stored": False, "replay": "recompute"},
|
||||
},
|
||||
)
|
||||
for index in range(60):
|
||||
recorder.on_packet(ui_check._fake_packet(index))
|
||||
recorder.stop()
|
||||
yield Path(recording)
|
||||
|
||||
|
||||
def test_recording_writes_the_documented_experiment_layout(make_window, record_root):
|
||||
import csv
|
||||
import json
|
||||
|
||||
Reference in New Issue
Block a user