"""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"})]