修复 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:
ykcheungab
2026-10-18 19:24:31 +08:00
parent dcabd547fa
commit 433b76e43c
38 changed files with 1036 additions and 171 deletions
+57
View File
@@ -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()
+8 -43
View File
@@ -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"
)
+34
View File
@@ -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,
1 at t_rel_s frame_id kind detail
2 2026-09-30T01:50:28.848+00:00 -1 record_started mode=video_data
3 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
1 session_id frame_id t_rel_s FN_raw FX_raw FY_raw FN_N FX_N FY_N valid calibration_id
2 2 20 0.0 13567.7392578125 1180.1070556640625 9125.2607421875 0.555386962890625 0.006123639892578124 0.276258865234375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
3 2 21 0.064133 11343.3232421875 2028.0238037109375 7816.4375 0.444166162109375 0.034952809326171876 0.23175887499999998 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
4 2 22 0.112021 16884.728515625 745.5536499023438 7191.43896484375 0.72123642578125 0.0 0.2105089248046875 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
5 2 23 0.17623 14818.259765625 2178.03466796875 9171.318359375 0.61791298828125 0.0400531787109375 0.27782482421875 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
6 2 24 0.24043 14969.4208984375 -1287.072509765625 11873.1181640625 0.625471044921875 -0.00976046533203125 0.36968601757812497 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
7 2 25 0.304304 14659.830078125 -141.17251586914062 2469.820556640625 0.60999150390625 -0.0 0.049973898925781246 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
8 2 26 0.352255 15476.34375 -3281.966552734375 15066.1357421875 0.6508171875 -0.07758686279296875 0.478248615234375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
9 2 27 0.416172 11016.5068359375 -3127.916748046875 13734.294921875 0.427825341796875 -0.07234916943359375 0.43296602734375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
10 2 28 0.480077 14330.064453125 273.25970458984375 5539.169921875 0.59350322265625 0.0 0.15433177734375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
11 2 29 0.543786 14844.3466796875 2567.879150390625 5841.8525390625 0.619217333984375 0.05330789111328125 0.164622986328125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
12 2 30 0.591816 16986.365234375 -1623.703125 5377.94091796875 0.72631826171875 -0.02120590625 0.1488499912109375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
13 2 31 0.656365 13733.6103515625 -1030.4215087890625 9520.9345703125 0.563680517578125 -0.0010343312988281249 0.289711775390625 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
14 2 32 0.720031 17051.982421875 -2590.688720703125 10953.798828125 0.72959912109375 -0.05408341650390625 0.33842916015625 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
15 2 33 0.784173 16303.94921875 2727.433837890625 6891.0400390625 0.6921974609375 0.05873275048828125 0.20029536132812498 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
16 2 34 0.831679 17633.34375 -208.40782165527344 5174.18798828125 0.7586671875 -0.0 0.14192239160156248 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
17 2 35 0.896141 14191.8671875 -971.9476928710938 11894.728515625 0.586593359375 -0.0 0.37042076953124997 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
18 2 36 0.960091 15828.78125 -1548.9967041015625 9286.84765625 0.6684390625000001 -0.018665887939453125 0.2817528203125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
19 2 37 1.024524 18615.650390625 -3690.62451171875 10522.7822265625 0.80778251953125 -0.0914812333984375 0.323774595703125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
20 2 38 1.071701 15312.724609375 1154.6263427734375 11247.951171875 0.6426362304687501 0.005257295654296875 0.34843033984375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
21 2 39 1.135903 12159.138671875 -82.20235443115234 10962.3251953125 0.48495693359375003 -0.0 0.33871905664062496 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
22 2 40 1.199965 15671.615234375 -540.1160888671875 17301.103515625 0.66058076171875 -0.0 0.55423751953125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
23 2 41 1.264015 18193.279296875 -280.51025390625 7286.34765625 0.78666396484375 -0.0 0.2137358203125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
24 2 42 1.311786 14463.1240234375 23.800241470336914 14572.9287109375 0.6001562011718751 0.0 0.461479576171875 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
25 2 43 1.376278 14139.1728515625 549.9257202148438 14745.4931640625 0.583958642578125 0.0 0.467346767578125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
26 2 44 1.439864 14622.09375 535.4642944335938 11325.8857421875 0.6081046875 0.0 0.351080115234375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
27 2 45 1.503909 16870.396484375 -5440.37158203125 13050.87890625 0.72051982421875 -0.15097263378906248 0.4097298828125 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
28 2 46 1.551771 17459.4765625 -322.2771911621094 9053.3154296875 0.749973828125 -0.0 0.273812724609375 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
29 2 47 1.616042 21696.408203125 -1986.04833984375 11181.533203125 0.96182041015625 -0.0335256435546875 0.34617212890625 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
30 2 48 1.680262 15386.84765625 -4424.57763671875 1783.9827880859375 0.6463423828125 -0.11643563964843749 0.026655414794921874 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
31 2 49 1.744307 15400.05859375 -2761.5791015625 15228.455078125 0.6470029296875 -0.059893689453124996 0.48376747265625 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
32 2 50 1.792308 14120.4208984375 116.24051666259766 9754.70703125 0.583021044921875 0.0 0.2976600390625 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
33 2 51 1.856446 15246.4052734375 -2575.3466796875 12753.658203125 0.639320263671875 -0.053561787109375 0.39962437890624997 1 171e598ede7eaca213139b615dae0c5f86756256f0645b1dc5002fa3b810c348
34 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
1 frame_id t_rel_s acquired_at segment segment_frame in_video dropped
2 20 0.0 2026-09-30T01:50:28.870+00:00 1 1 1 0
3 21 0.064133 2026-09-30T01:50:28.935+00:00 1 2 1 0
4 22 0.112021 2026-09-30T01:50:28.983+00:00 1 3 1 0
5 23 0.17623 2026-09-30T01:50:29.048+00:00 1 4 1 0
6 24 0.24043 2026-09-30T01:50:29.111+00:00 1 5 1 0
7 25 0.304304 2026-09-30T01:50:29.175+00:00 1 6 1 0
8 26 0.352255 2026-09-30T01:50:29.231+00:00 1 7 1 0
9 27 0.416172 2026-09-30T01:50:29.287+00:00 1 8 1 0
10 28 0.480077 2026-09-30T01:50:29.351+00:00 1 9 1 0
11 29 0.543786 2026-09-30T01:50:29.414+00:00 1 10 1 0
12 30 0.591816 2026-09-30T01:50:29.462+00:00 1 11 1 0
13 31 0.656365 2026-09-30T01:50:29.526+00:00 1 12 1 0
14 32 0.720031 2026-09-30T01:50:29.590+00:00 1 13 1 0
15 33 0.784173 2026-09-30T01:50:29.654+00:00 1 14 1 0
16 34 0.831679 2026-09-30T01:50:29.702+00:00 1 15 1 0
17 35 0.896141 2026-09-30T01:50:29.766+00:00 1 16 1 0
18 36 0.960091 2026-09-30T01:50:29.830+00:00 1 17 1 0
19 37 1.024524 2026-09-30T01:50:29.895+00:00 1 18 1 0
20 38 1.071701 2026-09-30T01:50:29.942+00:00 1 19 1 0
21 39 1.135903 2026-09-30T01:50:30.006+00:00 1 20 1 0
22 40 1.199965 2026-09-30T01:50:30.070+00:00 1 21 1 0
23 41 1.264015 2026-09-30T01:50:30.134+00:00 1 22 1 0
24 42 1.311786 2026-09-30T01:50:30.182+00:00 1 23 1 0
25 43 1.376278 2026-09-30T01:50:30.247+00:00 1 24 1 0
26 44 1.439864 2026-09-30T01:50:30.310+00:00 1 25 1 0
27 45 1.503909 2026-09-30T01:50:30.373+00:00 1 26 1 0
28 46 1.551771 2026-09-30T01:50:30.422+00:00 1 27 1 0
29 47 1.616042 2026-09-30T01:50:30.486+00:00 1 28 1 0
30 48 1.680262 2026-09-30T01:50:30.552+00:00 1 29 1 0
31 49 1.744307 2026-09-30T01:50:30.615+00:00 1 30 1 0
32 50 1.792308 2026-09-30T01:50:30.664+00:00 1 31 1 0
33 51 1.856446 2026-09-30T01:50:30.728+00:00 1 32 1 0
34 52 1.920255 2026-09-30T01:50:30.792+00:00 1 33 1 0
@@ -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": {}
}
}
+28 -22
View File
@@ -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 -------------------------
+1 -1
View File
@@ -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):
+45 -19
View File
@@ -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
+48 -29
View File
@@ -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
+9 -5
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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)
+61 -12
View File
@@ -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