From e2b906a640348a8413a6394182ee7ee6cd43a17d Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Sun, 20 Sep 2026 11:20:47 +0800 Subject: [PATCH] fix preflight config path check --- src/orisys/configs.py | 12 +++- src/orisys/sensor.py | 6 +- tests/test_signal_processing.py | 119 ++++++++++++++++++++++++-------- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/orisys/configs.py b/src/orisys/configs.py index 7d0d0aa..63ee12d 100644 --- a/src/orisys/configs.py +++ b/src/orisys/configs.py @@ -103,7 +103,17 @@ def _runtime_roots() -> list[Path]: except (OSError, ValueError): # pragma: no cover - defensive continue - return [path for path in _unique_paths(anchors) if _is_sdk_runtime_root(path)] + anchors = _unique_paths(anchors) + # Keep the *innermost* matching directories. Walking up from the launcher + # also visits every parent, and a parent that happens to carry an + # ``examples/`` directory must not outrank the real root below it: doing so + # made a workspace directory shadow the release root's own bundle. + return [ + path + for path in anchors + if _is_sdk_runtime_root(path) + and not any(other != path and other.is_relative_to(path) for other in anchors) + ] def _candidate_config_dirs() -> list[Path]: diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 8473e01..c31c82f 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -1271,9 +1271,11 @@ class Sensor: that root had coefficients. """ attempts = self._force_cal_attempts or [] - found = [entry for entry in attempts if entry.get("found_root") is not None] + # ``found_root`` is keyed even when nothing was found (valued None), so + # test its value, not the key. + found = [entry for entry in attempts if entry.get("found_root")] with_cal = [entry for entry in found if entry.get("force_calibration_json")] - chosen = (with_cal or found or attempts or [None])[0] + chosen = (with_cal or found or [None])[0] searched: list = [] for entry in attempts: for root in entry.get("searched") or (): diff --git a/tests/test_signal_processing.py b/tests/test_signal_processing.py index 0a735dd..1a8c6f1 100644 --- a/tests/test_signal_processing.py +++ b/tests/test_signal_processing.py @@ -293,16 +293,22 @@ _CAL_DOC = { def _make_root(base, *, name="anything", sensor_json=True, calibration=True): - """A runnable SDK root: ``examples/`` beside ``sensors//``.""" + """A runnable SDK root: ``examples/`` beside ``sensors//``. + + ``sensor.json`` is copied from the shipped bundle because it is a *sensor + config*: a stub document would make ``_load_config`` depend on whichever + config the machine happens to resolve, which is not what these tests are + about. + """ root = base / name (root / "examples").mkdir(parents=True, exist_ok=True) - if sensor_json: - (root / "examples" / "basic_pipeline.py").write_text("# demo\n", encoding="utf-8") + (root / "examples" / "basic_pipeline.py").write_text("# demo\n", encoding="utf-8") bundle = root / "sensors" / "mini" bundle.mkdir(parents=True, exist_ok=True) if sensor_json: + shipped = Path(__file__).resolve().parents[1] / "sensors" / "mini" / "sensor.json" (bundle / "sensor.json").write_text( - json.dumps({"name": "prototype"}), encoding="utf-8" + shipped.read_text(encoding="utf-8"), encoding="utf-8" ) if calibration: (bundle / "force_calibration.json").write_text( @@ -311,6 +317,18 @@ def _make_root(base, *, name="anything", sensor_json=True, calibration=True): return root +def _bare_sensor(**attrs): + """A Sensor instance without running ``__init__``/``_load_config``. + + The note-formatting helpers touch no sensor state, so a whole sensor (and + therefore a whole machine config) is not needed to test them. + """ + sensor = object.__new__(Sensor) + for key, value in attrs.items(): + setattr(sensor, key, value) + return sensor + + def test_runtime_root_anchor_keys_on_layout_not_on_the_folder_name(tmp_path): named = _make_root(tmp_path, name="some-random-name") assert configs._is_sdk_runtime_root(named) is True @@ -332,6 +350,24 @@ def test_runtime_roots_walk_up_from_a_subdirectory(tmp_path, monkeypatch): assert str(root) in roots +def test_a_parent_root_never_outranks_the_root_below_it(tmp_path, monkeypatch): + """Walking up must not let a parent's bundle shadow the real run root. + + A directory that happens to carry ``examples/`` above the launcher (a + workspace holding several checkouts, one of which keeps an ``examples/`` + folder) used to be treated as a run root too, and its ``sensors/`` came + first — silently shadowing the root the operator was actually running from. + """ + parent = tmp_path / "workspace" + inner = _make_root(parent, name="inner-release") + (parent / "examples").mkdir(parents=True, exist_ok=True) + assert configs._is_sdk_runtime_root(parent) is True + + monkeypatch.setattr(configs.Path, "cwd", classmethod(lambda cls: inner)) + roots = [str(path) for path in configs._runtime_roots()] + assert roots == [str(inner)], roots + + def test_the_running_root_bundle_beats_a_calibration_less_checkout( tmp_path, monkeypatch ): @@ -339,6 +375,13 @@ def test_the_running_root_bundle_beats_a_calibration_less_checkout( running = _make_root(tmp_path, name="running-root") checkout = _make_root(tmp_path, name="importable-checkout", calibration=False) monkeypatch.setattr(configs, "_runtime_roots", lambda: [running, checkout]) + # Pinned to real directories, so a leftover or unrelated bundle elsewhere on + # the machine cannot decide the outcome. + monkeypatch.setattr( + configs, + "_candidate_sensor_roots", + lambda: [running / "sensors", checkout / "sensors"], + ) bundle = configs.resolve_sensor_bundle("mini") assert bundle is not None @@ -347,17 +390,16 @@ def test_the_running_root_bundle_beats_a_calibration_less_checkout( running / "sensors" / "mini" / "force_calibration.json" ).resolve() - # The whole point: the running root's document is the one that reaches the - # sensor, split shear and all. - sensor = Sensor("external", config_name="mini", verbose=False, backend="cpu") - try: - assert sensor.force_calibrated is True - assert sensor.force_unit == "N" - assert sensor.calibration.source_path == ( - running / "sensors" / "mini" / "force_calibration.json" - ).resolve() - finally: - sensor.disconnect() + # The whole point: the running root's document is the one the SDK loads. + from orisys.calibration import load as load_calibration + + cal = load_calibration(sensor_type="mini") + assert cal is not None + assert cal.schema_version == 2 + assert sorted(cal.channels) == ["fn", "fx", "fy"] + assert cal.source_path == ( + running / "sensors" / "mini" / "force_calibration.json" + ).resolve() def test_a_nearer_bundle_wins_even_without_coefficients(tmp_path, monkeypatch): @@ -370,6 +412,11 @@ def test_a_nearer_bundle_wins_even_without_coefficients(tmp_path, monkeypatch): checkout = _make_root(tmp_path, name="checkout", calibration=False) running = _make_root(tmp_path, name="run-root") monkeypatch.setattr(configs, "_runtime_roots", lambda: [checkout, running]) + monkeypatch.setattr( + configs, + "_candidate_sensor_roots", + lambda: [checkout / "sensors", running / "sensors"], + ) bundle = configs.resolve_sensor_bundle("mini") assert bundle is not None assert bundle.root == (checkout / "sensors" / "mini").resolve() @@ -402,19 +449,31 @@ def test_describe_bundle_resolution_names_the_bundle_and_the_searched_roots( assert missing["force_calibration_json"] is False -def test_the_calibration_note_names_the_bundle_that_was_found( - tmp_path, monkeypatch -): +def test_the_calibration_note_names_the_bundle_that_was_found(tmp_path, monkeypatch): + """The note explains a bundle with no coefficients, not a "missing file".""" checkout = _make_root(tmp_path, name="checkout", calibration=False) - monkeypatch.setattr(configs, "_runtime_roots", lambda: [checkout]) - sensor = Sensor("external", config_name="mini", verbose=False, backend="cpu") - try: - assert sensor.calibration is None - # Still None: callers read a non-None value here as "calibrated". - assert sensor._force_cal_sensor_name is None - note = sensor._force_calibration_note() - assert "has no force_calibration.json" in note - assert str((checkout / "sensors" / "mini").resolve()) in note - assert "roots searched" in note - finally: - sensor.disconnect() + roots = [checkout / "sensors"] + monkeypatch.setattr(configs, "_candidate_sensor_roots", lambda: list(roots)) + + # Composed via _bare_sensor: the note reads only _force_cal_attempts, so this + # needs no machine config and cannot fail on config resolution. + sensor = _bare_sensor( + _force_cal_attempts=[configs.describe_bundle_resolution("mini")], + _force_cal_sensor_name=None, + ) + note = sensor._force_calibration_note() + assert "has no force_calibration.json" in note + assert str((checkout / "sensors" / "mini").resolve()) in note + assert "roots searched" in note + + # No bundle anywhere: say that instead of naming a root. + (checkout / "sensors" / "mini" / "sensor.json").unlink() + sensor = _bare_sensor( + _force_cal_attempts=[configs.describe_bundle_resolution("mini")], + _force_cal_sensor_name=None, + ) + note = sensor._force_calibration_note() + assert "no sensor bundle resolved" in note + # A dict always keys ``found_root``, so guard its value: an unguarded copy of + # this branch prints "bundle found at None". + assert "None" not in note