修复 Studio 失败测试并新增合并门禁
把丢失的 box 录像放回 apps/studio/tests/data/,修掉第 3 节 3–10 号与两处过时测试桩, 注册 timing 标记并让根目录收集零错误;新增 tools/gate.ps1 与可选的 pre-push 钩子。 验证:studio 524 通过 19 跳过、shared 467 通过 18 跳过、ui_check 632 项 0 失败、gate.ps1 退出码 0。 细节见 docs/test-repair-plan.zh.md。
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""Plain helpers the studio UI tests share, importable without a ``tests`` package.
|
||||
|
||||
These four functions used to live in ``conftest.py`` and were imported as
|
||||
``from tests.conftest import ...``. That import only worked when pytest ran with
|
||||
``apps/studio`` in ``sys.path`` (or as the working directory): run from the
|
||||
repository root, the ``tests`` directory of *any* product could be shadowed, and
|
||||
the suite could not be collected at all. A module beside the tests, with the
|
||||
tests' own directory on ``sys.path``, has none of that ambiguity.
|
||||
|
||||
Fixtures stay in ``conftest.py`` (pytest discovers them); only the functions a
|
||||
test imports by name live here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def visible_descendants(widget):
|
||||
"""Yield every visible descendant widget of ``widget``."""
|
||||
for child in widget.findChildren(object):
|
||||
if not hasattr(child, "isVisible") or not hasattr(child, "geometry"):
|
||||
continue
|
||||
if child.isVisible():
|
||||
yield child
|
||||
|
||||
|
||||
def show_window(window, size):
|
||||
"""Show a window at ``size`` and flush pending Qt events."""
|
||||
from PySide6.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()
|
||||
@@ -51,6 +51,14 @@ _TOOLS_DIR = Path(__file__).resolve().parents[1] / "tools"
|
||||
if _TOOLS_DIR.is_dir() and str(_TOOLS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_TOOLS_DIR))
|
||||
|
||||
# The tests' own directory, so ``_studio_helpers`` is importable however pytest was
|
||||
# started. Relying on pytest to insert it works only under the default import mode and
|
||||
# only for the directory a test file happens to sit in; naming it here is what lets the
|
||||
# suite be collected from the repository root (see ``docs/test-repair-plan.zh.md`` §4).
|
||||
_TESTS_DIR = Path(__file__).resolve().parent
|
||||
if str(_TESTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_TESTS_DIR))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qt_app():
|
||||
@@ -117,49 +125,6 @@ def decline_video_prompt(monkeypatch):
|
||||
)
|
||||
|
||||
|
||||
def visible_descendants(widget):
|
||||
"""Yield every visible descendant widget of ``widget``."""
|
||||
for child in widget.findChildren(object):
|
||||
if not hasattr(child, "isVisible") or not hasattr(child, "geometry"):
|
||||
continue
|
||||
if child.isVisible():
|
||||
yield child
|
||||
|
||||
|
||||
def show_window(window, size):
|
||||
"""Show a window at ``size`` and flush pending Qt events."""
|
||||
from PySide6.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"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Test recordings
|
||||
|
||||
`box_short_20260930/` is one real recording, kept in the repository because several tests
|
||||
must drive the real SDK chain over real footage. A synthetic clip cannot replace it: the
|
||||
box chain's stitch probe looks for the lenslet grid in the image, does not converge on
|
||||
generated pixels, and a synthetic 896² clip sent the chain into minutes of work where this
|
||||
33-frame clip takes about two seconds.
|
||||
|
||||
## What it is
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Sensor | `box` (shipped bundle) |
|
||||
| Source | `dist/OrisysStudio/outputs/recordings/20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011`, recorded 2026-09-30, `session_id` 2 |
|
||||
| Length | 33 frames, frame ids **20…52**, 1.92 s of signal time |
|
||||
| Files | `raw_0001.mp4`, `frames.csv`, `forces.csv`, `events.csv`, `session.json` (3.8 MB) |
|
||||
| Not included | `preroll_0001.mp4` (3.5 MB, optional for every reader: `RecordingReader.preroll_segment()` answers `None` when the file is absent) |
|
||||
|
||||
`dist/` is build output and is not tracked, so the copy under it disappeared on
|
||||
2026-10-14 when the studio was repackaged. That took 11 tests to `ERROR` and 4 to
|
||||
`failed` without anyone noticing — see
|
||||
[`docs/test-repair-plan.zh.md`](../../../docs/test-repair-plan.zh.md) §1 and §3 #1–#2.
|
||||
|
||||
The frame ids are deliberately not 0…32: a test that used the frame *index* would pass on
|
||||
a 0-based recording, and real recordings start wherever the device's counter was.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Do not replace it with a generated recording.** The chain's behaviour on these pixels
|
||||
is part of what the tests defend.
|
||||
- **Do not write into it from a test.** The tests that analyse it copy it into `tmp_path`
|
||||
first, because a pass writes its result beside the recording.
|
||||
- **If it goes missing, restore it or the tests fail.** They no longer skip on a missing
|
||||
input: a skip is how this went unnoticed.
|
||||
@@ -0,0 +1,3 @@
|
||||
at,t_rel_s,frame_id,kind,detail
|
||||
2026-09-30T01:50:28.848+00:00,,-1,record_started,mode=video_data
|
||||
2026-09-30T01:50:30.825+00:00,,-1,record_stopped,
|
||||
|
@@ -0,0 +1,34 @@
|
||||
session_id,frame_id,t_rel_s,FN_raw,FX_raw,FY_raw,FN_N,FX_N,FY_N,valid,calibration_id
|
||||
2,20,0.0,13567.7392578125,1180.1070556640625,9125.2607421875,0.555386962890625,0.006123639892578124,0.276258865234375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,21,0.064133,11343.3232421875,2028.0238037109375,7816.4375,0.444166162109375,0.034952809326171876,0.23175887499999998,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,22,0.112021,16884.728515625,745.5536499023438,7191.43896484375,0.72123642578125,0.0,0.2105089248046875,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,23,0.17623,14818.259765625,2178.03466796875,9171.318359375,0.61791298828125,0.0400531787109375,0.27782482421875,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,24,0.24043,14969.4208984375,-1287.072509765625,11873.1181640625,0.625471044921875,-0.00976046533203125,0.36968601757812497,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,25,0.304304,14659.830078125,-141.17251586914062,2469.820556640625,0.60999150390625,-0.0,0.049973898925781246,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,26,0.352255,15476.34375,-3281.966552734375,15066.1357421875,0.6508171875,-0.07758686279296875,0.478248615234375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,27,0.416172,11016.5068359375,-3127.916748046875,13734.294921875,0.427825341796875,-0.07234916943359375,0.43296602734375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,28,0.480077,14330.064453125,273.25970458984375,5539.169921875,0.59350322265625,0.0,0.15433177734375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,29,0.543786,14844.3466796875,2567.879150390625,5841.8525390625,0.619217333984375,0.05330789111328125,0.164622986328125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,30,0.591816,16986.365234375,-1623.703125,5377.94091796875,0.72631826171875,-0.02120590625,0.1488499912109375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,31,0.656365,13733.6103515625,-1030.4215087890625,9520.9345703125,0.563680517578125,-0.0010343312988281249,0.289711775390625,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,32,0.720031,17051.982421875,-2590.688720703125,10953.798828125,0.72959912109375,-0.05408341650390625,0.33842916015625,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,33,0.784173,16303.94921875,2727.433837890625,6891.0400390625,0.6921974609375,0.05873275048828125,0.20029536132812498,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,34,0.831679,17633.34375,-208.40782165527344,5174.18798828125,0.7586671875,-0.0,0.14192239160156248,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,35,0.896141,14191.8671875,-971.9476928710938,11894.728515625,0.586593359375,-0.0,0.37042076953124997,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,36,0.960091,15828.78125,-1548.9967041015625,9286.84765625,0.6684390625000001,-0.018665887939453125,0.2817528203125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,37,1.024524,18615.650390625,-3690.62451171875,10522.7822265625,0.80778251953125,-0.0914812333984375,0.323774595703125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,38,1.071701,15312.724609375,1154.6263427734375,11247.951171875,0.6426362304687501,0.005257295654296875,0.34843033984375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,39,1.135903,12159.138671875,-82.20235443115234,10962.3251953125,0.48495693359375003,-0.0,0.33871905664062496,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,40,1.199965,15671.615234375,-540.1160888671875,17301.103515625,0.66058076171875,-0.0,0.55423751953125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,41,1.264015,18193.279296875,-280.51025390625,7286.34765625,0.78666396484375,-0.0,0.2137358203125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,42,1.311786,14463.1240234375,23.800241470336914,14572.9287109375,0.6001562011718751,0.0,0.461479576171875,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,43,1.376278,14139.1728515625,549.9257202148438,14745.4931640625,0.583958642578125,0.0,0.467346767578125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,44,1.439864,14622.09375,535.4642944335938,11325.8857421875,0.6081046875,0.0,0.351080115234375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,45,1.503909,16870.396484375,-5440.37158203125,13050.87890625,0.72051982421875,-0.15097263378906248,0.4097298828125,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,46,1.551771,17459.4765625,-322.2771911621094,9053.3154296875,0.749973828125,-0.0,0.273812724609375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,47,1.616042,21696.408203125,-1986.04833984375,11181.533203125,0.96182041015625,-0.0335256435546875,0.34617212890625,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,48,1.680262,15386.84765625,-4424.57763671875,1783.9827880859375,0.6463423828125,-0.11643563964843749,0.026655414794921874,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,49,1.744307,15400.05859375,-2761.5791015625,15228.455078125,0.6470029296875,-0.059893689453124996,0.48376747265625,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,50,1.792308,14120.4208984375,116.24051666259766,9754.70703125,0.583021044921875,0.0,0.2976600390625,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,51,1.856446,15246.4052734375,-2575.3466796875,12753.658203125,0.639320263671875,-0.053561787109375,0.39962437890624997,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
2,52,1.920255,17219.171875,-6285.30517578125,13345.544921875,0.73795859375,-0.1797003759765625,0.41974852734375,1,171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
|
||||
|
@@ -0,0 +1,34 @@
|
||||
frame_id,t_rel_s,acquired_at,segment,segment_frame,in_video,dropped
|
||||
20,0.0,2026-09-30T01:50:28.870+00:00,1,1,1,0
|
||||
21,0.064133,2026-09-30T01:50:28.935+00:00,1,2,1,0
|
||||
22,0.112021,2026-09-30T01:50:28.983+00:00,1,3,1,0
|
||||
23,0.17623,2026-09-30T01:50:29.048+00:00,1,4,1,0
|
||||
24,0.24043,2026-09-30T01:50:29.111+00:00,1,5,1,0
|
||||
25,0.304304,2026-09-30T01:50:29.175+00:00,1,6,1,0
|
||||
26,0.352255,2026-09-30T01:50:29.231+00:00,1,7,1,0
|
||||
27,0.416172,2026-09-30T01:50:29.287+00:00,1,8,1,0
|
||||
28,0.480077,2026-09-30T01:50:29.351+00:00,1,9,1,0
|
||||
29,0.543786,2026-09-30T01:50:29.414+00:00,1,10,1,0
|
||||
30,0.591816,2026-09-30T01:50:29.462+00:00,1,11,1,0
|
||||
31,0.656365,2026-09-30T01:50:29.526+00:00,1,12,1,0
|
||||
32,0.720031,2026-09-30T01:50:29.590+00:00,1,13,1,0
|
||||
33,0.784173,2026-09-30T01:50:29.654+00:00,1,14,1,0
|
||||
34,0.831679,2026-09-30T01:50:29.702+00:00,1,15,1,0
|
||||
35,0.896141,2026-09-30T01:50:29.766+00:00,1,16,1,0
|
||||
36,0.960091,2026-09-30T01:50:29.830+00:00,1,17,1,0
|
||||
37,1.024524,2026-09-30T01:50:29.895+00:00,1,18,1,0
|
||||
38,1.071701,2026-09-30T01:50:29.942+00:00,1,19,1,0
|
||||
39,1.135903,2026-09-30T01:50:30.006+00:00,1,20,1,0
|
||||
40,1.199965,2026-09-30T01:50:30.070+00:00,1,21,1,0
|
||||
41,1.264015,2026-09-30T01:50:30.134+00:00,1,22,1,0
|
||||
42,1.311786,2026-09-30T01:50:30.182+00:00,1,23,1,0
|
||||
43,1.376278,2026-09-30T01:50:30.247+00:00,1,24,1,0
|
||||
44,1.439864,2026-09-30T01:50:30.310+00:00,1,25,1,0
|
||||
45,1.503909,2026-09-30T01:50:30.373+00:00,1,26,1,0
|
||||
46,1.551771,2026-09-30T01:50:30.422+00:00,1,27,1,0
|
||||
47,1.616042,2026-09-30T01:50:30.486+00:00,1,28,1,0
|
||||
48,1.680262,2026-09-30T01:50:30.552+00:00,1,29,1,0
|
||||
49,1.744307,2026-09-30T01:50:30.615+00:00,1,30,1,0
|
||||
50,1.792308,2026-09-30T01:50:30.664+00:00,1,31,1,0
|
||||
51,1.856446,2026-09-30T01:50:30.728+00:00,1,32,1,0
|
||||
52,1.920255,2026-09-30T01:50:30.792+00:00,1,33,1,0
|
||||
|
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"format_version": "1.1",
|
||||
"session_id": 2,
|
||||
"calibration_id": "171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348",
|
||||
"started_at": "2026-09-30T01:50:28+00:00",
|
||||
"ended_at": "2026-09-30T01:50:30+00:00",
|
||||
"video": {
|
||||
"video": true,
|
||||
"lossless_frames": false,
|
||||
"fps": 30.0,
|
||||
"codec": "mp4v",
|
||||
"segment_seconds": 300.0,
|
||||
"max_queue": 2048,
|
||||
"max_queue_bytes": 536870912,
|
||||
"preroll_frames": 30,
|
||||
"preroll_bytes": 201326592,
|
||||
"lossy_note": "MP4 is a lossy container: this is the pre-processing camera image, not a lossless record of the raw sensor data."
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"at": "2026-09-30T01:50:28.848+00:00",
|
||||
"mono_t": null,
|
||||
"frame_id": -1,
|
||||
"kind": "record_started",
|
||||
"detail": "mode=video_data"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-30T01:50:30.825+00:00",
|
||||
"mono_t": null,
|
||||
"frame_id": -1,
|
||||
"kind": "record_stopped",
|
||||
"detail": ""
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"frames": 33,
|
||||
"video_frames": 33,
|
||||
"segments": 1,
|
||||
"dropped_data": 0,
|
||||
"dropped_video": 0,
|
||||
"dropped_gaps": 0,
|
||||
"unrecorded_gaps": 0,
|
||||
"resampled_video": 0,
|
||||
"write_errors": 0,
|
||||
"failed": false,
|
||||
"last_error": "",
|
||||
"duration_s": 1.920255,
|
||||
"bytes_written": 7487099,
|
||||
"rejected_after_failure": 0
|
||||
},
|
||||
"calibration_snapshots": [],
|
||||
"preroll_frames": 30,
|
||||
"preroll_segment": "preroll_0001.mp4",
|
||||
"files": {
|
||||
"forces": "forces.csv",
|
||||
"frames": "frames.csv",
|
||||
"events": "events.csv",
|
||||
"video": "raw_%04d.mp4",
|
||||
"lossless_frames": null
|
||||
},
|
||||
"device": {
|
||||
"sensor_type": "box",
|
||||
"config_ref": "D:\\orisys_ws\\orisys-app\\dist\\OrisysStudio\\config\\sensors\\box\\sensor.json",
|
||||
"config_sha256": "27343dbbfc768c4d43ea48917ae2f9fbc9f91ba285b049b4c58a04673e3bec1e",
|
||||
"frame": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.0,
|
||||
"measured_fps": 16.6642954760577
|
||||
}
|
||||
},
|
||||
"versions": {
|
||||
"app": "0.1.0",
|
||||
"shared": "0.1.0",
|
||||
"shared_api": "0.1",
|
||||
"sdk": "0.5.3"
|
||||
},
|
||||
"capture": {
|
||||
"requested": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.0,
|
||||
"fourcc": "MJPG"
|
||||
},
|
||||
"negotiated": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.00003000003,
|
||||
"fourcc": "MJPG",
|
||||
"backend": "DSHOW"
|
||||
},
|
||||
"dropped_frames": 18,
|
||||
"measured_fps": 16.6642954760577
|
||||
},
|
||||
"calibration": {
|
||||
"config_ref": "D:\\orisys_ws\\orisys-app\\dist\\OrisysStudio\\config\\sensors\\box\\sensor.json",
|
||||
"force_calibrated": true,
|
||||
"version": "171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348",
|
||||
"sn": "",
|
||||
"device_sn": ""
|
||||
},
|
||||
"depth": {
|
||||
"stored": false,
|
||||
"replay": "none",
|
||||
"quantity": "relative deformation / algorithm response, not a calibrated length",
|
||||
"method": "re-run the SDK over raw_*.mp4 with this config_ref",
|
||||
"config_sha256": "27343dbbfc768c4d43ea48917ae2f9fbc9f91ba285b049b4c58a04673e3bec1e",
|
||||
"sdk_version": "0.5.3"
|
||||
},
|
||||
"conventions": {
|
||||
"axis_order": [
|
||||
"FN",
|
||||
"FX",
|
||||
"FY"
|
||||
],
|
||||
"force_unit": "N when calibrated, otherwise raw counts",
|
||||
"time_base": "t_rel_s is relative to the first recorded frame; an event that predates it has no relative base and is left empty",
|
||||
"frame_id": "monotonic per acquisition session"
|
||||
},
|
||||
"filter": {
|
||||
"measurement": {
|
||||
"flow_ema_alpha": 1.0,
|
||||
"flow_ema_applied": false,
|
||||
"hhd_downsample": 0,
|
||||
"dis_mode": 3
|
||||
},
|
||||
"readout": {
|
||||
"force_median_window": 1,
|
||||
"force_median_applied": false,
|
||||
"force_lowpass_cutoff_hz": 0.0,
|
||||
"force_lowpass_order": 2,
|
||||
"force_lowpass_sample_rate_hz": 30.0,
|
||||
"force_lowpass_rate_auto": true,
|
||||
"force_lowpass_applied": false,
|
||||
"sample_rate_source": "config"
|
||||
},
|
||||
"describe": "none",
|
||||
"source": "sensor.filters_state",
|
||||
"show_unfiltered": false
|
||||
},
|
||||
"recording_mode": "video_data",
|
||||
"video_config": {
|
||||
"video": true,
|
||||
"lossless_frames": false,
|
||||
"fps": 30.0,
|
||||
"codec": "mp4v",
|
||||
"segment_seconds": 300.0,
|
||||
"max_queue": 2048,
|
||||
"max_queue_bytes": 536870912,
|
||||
"preroll_frames": 30,
|
||||
"preroll_bytes": 201326592,
|
||||
"lossy_note": "MP4 is a lossy container: this is the pre-processing camera image, not a lossless record of the raw sensor data."
|
||||
},
|
||||
"visualization3d": {
|
||||
"block_version": 1,
|
||||
"enabled": false,
|
||||
"available": false,
|
||||
"reason": "此传感器类型没有三维场景",
|
||||
"scene": {
|
||||
"path": "",
|
||||
"keys": "{'verify_key': False, 'content_key': False, 'require_signature': False}"
|
||||
},
|
||||
"gl": {},
|
||||
"display": {},
|
||||
"stream": {}
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,10 @@ Where a failure would be invisible:
|
||||
* a box result is recorded on the box sample grid, so the canvas is compared with
|
||||
``draw_arrow_display`` on the recovered field.
|
||||
|
||||
The fixture clip is the project's recorded box footage under ``dist/``, copied into the test's
|
||||
own directory. If it is absent the test **fails** (``_require_clip``) rather than skipping: a
|
||||
silently skipped end-to-end test is how a broken workflow ships.
|
||||
The fixture clip is the project's recorded box footage, tracked in the repository beside
|
||||
this test (``tests/data/box_short_20260930``, see its README). If it is absent the test
|
||||
**fails** (``_require_clip``) rather than skipping: a silently skipped end-to-end test is
|
||||
how a broken workflow ships.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -65,17 +66,12 @@ from PySide6.QtWidgets import QApplication, QDialog, QDialogButtonBox
|
||||
|
||||
from orisys_studio.ui.video_analysis_dialog import VideoAnalysisDialog
|
||||
|
||||
APP = Path(__file__).resolve().parents[1]
|
||||
REPO = APP.parents[1]
|
||||
EVIDENCE = (
|
||||
REPO
|
||||
/ "dist"
|
||||
/ "OrisysStudio"
|
||||
/ "outputs"
|
||||
/ "recordings"
|
||||
/ "20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011"
|
||||
)
|
||||
SOURCE_CLIP = EVIDENCE / "raw_0001.mp4"
|
||||
#: The one real input, tracked in the repository beside this test: a 33-frame box
|
||||
#: recording (2026-09-30, session 2, 1.9 s) copied out of ``dist/`` before that build
|
||||
#: output was next repackaged. See ``data/README.md`` for what it is and why it is here
|
||||
#: rather than generated.
|
||||
DATA = Path(__file__).resolve().parent / "data" / "box_short_20260930"
|
||||
SOURCE_CLIP = DATA / "raw_0001.mp4"
|
||||
|
||||
|
||||
def _require_clip() -> Path:
|
||||
@@ -90,7 +86,8 @@ def _require_clip() -> Path:
|
||||
"""
|
||||
assert SOURCE_CLIP.is_file(), (
|
||||
"the bare-video test needs recorded box footage at "
|
||||
f"{SOURCE_CLIP}; copy it there or run the build that produces dist/"
|
||||
f"{SOURCE_CLIP}; it is tracked in the repository (apps/studio/tests/data/README.md), "
|
||||
"so restore it rather than skipping the test"
|
||||
)
|
||||
pytest.importorskip("orisys", reason="the analysis pass drives the SDK chain")
|
||||
return SOURCE_CLIP
|
||||
@@ -396,16 +393,25 @@ def test_a_bare_video_is_analysed_and_scrubbed_in_both_directions(
|
||||
"the arrow view is the video's raw picture"
|
||||
)
|
||||
|
||||
# ---- overlay: arrows on the captured frame, not on the black placeholder -
|
||||
# ---- overlay: a result whose captured base cannot be rebuilt is refused -----
|
||||
# The overlay views paint on the live-shaped picture (UV unroll / box stitch). A
|
||||
# stored result does not contain it — replay rebuilds it from the result's
|
||||
# provenance — and for a box result that rebuild fails on this footage (measured:
|
||||
# every frame; the SDK stitch raises inside `Sensor.get_img`). Since 425fa0d the
|
||||
# panel **refuses** instead of painting arrows on a resized fisheye, and the refusal
|
||||
# with its reason is the honest thing to assert here.
|
||||
#
|
||||
# It used to assert painted arrows over the captured frame. That claim was only ever
|
||||
# green because the footage it needed had been deleted from `dist/`; the box overlay
|
||||
# base is itself broken (tracked separately) — this test states what the operator
|
||||
# actually gets, not what an earlier build happened to draw.
|
||||
set_mode(window, ARROW_STITCHED)
|
||||
QApplication.processEvents()
|
||||
overlay = panel_bgr(window)
|
||||
assert painted(overlay, canvas) > 0, "the overlay painted no arrows"
|
||||
assert float(np.mean(overlay)) > 8.0, (
|
||||
"the overlay is still a near-black canvas; the captured frame was not under the arrows"
|
||||
assert window.arrow_panel._source_image is None, (
|
||||
"the overlay drew something without the captured frame under it"
|
||||
)
|
||||
assert not np.array_equal(overlay, middle_shot), (
|
||||
"the overlay view is the pure-colour canvas"
|
||||
assert window.arrow_panel.image_label.text() == t("panels.arrow_capture_unavailable"), (
|
||||
window.arrow_panel.image_label.text()
|
||||
)
|
||||
|
||||
# ---- the depth view, from the same stored frame -------------------------
|
||||
|
||||
@@ -14,7 +14,7 @@ button, is what tells stopped from paused.
|
||||
from __future__ import annotations
|
||||
|
||||
from orisys_app_shared.i18n import t
|
||||
from tests.conftest import finish_run, running_window, show_window
|
||||
from _studio_helpers import finish_run, running_window, show_window
|
||||
|
||||
|
||||
def test_the_transport_is_idle_with_no_session(make_window):
|
||||
|
||||
@@ -13,12 +13,13 @@ What is being defended, in the order the plan asked for it:
|
||||
5. **frames are addressed by id.** An id the recording does not hold is refused, never
|
||||
coerced into an index.
|
||||
|
||||
The fixtures use real footage — the 33-frame box recording under ``dist/`` and the
|
||||
shipped finger configuration — driven through the **finger** chain, which is what gives
|
||||
the result a validity mask and therefore a static candidate grid. The pass is O(5 s)
|
||||
and is shared per module. Where the SDK or the footage is absent the tests skip rather
|
||||
than fake a result: a synthetic result would not defend the id span (20…52), the
|
||||
keyframe identity or the sample round trip.
|
||||
The fixtures use real footage — the 33-frame box recording tracked beside this test
|
||||
(``data/box_short_20260930``) and the shipped finger configuration — driven through the
|
||||
**finger** chain, which is what gives the result a validity mask and therefore a static
|
||||
candidate grid. The pass is O(5 s) and is shared per module. Where the SDK is absent the
|
||||
tests skip rather than fake a result: a synthetic result would not defend the id span
|
||||
(20…52), the keyframe identity or the sample round trip. Where the *footage* is absent they
|
||||
fail, naming the file: a missing input is not an environment that should go green.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -73,16 +74,13 @@ from orisys_studio.replay_analysis import (
|
||||
ResultPolicyError,
|
||||
)
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
STUDIO = REPO / "dist" / "OrisysStudio"
|
||||
RECORDING = (
|
||||
STUDIO
|
||||
/ "outputs"
|
||||
/ "recordings"
|
||||
/ "20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011"
|
||||
)
|
||||
FINGER_CONFIG = STUDIO / "config" / "sensors" / "finger" / "sensor.json"
|
||||
FINGER_EXPORT = STUDIO / "config" / "sensors" / "finger" / "assets" / "30fps-0703"
|
||||
#: The tracked test input (``apps/studio/tests/data/README.md`` says where it came from).
|
||||
RECORDING = Path(__file__).resolve().parent / "data" / "box_short_20260930"
|
||||
#: The repository's own sensor configuration. These used to be read from the packaged copy
|
||||
#: under ``dist/``, which a build can delete — the same reason the footage moved here.
|
||||
CONFIG = Path(__file__).resolve().parents[1] / "config"
|
||||
FINGER_CONFIG = CONFIG / "sensors" / "finger" / "sensor.json"
|
||||
FINGER_EXPORT = CONFIG / "sensors" / "finger" / "assets" / "30fps-0703"
|
||||
|
||||
#: The evidence recording's own frame ids. Deliberately not 0…32: an implementation
|
||||
#: that used an index would pass every test written against a 0-based recording.
|
||||
@@ -91,13 +89,22 @@ LAST_FRAME_ID = 52
|
||||
|
||||
|
||||
def _require_inputs() -> None:
|
||||
"""The footage is committed, so its absence is a failure rather than a skip."""
|
||||
missing = [
|
||||
str(path)
|
||||
for path in (RECORDING / "frames.csv", RECORDING / "raw_0001.mp4", FINGER_CONFIG)
|
||||
for path in (
|
||||
RECORDING / "frames.csv",
|
||||
RECORDING / "raw_0001.mp4",
|
||||
FINGER_CONFIG,
|
||||
)
|
||||
if not path.exists()
|
||||
]
|
||||
if missing:
|
||||
pytest.skip("analysis inputs are not present: " + ", ".join(missing))
|
||||
assert not missing, (
|
||||
"analysis inputs are missing: "
|
||||
+ ", ".join(missing)
|
||||
+ ". The recording is committed (apps/studio/tests/data/README.md); restore it "
|
||||
"rather than skipping."
|
||||
)
|
||||
pytest.importorskip("orisys", reason="the SDK is needed to run the analysis pass")
|
||||
|
||||
|
||||
@@ -479,6 +486,11 @@ class _Host:
|
||||
if paint_2d:
|
||||
from orisys_app_shared.acquisition.image_backend import ImageFrame
|
||||
|
||||
# The fields the window's own sink puts on the frame it draws
|
||||
# (``app_window.py:2835-2851``). ``depth_valid``/``flow_valid`` are what
|
||||
# ``plan_image`` reads to decide whether the frame can be drawn as that
|
||||
# view, so a stub that leaves them out cannot answer "the depth view is
|
||||
# real" — which is exactly what the depth test below asks.
|
||||
self.frames.append(
|
||||
ImageFrame(
|
||||
getattr(packet, "processed_frame", None),
|
||||
@@ -487,6 +499,10 @@ class _Host:
|
||||
valid_mask=getattr(packet, "valid_mask", None),
|
||||
source=image_source or "replay",
|
||||
frame_id=int(getattr(packet, "frame_id", 0) or 0),
|
||||
flow_valid=getattr(packet, "flow_valid", None),
|
||||
flow_frame_id=getattr(packet, "flow_frame_id", None),
|
||||
depth_valid=getattr(packet, "depth_valid", None),
|
||||
depth_frame_id=getattr(packet, "depth_frame_id", None),
|
||||
arrow_policy=arrow_policy,
|
||||
result_available=True,
|
||||
)
|
||||
@@ -1652,6 +1668,8 @@ def test_a_stitch_grid_is_not_painted_onto_the_camera_frame():
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from orisys_app_shared.analysis.model import Provenance
|
||||
|
||||
class _Reader:
|
||||
complete = True
|
||||
key = "stitch"
|
||||
@@ -1659,6 +1677,14 @@ def test_a_stitch_grid_is_not_painted_onto_the_camera_frame():
|
||||
frame_ids = np.array([1], dtype=np.int64)
|
||||
times = np.array([0.0])
|
||||
keyframes: dict = {}
|
||||
#: A real reader always carries one (``AnalysisReader.__init__`` builds it from
|
||||
#: the result's metadata) and ``AnalysisSession`` reads it: *where* the samples
|
||||
#: are travels from ``provenance.policy``, and the overlay base opens from the
|
||||
#: rest. ``829d2c1`` added those reads; this stub predated them and the default
|
||||
#: ``Provenance()`` is the honest stand-in — an empty policy, so the session
|
||||
#: falls back to the display policy exactly as it does for a result that stored
|
||||
#: no grid.
|
||||
provenance = Provenance()
|
||||
|
||||
def mask(self):
|
||||
return None
|
||||
|
||||
@@ -18,7 +18,7 @@ chain — and assert on the picture the panel actually shows, not on a state a h
|
||||
built.
|
||||
|
||||
The recording is copied before use: a pass writes its result into the recording's own
|
||||
directory, and nothing may write into ``dist/``.
|
||||
directory, and the tracked copy the tests read must never be written to.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -34,11 +34,11 @@ from orisys_app_shared.acquisition.image_backend import (
|
||||
ARROW_PURE,
|
||||
ARROW_STITCHED,
|
||||
DEPTH,
|
||||
DEPTH_ARROWS,
|
||||
DERIVED_MODES,
|
||||
RECORDED,
|
||||
plan_image,
|
||||
)
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_app_shared.scene3d.contracts import DisplaySettings
|
||||
from orisys_app_shared.scene3d.visual_policy import (
|
||||
select_flow_samples,
|
||||
@@ -46,14 +46,8 @@ from orisys_app_shared.scene3d.visual_policy import (
|
||||
from orisys_studio.replay import MODE_RECORDING
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
STUDIO = REPO / "dist" / "OrisysStudio"
|
||||
SOURCE_RECORDING = (
|
||||
STUDIO
|
||||
/ "outputs"
|
||||
/ "recordings"
|
||||
/ "20260930T015028Z_session0002_40cb8e7441434feab48bb646afdba011"
|
||||
)
|
||||
#: The tracking-visible test input (``data/README.md`` explains where it came from).
|
||||
DATA = Path(__file__).resolve().parent / "data" / "box_short_20260930"
|
||||
#: What makes a directory a recording for the application, and all a replay reads.
|
||||
RECORDING_FILES = (
|
||||
"frames.csv",
|
||||
@@ -61,29 +55,41 @@ RECORDING_FILES = (
|
||||
"events.csv",
|
||||
"session.json",
|
||||
"raw_0001.mp4",
|
||||
"preroll_0001.mp4",
|
||||
)
|
||||
#: Written by a recording and read by nothing a replay needs: ``preroll_segment()``
|
||||
#: answers ``None`` when the file is absent, so it is copied when it is there and never
|
||||
#: required. It stays out of the repository because it is a third of the data's size.
|
||||
OPTIONAL_FILES = ("preroll_0001.mp4",)
|
||||
|
||||
|
||||
def _require_recording() -> None:
|
||||
"""The recording must be here, and its absence is a failure, not a skip.
|
||||
|
||||
The old version skipped: the tests went green on a machine where the footage had
|
||||
been repackaged away, which is exactly how eleven failing tests went unnoticed.
|
||||
"""
|
||||
missing = [
|
||||
str(DATA / name) for name in RECORDING_FILES if not (DATA / name).is_file()
|
||||
]
|
||||
assert not missing, (
|
||||
"the replay wiring tests need the tracked box test recording at "
|
||||
f"{DATA}; missing: {', '.join(missing)}. It is committed "
|
||||
"(apps/studio/tests/data/README.md); restore it rather than skipping."
|
||||
)
|
||||
|
||||
|
||||
def _require_inputs() -> None:
|
||||
missing = [
|
||||
str(SOURCE_RECORDING / name)
|
||||
for name in ("frames.csv", "raw_0001.mp4", "session.json")
|
||||
if not (SOURCE_RECORDING / name).exists()
|
||||
]
|
||||
if missing:
|
||||
pytest.skip(
|
||||
"the replay wiring tests need the evidence recording: " + ", ".join(missing)
|
||||
)
|
||||
_require_recording()
|
||||
pytest.importorskip("orisys", reason="the analysis pass drives the SDK chain")
|
||||
|
||||
|
||||
def _copy_recording(destination: Path) -> Path:
|
||||
"""The evidence recording, copied: nothing may write into ``dist/``."""
|
||||
"""The test recording, copied: nothing may write into the tracked data."""
|
||||
_require_recording()
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
for name in RECORDING_FILES:
|
||||
source = SOURCE_RECORDING / name
|
||||
if source.exists():
|
||||
for name in RECORDING_FILES + OPTIONAL_FILES:
|
||||
source = DATA / name
|
||||
if source.is_file():
|
||||
shutil.copy2(source, destination / name)
|
||||
return destination
|
||||
|
||||
@@ -303,11 +309,16 @@ def test_the_operator_sees_the_cached_views_over_the_recording(
|
||||
"a pass ran over a recording that already had a result"
|
||||
)
|
||||
|
||||
# ``depth_arrows`` joined the derived views in ``091d0fd`` ("二维增加深度叠箭头"), which
|
||||
# updated ``ui_check`` and the live-view tests but not this one: it was skipping on the
|
||||
# missing footage at the time, so nothing caught it. The claim is unchanged — the
|
||||
# recording's own picture stays one step away from *every* derived view.
|
||||
assert window.arrow_panel.view_modes() == (
|
||||
RECORDED,
|
||||
ARROW_PURE,
|
||||
ARROW_STITCHED,
|
||||
DEPTH,
|
||||
DEPTH_ARROWS,
|
||||
), "the recording's own picture must stay one step away from the derived views"
|
||||
assert window.arrow_panel.view_mode() in DERIVED_MODES, (
|
||||
"the operator was left on the camera picture although a result is served"
|
||||
@@ -343,14 +354,22 @@ def test_the_operator_sees_the_cached_views_over_the_recording(
|
||||
"the arrow view is the recording's raw picture"
|
||||
)
|
||||
|
||||
# The depth view is the stored map, drawn through the window's own backend. This
|
||||
# result's map is exactly zero on every frame it holds — the box chain's depth is
|
||||
# flat on this footage — and it is still a stored array of the result's own shape,
|
||||
# so the pixels are checked against it rather than against a flat placeholder.
|
||||
# The depth view is the stored map, drawn through the window's own backend. The
|
||||
# map is this result's own array of the result's own shape, so the pixels are checked
|
||||
# against it rather than against a placeholder.
|
||||
#
|
||||
# It is **not** all-zero, which is what this asserted when the recording was first
|
||||
# played (a "flat depth" note): measured on the restored footage with the shipped box
|
||||
# config, the stored map has 8926 non-zero cells of 365², i.e. the chain does return
|
||||
# a depth field here. Flatness was a property of an earlier run's result, not a
|
||||
# product promise; the claim this block exists for — the depth view's pixels are
|
||||
# ``depth_map_to_bgr`` of the stored frame — is asserted below and is unchanged.
|
||||
# The colour-scale caption names the range only.
|
||||
_set_mode(window, DEPTH)
|
||||
target = active_frame
|
||||
assert target.depth is not None and target.depth_nonzero == 0
|
||||
assert target.depth is not None and target.depth_nonzero > 0, (
|
||||
f"the stored depth map holds no value at all: {target.depth_nonzero} non-zero cells"
|
||||
)
|
||||
QApplication.processEvents()
|
||||
drawn = captured[-1]
|
||||
assert drawn.frame_id == target.frame_id
|
||||
|
||||
@@ -49,13 +49,13 @@ from orisys_studio.scene3d.asset_source import (
|
||||
)
|
||||
from PySide6.QtCore import Signal
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
finish_run as _finish,
|
||||
)
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
running_window as _running_window,
|
||||
)
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
show_window as _show,
|
||||
)
|
||||
|
||||
@@ -1131,10 +1131,14 @@ def test_the_replay_view_shows_the_pane_and_says_where_the_field_comes_from(
|
||||
assert "回放" in label or "Replay" in label, label
|
||||
|
||||
# A running pass means the pixels are recomputed, and the label must say so.
|
||||
# Compared through the locale key rather than a word from it: ``c0a10e6`` reworded
|
||||
# this line ("重算" → "分析结果:非录制时的测量值"), and the claim being defended is
|
||||
# "*which* source the pane says it is showing", not the phrasing.
|
||||
window._recompute_running = True
|
||||
window._apply_panel_visibility()
|
||||
label = view.panel.source_label.text()
|
||||
assert "重算" in label or "Recomputed" in label, label
|
||||
assert view.panel.source_label.text() == t("scene3d.source_recompute"), (
|
||||
view.panel.source_label.text()
|
||||
)
|
||||
|
||||
# Stopping the pass must clear the picture, not leave the last frame up.
|
||||
window._recompute_running = False
|
||||
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import ui_check
|
||||
from orisys_app_shared.i18n import t
|
||||
from tests.conftest import finish_run, running_window, show_window
|
||||
from _studio_helpers import finish_run, running_window, show_window
|
||||
|
||||
|
||||
def test_the_group_is_exactly_play_pause_stop_reset(make_window):
|
||||
|
||||
@@ -30,7 +30,7 @@ import ui_check
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui import elements
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from tests.conftest import finish_run, show_window
|
||||
from _studio_helpers import finish_run, show_window
|
||||
|
||||
VIEWS = ("live", "replay", "calibration")
|
||||
SIZE = (1440, 880)
|
||||
|
||||
@@ -7,16 +7,16 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
import pytest
|
||||
import ui_check
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
finish_run as _finish,
|
||||
)
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
running_window as _running_window,
|
||||
)
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
show_window as _show,
|
||||
)
|
||||
from tests.conftest import (
|
||||
from _studio_helpers import (
|
||||
visible_descendants,
|
||||
)
|
||||
|
||||
@@ -193,13 +193,27 @@ def test_status_chip_tracks_phase(make_window):
|
||||
|
||||
|
||||
def test_run_tooltip_explains_disabled_reason(make_window):
|
||||
"""A video source never starts a live run, and the rail says why.
|
||||
|
||||
The button used to become enabled when a video file was chosen here; the live view
|
||||
is a camera only (``test_live_run_refuses_a_video_source`` pins the same rule from
|
||||
the other side), so choosing a file leaves Run unavailable and the reason names the
|
||||
camera instead of a missing file.
|
||||
"""
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
window = _show(make_window(source_mode="video", video_path=""), (1400, 880))
|
||||
assert not window.toolbar.btn_run.isEnabled()
|
||||
assert window.toolbar.btn_run.toolTip()
|
||||
|
||||
window.toolbar.set_video_path("C:/tmp/demo.mp4")
|
||||
window._update_run_enabled()
|
||||
assert window.toolbar.btn_run.isEnabled()
|
||||
reason = t("toolbar.run_video_in_replay")
|
||||
assert not window.toolbar.btn_run.isEnabled(), (
|
||||
"a video source still starts a live run"
|
||||
)
|
||||
assert window.toolbar.btn_run.toolTip() == reason, window.toolbar.btn_run.toolTip()
|
||||
assert window.toolbar.run_hint.text() == reason, window.toolbar.run_hint.text()
|
||||
|
||||
|
||||
def test_toolbar_run_button_is_labelled(make_window):
|
||||
@@ -1501,7 +1515,18 @@ def test_the_sensor_summary_opens_the_dialog(make_window):
|
||||
assert dialog.calib_title.text() == t("sensor.calib_section")
|
||||
assert dialog.version_label.text() and dialog.axes_label.text()
|
||||
assert dialog.export_label.text() == t("sensor.export_none")
|
||||
assert not dialog.btn_apply.isEnabled(), "applying with no export"
|
||||
# 应用 is the *live calibration* action, not "apply the last export" (``3d2fb0c``
|
||||
# moved it here): it is offered with no export because there is always a choice to
|
||||
# apply — the profile's declared file, a browsed one, or raw force — and with no
|
||||
# session running it only remembers that choice (``sensor_dialog.py:324`` enables it
|
||||
# unconditionally; ``apply_live_calibration_choice`` reports a problem instead of
|
||||
# raising, so an offered button cannot fail).
|
||||
assert dialog.btn_apply.isEnabled(), "the live calibration cannot be applied"
|
||||
assert dialog.btn_apply.text() == t("cal.apply")
|
||||
assert dialog.btn_apply.toolTip() == t("sensor.live_calibration_apply_tip")
|
||||
assert dialog.calibration_combo.currentData() in ("default", "none"), (
|
||||
dialog.calibration_combo.currentData()
|
||||
)
|
||||
|
||||
# Changing the product in the dialog reaches the window and the rail.
|
||||
picker.type_combo.setCurrentIndex(picker.type_combo.findData("finger"))
|
||||
@@ -1659,11 +1684,17 @@ def test_the_2d_area_keeps_the_output_aspect_ratio(make_window):
|
||||
|
||||
|
||||
def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
"""One layout for both views; the only difference is the source slot.
|
||||
"""The same panes and the same transport as the live view; two stated differences.
|
||||
|
||||
The replay used to carry its own bar (a progress slider plus a recording
|
||||
picker). It is gone: the chart's cursor is the playhead, its header carries
|
||||
the transport, and the rail shows the video instead of the camera.
|
||||
|
||||
What differs is deliberate (``321e52f``: "keep each preset's panel priority"):
|
||||
the replay gives the **charts** the room, so the 2D panel is narrower than in the
|
||||
live view and the charts are wider. This used to assert the splitter widths were
|
||||
*equal*, which was true before that change and is the opposite of what the
|
||||
layout now promises.
|
||||
"""
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
@@ -1673,13 +1704,23 @@ def test_the_replay_view_is_the_live_view_with_a_video_source(make_window):
|
||||
|
||||
QApplication.processEvents()
|
||||
live_sizes = tuple(window.content_splitter.sizes())
|
||||
# Slot 1 is the 2D panel and slot 2 the charts (0 and 3 are the mesh and calibration
|
||||
# panels, both hidden here, hence their zero width).
|
||||
assert live_sizes[1] > 0 and live_sizes[2] > 0, live_sizes
|
||||
|
||||
window.brand_bar.btn_playback.click()
|
||||
QApplication.processEvents()
|
||||
assert not hasattr(window, "replay_bar"), "the replay still has a separate bar"
|
||||
assert tuple(window.content_splitter.sizes()) == live_sizes, (
|
||||
"the replay view is not laid out like the live view"
|
||||
replay_sizes = tuple(window.content_splitter.sizes())
|
||||
assert replay_sizes[1] < live_sizes[1], (
|
||||
f"the replay 2D panel is not narrower than the live one: {replay_sizes} vs {live_sizes}"
|
||||
)
|
||||
assert replay_sizes[2] > live_sizes[2], (
|
||||
f"the replay charts did not take that room: {replay_sizes} vs {live_sizes}"
|
||||
)
|
||||
# The priority behind those numbers is ``view_layout.py``'s per-preset stretch
|
||||
# factors (arrow 4, charts 5 in replay). The splitter exposes no getter for them in
|
||||
# PySide6, so the widths above are what this test can read.
|
||||
assert window.toolbar.btn_replay_video.isVisible(), "no video picker while replaying"
|
||||
assert window.toolbar.btn_replay_video.text() == t("toolbar.choose_video")
|
||||
assert window.toolbar.btn_replay_video.toolTip() == t("toolbar.choose_video_tip")
|
||||
@@ -2949,6 +2990,14 @@ def test_the_rail_puts_result_identity_before_a_long_video_name(
|
||||
|
||||
|
||||
def test_old_result_depth_is_not_sent_to_the_new_sensor_scene(make_window, monkeypatch):
|
||||
"""A result from the previous sensor must not reach the new sensor's 3D scene.
|
||||
|
||||
Driven through the window's public packet entry (``show_analysis_packet``, which
|
||||
``cf49b08``/``28e955a`` made the replay path; the private ``_show_analysis_frame``
|
||||
it replaced no longer exists). The claim is unchanged: a Finger window refuses a
|
||||
packet whose result says it came from a Box, and accepts the same packet once the
|
||||
result names the sensor on screen.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
window = make_window(sensor_type="finger")
|
||||
@@ -2968,10 +3017,10 @@ def test_old_result_depth_is_not_sent_to_the_new_sensor_scene(make_window, monke
|
||||
monkeypatch.setattr(window, "_connect_analysis_display", lambda _panel: True)
|
||||
monkeypatch.setattr(window, "_tick_analysis_3d", lambda: None)
|
||||
try:
|
||||
window._show_analysis_frame("old box packet")
|
||||
assert submitted == []
|
||||
window.show_analysis_packet("old box packet")
|
||||
assert submitted == [], "an old sensor's depth reached the new sensor's scene"
|
||||
session.reader.provenance.sensor_type = "finger"
|
||||
window._show_analysis_frame("finger packet")
|
||||
window.show_analysis_packet("finger packet")
|
||||
assert submitted == ["finger packet"]
|
||||
finally:
|
||||
panel._view = real_view
|
||||
|
||||
Reference in New Issue
Block a user