e9710e140b
从配置包读取几何资源,并提供显式无载荷启动置零。30 项测试、静态检查及现有录像验证通过。
150 lines
6.5 KiB
Python
150 lines
6.5 KiB
Python
"""Exercise the example's SDK boundary without opening a camera or display."""
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def example(monkeypatch):
|
|
path = Path(__file__).resolve().parents[1] / "examples" / "finger_pipeline.py"
|
|
monkeypatch.syspath_prepend(str(path.parent))
|
|
spec = importlib.util.spec_from_file_location("finger_pipeline_example", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.mark.parametrize("zero_mode", ["raw", "measure", "none"])
|
|
def test_schema3_reference_is_applied_after_warmup_before_pipeline(example, monkeypatch, capsys, zero_mode):
|
|
events = []
|
|
options = {}
|
|
snapshot = SimpleNamespace(fps=20., normal_force=1., shear_x=2., shear_y=3., age_ms=1.,
|
|
force_units=("N", "raw", "raw"), calibration_status={"fn": "ok"},
|
|
image=None, flow=None)
|
|
|
|
class Pipe:
|
|
def __enter__(self):
|
|
events.append("pipeline")
|
|
self.frames = iter([snapshot, None])
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
pass
|
|
|
|
def wait(self):
|
|
return next(self.frames)
|
|
|
|
runtime = SimpleNamespace(
|
|
tactile=SimpleNamespace(raw_sensor=SimpleNamespace(calibration=SimpleNamespace(schema_version=3)),
|
|
mask_stats={}, force_units=("wrong",) * 3),
|
|
warmup=lambda **kwargs: events.append("warmup"),
|
|
set_calibration_reference=lambda raw, **kwargs: events.append(("reference", raw, kwargs)),
|
|
pipeline=lambda **kwargs: Pipe(), close=lambda: events.append("close"))
|
|
|
|
def open_runtime(**kwargs):
|
|
options.update(kwargs)
|
|
return runtime
|
|
|
|
monkeypatch.setattr(example.FingerRuntime, "open", open_runtime)
|
|
monkeypatch.setattr(example, "measure_unloaded_reference",
|
|
lambda runtime: events.append("measure"))
|
|
monkeypatch.setattr(example.cv2, "waitKey", lambda *args: pytest.fail("headless must not call GUI"))
|
|
zero_args = {"raw": ["--zero-fn-raw", "1765.1"], "measure": ["--zero"], "none": []}[zero_mode]
|
|
monkeypatch.setattr(sys, "argv", ["finger_pipeline.py", "--video", "test.mp4", "--no-show",
|
|
"--cal-path", "model.json", "--device-id", "dd02-gl",
|
|
"--export-dir", "assets", "--map-cache", "map", *zero_args])
|
|
assert example.main() == 0
|
|
assert options["cal_path"] == "model.json" and options["device_id"] == "dd02-gl"
|
|
assert options["export_dir"] == "assets" and options["map_cache"] == "map"
|
|
reference_events = {"raw": [("reference", {"fn": 1765.1}, {"reference_id": "cli-unloaded-reference"})],
|
|
"measure": ["measure"], "none": []}[zero_mode]
|
|
assert events == ["warmup", *reference_events, "pipeline", "close"]
|
|
output = capsys.readouterr().out
|
|
assert "calibration=fn:ok" in output and "wrong" not in output
|
|
|
|
|
|
def test_selected_calibration_error_is_reported_without_fallback(example, monkeypatch, capsys):
|
|
def fail(**kwargs):
|
|
raise ValueError("calibration identity mismatch")
|
|
|
|
monkeypatch.setattr(example.FingerRuntime, "open", fail)
|
|
monkeypatch.setattr(sys, "argv", ["finger_pipeline.py", "--no-show", "--cal_path", "bad.json"])
|
|
assert example.main() == 1
|
|
assert "calibration identity mismatch" in capsys.readouterr().err
|
|
|
|
|
|
@pytest.mark.parametrize("arguments", [["--zero-fn-raw", "nan"], ["--calibration-dir", "calibrations"],
|
|
["--device-id", "SN1"], ["--cal-path", "a", "--calibration-dir", "b"]])
|
|
def test_invalid_selection_fails_before_capture(example, monkeypatch, arguments):
|
|
monkeypatch.setattr(example.FingerRuntime, "open", lambda **kwargs: pytest.fail("must not open capture"))
|
|
monkeypatch.setattr(sys, "argv", ["finger_pipeline.py", *arguments])
|
|
with pytest.raises(SystemExit) as exc:
|
|
example.main()
|
|
assert exc.value.code == 2
|
|
|
|
|
|
def test_bundle_geometry_follows_explicit_config(example, tmp_path, monkeypatch):
|
|
bundle = tmp_path / "custom"
|
|
bundle.mkdir()
|
|
(bundle / "sensor.json").write_text('{}')
|
|
(bundle / "manifest.json").write_text('{"assets_dir": "geometry"}')
|
|
(bundle / "geometry").mkdir()
|
|
monkeypatch.chdir(tmp_path)
|
|
assert Path(example.configured_export_dir("custom/sensor.json")) == bundle / "geometry"
|
|
|
|
|
|
def test_missing_declared_geometry_does_not_use_default_bundle(example, tmp_path):
|
|
(tmp_path / "sensor.json").write_text('{}')
|
|
(tmp_path / "manifest.json").write_text('{"assets_dir": "missing"}')
|
|
with pytest.raises(ValueError, match="assets_dir"):
|
|
example.configured_export_dir(str(tmp_path / "sensor.json"))
|
|
|
|
|
|
@pytest.mark.parametrize("advanced", [False, True])
|
|
def test_help_separates_debug_overrides(example, monkeypatch, capsys, advanced):
|
|
monkeypatch.setattr(sys, "argv", ["finger_pipeline.py", "--help-advanced" if advanced else "--help"])
|
|
with pytest.raises(SystemExit) as exc:
|
|
example.main()
|
|
assert exc.value.code == 0
|
|
output = capsys.readouterr().out
|
|
assert "--cal-path" in output and "--zero " in output
|
|
assert ("--zero-fn-raw" in output) == advanced
|
|
assert ("--export-dir" in output) == advanced
|
|
assert ("--device-id" in output) == advanced
|
|
|
|
|
|
@pytest.mark.parametrize("failure", [None, "short", "nan", "legacy"])
|
|
def test_unloaded_reference_is_measured_atomically(example, failure):
|
|
events = []
|
|
frames = iter(range(30))
|
|
count = 0
|
|
|
|
def grab(**kwargs):
|
|
nonlocal count
|
|
count += 1
|
|
return not (failure == "short" and count == 30), None
|
|
|
|
def read(*keys):
|
|
value = next(frames)
|
|
return (float("nan") if failure == "nan" and value == 29 else value,)
|
|
|
|
runtime = SimpleNamespace(
|
|
tactile=SimpleNamespace(
|
|
raw_sensor=SimpleNamespace(calibration=SimpleNamespace(
|
|
schema_version=2 if failure == "legacy" else 3, channels={"fn": {}})),
|
|
info=SimpleNamespace(FNORMAL_RAW=0, FSHEARX_RAW=1, FSHEARY_RAW=2),
|
|
compute_deformation=lambda: None, read_info=read),
|
|
grab_and_process=grab,
|
|
set_calibration_reference=lambda raw, **kwargs: events.append((raw, kwargs)))
|
|
if failure:
|
|
with pytest.raises(ValueError):
|
|
example.measure_unloaded_reference(runtime)
|
|
assert events == []
|
|
else:
|
|
example.measure_unloaded_reference(runtime)
|
|
assert events == [({"fn": 14.5}, {"reference_id": "startup-unloaded-reference"})]
|