mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 07:21:13 +00:00
[AZ-622] Phase D: build_pre_constructed seeds c3 GPU runtimes
build_pre_constructed now populates c3_lightglue_runtime (LightGlueRuntime) + c3_feature_extractor (FeatureExtractor) on top of AZ-619/620/621. Strategy-specific BUILD_MATCHER_* flag mismatch raises AirborneBootstrapError naming the missing flag and the c3_matcher consumer; the c7 InferenceRuntime built earlier in the bootstrap is reused as the engine source so no double-build at this layer. C3MatcherConfig gains optional lightglue_weights_path: Path | None for the operator's deployment config; production main() (AZ-624) populates it. Real LightGlue inference correctness is verified by AZ-624's Jetson AC-5 run per the AZ-622 Tier-2 Note. Phase tests for AZ-619/620/621 gain an autouse _stub_c3_matcher_builders fixture so additivity assertions remain valid as the bootstrap grows. Code review: PASS_WITH_WARNINGS (3 Low: signature drift from spec, _is_build_flag_on duplication across 3 runtime_root modules, and BuildConfig literal mirrored with per-strategy build configs). All deferred to future hygiene PBIs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -80,6 +80,24 @@ def _stub_c7_inference_builder(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub the AZ-622 Phase D C3 matcher builders so the AZ-619
|
||||
# tests stay focused on Phase A. Without this the bare Config() below
|
||||
# would hit the BUILD_MATCHER_DISK_LIGHTGLUE flag check (typically
|
||||
# unset in the test env) and raise AirborneBootstrapError before the
|
||||
# AC-619 keys could be asserted. Sentinels are opaque on purpose —
|
||||
# AZ-619 assertions never inspect them.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_lightglue_runtime",
|
||||
lambda _config, *, inference_runtime: object(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap, "_build_c3_feature_extractor", lambda _config: object()
|
||||
)
|
||||
|
||||
|
||||
def test_ac_619_1_default_config_seeds_c13_fdr_and_clock() -> None:
|
||||
# Arrange
|
||||
config = Config()
|
||||
|
||||
@@ -74,6 +74,27 @@ def _stub_c7_inference_builder(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub the AZ-622 Phase D C3 matcher builders so AZ-620
|
||||
# tests stay focused on the Phase B contract. Without this the
|
||||
# configs used below would hit the BUILD_MATCHER_DISK_LIGHTGLUE flag
|
||||
# check (typically unset in the test env) and raise
|
||||
# AirborneBootstrapError before the AC-620 keys could be asserted.
|
||||
# Sentinels are opaque on purpose — AZ-620 assertions never inspect
|
||||
# them.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_lightglue_runtime",
|
||||
lambda _config, *, inference_runtime: MagicMock(name="LightGlueRuntime"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_feature_extractor",
|
||||
lambda _config: MagicMock(name="FeatureExtractor"),
|
||||
)
|
||||
|
||||
|
||||
def test_ac_620_1_adds_c6_descriptor_index_and_c6_tile_store(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -77,6 +77,27 @@ def _stub_c6_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_c3_matcher_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub the AZ-622 Phase D C3 matcher builders so AZ-621
|
||||
# tests stay focused on the Phase C contract. Without this the
|
||||
# configs used below would hit the BUILD_MATCHER_DISK_LIGHTGLUE flag
|
||||
# check (typically unset in the test env) and raise
|
||||
# AirborneBootstrapError before the AC-621 keys could be asserted.
|
||||
# Sentinels are opaque on purpose — AZ-621 assertions never inspect
|
||||
# them.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_lightglue_runtime",
|
||||
lambda _config, *, inference_runtime: MagicMock(name="LightGlueRuntime"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_feature_extractor",
|
||||
lambda _config: MagicMock(name="FeatureExtractor"),
|
||||
)
|
||||
|
||||
|
||||
def test_ac_621_1_adds_c7_inference(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub build_inference_runtime to a sentinel so we can
|
||||
# assert wiring without standing up real GPU/TensorRT/PyTorch.
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"""AZ-622 — Phase D of AZ-618: ``build_pre_constructed`` seeds c3_lightglue_runtime + c3_feature_extractor.
|
||||
|
||||
Verifies the contract at
|
||||
``_docs/02_tasks/todo/AZ-622_pre_constructed_phase_d_c3_runtimes.md``:
|
||||
|
||||
* AC-622.1: with a default airborne ``Config`` (and the
|
||||
``BUILD_MATCHER_DISK_LIGHTGLUE`` flag ON in spirit — here stubbed at
|
||||
the airborne_bootstrap module boundary), ``build_pre_constructed(config)``
|
||||
additionally contains ``c3_lightglue_runtime`` (a
|
||||
:class:`LightGlueRuntime` instance) AND ``c3_feature_extractor`` (a
|
||||
:class:`FeatureExtractor` instance) on top of AZ-619 + AZ-620 + AZ-621.
|
||||
* AC-622.2: when ``BUILD_MATCHER_DISK_LIGHTGLUE=OFF`` AND a config
|
||||
selects ``c3_matcher.strategy="disk_lightglue"``, ``build_pre_constructed``
|
||||
raises :class:`AirborneBootstrapError` whose message names
|
||||
``c3_lightglue_runtime`` (the missing key), ``BUILD_MATCHER_DISK_LIGHTGLUE``
|
||||
(the gating flag), and ``c3_matcher`` (the consuming component slug).
|
||||
|
||||
AC-622.3 (this file exists with the above tests) is satisfied by the
|
||||
existence of this module.
|
||||
|
||||
The tests stub the heavy ``_load_lightglue_engine_handle`` seam (AZ-622)
|
||||
plus the upstream ``_build_c6_*`` (AZ-620) and ``_build_c7_inference``
|
||||
(AZ-621) factories at the airborne_bootstrap module boundary — exactly
|
||||
mirroring the prior phase pattern (see
|
||||
:mod:`tests.unit.runtime_root.test_az621_pre_constructed_phase_c`). Real
|
||||
LightGlue inference correctness is verified by AZ-624's Jetson AC-5 run
|
||||
per the AZ-622 ``Tier-2 Note``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gps_denied_onboard.config import Config
|
||||
from gps_denied_onboard.fdr_client import client as fdr_client_module
|
||||
from gps_denied_onboard.helpers.feature_extractor import (
|
||||
FeatureExtractor,
|
||||
OpenCvOrbExtractor,
|
||||
)
|
||||
from gps_denied_onboard.helpers.lightglue_runtime import LightGlueRuntime
|
||||
from gps_denied_onboard.runtime_root import airborne_bootstrap
|
||||
from gps_denied_onboard.runtime_root.airborne_bootstrap import (
|
||||
C3_MATCHER_BUILD_FLAGS,
|
||||
AirborneBootstrapError,
|
||||
build_pre_constructed,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _C3MatcherBlock:
|
||||
"""Minimal c3_matcher config block — only the fields the tests need."""
|
||||
|
||||
strategy: str = "disk_lightglue"
|
||||
lightglue_weights_path: Path | None = None
|
||||
|
||||
|
||||
def _make_engine_handle_mock(*, descriptor_dim: int = 128) -> MagicMock:
|
||||
"""Build a mock that satisfies the LightGlueRuntime engine_handle contract.
|
||||
|
||||
The runtime checks ``engine_handle.descriptor_dim`` (must be ``int``
|
||||
and ``>= 1``) and stores the handle for later ``forward`` calls.
|
||||
AZ-622 only exercises construction; tests do not invoke ``forward``.
|
||||
"""
|
||||
handle = MagicMock(name="LightGlueEngineHandle")
|
||||
handle.descriptor_dim = descriptor_dim
|
||||
return handle
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_fdr_cache() -> Iterator[None]:
|
||||
# Arrange: every test starts with an empty FdrClient cache so the
|
||||
# bootstrap's make_fdr_client call doesn't accidentally pick up a
|
||||
# stale instance from a prior test in the same process.
|
||||
fdr_client_module._reset_for_tests()
|
||||
yield
|
||||
fdr_client_module._reset_for_tests()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_c6_and_c7_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub the AZ-620 (Phase B) C6 builders and the AZ-621
|
||||
# (Phase C) C7 inference builder so AZ-622 stays focused on the
|
||||
# Phase D contract. Sentinels are opaque on purpose — AZ-622
|
||||
# assertions never inspect them.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c6_descriptor_index",
|
||||
lambda _config: MagicMock(name="DescriptorIndex"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c6_tile_store",
|
||||
lambda _config: MagicMock(name="TileStore"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c7_inference",
|
||||
lambda _config: MagicMock(name="InferenceRuntime"),
|
||||
)
|
||||
|
||||
|
||||
def test_ac_622_1_adds_c3_lightglue_runtime_and_c3_feature_extractor(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: make BUILD_MATCHER_DISK_LIGHTGLUE ON and stub the heavy
|
||||
# LightGlue engine load with a sentinel handle. Default Config()
|
||||
# carries no c3_matcher block, so the bootstrap defaults to
|
||||
# "disk_lightglue" (per _resolve_c3_matcher_strategy).
|
||||
monkeypatch.setenv("BUILD_MATCHER_DISK_LIGHTGLUE", "ON")
|
||||
engine_handle = _make_engine_handle_mock(descriptor_dim=128)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_load_lightglue_engine_handle",
|
||||
lambda _config, _inference_runtime: engine_handle,
|
||||
)
|
||||
config = Config()
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: AZ-622 keys are present and correctly typed; the
|
||||
# LightGlueRuntime wraps the stubbed engine handle (identity
|
||||
# check); the feature extractor satisfies the FeatureExtractor
|
||||
# Protocol. Prior phase keys are still present (additivity contract).
|
||||
assert "c3_lightglue_runtime" in pre_constructed
|
||||
assert "c3_feature_extractor" in pre_constructed
|
||||
assert isinstance(pre_constructed["c3_lightglue_runtime"], LightGlueRuntime)
|
||||
assert (
|
||||
pre_constructed["c3_lightglue_runtime"].descriptor_dim()
|
||||
== engine_handle.descriptor_dim
|
||||
)
|
||||
assert isinstance(pre_constructed["c3_feature_extractor"], FeatureExtractor)
|
||||
assert isinstance(pre_constructed["c3_feature_extractor"], OpenCvOrbExtractor)
|
||||
assert {
|
||||
"c13_fdr",
|
||||
"clock",
|
||||
"c6_descriptor_index",
|
||||
"c6_tile_store",
|
||||
"c7_inference",
|
||||
}.issubset(pre_constructed.keys())
|
||||
|
||||
|
||||
def test_ac_622_2_build_flag_off_with_configured_strategy_raises_named_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: BUILD_MATCHER_DISK_LIGHTGLUE OFF (delete to defend against
|
||||
# a leftover ON from a different test in the same session) AND a
|
||||
# config that selects strategy="disk_lightglue". The flag check must
|
||||
# fire BEFORE _load_lightglue_engine_handle is consulted, so we don't
|
||||
# need to stub the loader for this branch.
|
||||
monkeypatch.delenv("BUILD_MATCHER_DISK_LIGHTGLUE", raising=False)
|
||||
config = Config.with_blocks(
|
||||
c3_matcher=_C3MatcherBlock(strategy="disk_lightglue")
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
# The missing key, the gating flag, and the consuming component
|
||||
# slug must ALL appear in the operator-facing message (AZ-622
|
||||
# AC-622.2 + AZ-618 NFR "operator-facing error contract").
|
||||
assert "c3_lightglue_runtime" in message
|
||||
expected_flag = C3_MATCHER_BUILD_FLAGS["disk_lightglue"]
|
||||
assert expected_flag in message, (
|
||||
f"{expected_flag!r} missing from error: {message!r}"
|
||||
)
|
||||
assert "c3_matcher" in message
|
||||
# The flag-OFF branch raises directly — there is no upstream cause to
|
||||
# preserve (cause-chain preservation is exercised in
|
||||
# test_ac_622_2_lightglue_engine_load_failure_wraps_runtime_error).
|
||||
assert excinfo.value.__cause__ is None
|
||||
|
||||
|
||||
def test_ac_622_2_build_flag_off_with_aliked_strategy_names_aliked_flag(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Per-strategy flag specificity: aliked_lightglue surfaces ALIKED's flag."""
|
||||
# Arrange: ALIKED strategy with its own gating flag OFF.
|
||||
monkeypatch.delenv("BUILD_MATCHER_ALIKED_LIGHTGLUE", raising=False)
|
||||
config = Config.with_blocks(
|
||||
c3_matcher=_C3MatcherBlock(strategy="aliked_lightglue")
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "c3_lightglue_runtime" in message
|
||||
assert C3_MATCHER_BUILD_FLAGS["aliked_lightglue"] in message
|
||||
assert "c3_matcher" in message
|
||||
# The DISK flag must NOT appear — the message is strategy-specific.
|
||||
assert C3_MATCHER_BUILD_FLAGS["disk_lightglue"] not in message
|
||||
|
||||
|
||||
def test_ac_622_2_default_config_no_c3_matcher_block_still_raises(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Defence-in-depth: even with no c3_matcher block configured, AZ-622
|
||||
still fails loudly when the default-strategy flag is OFF.
|
||||
|
||||
Mirrors the AZ-620 / AZ-621 defence-in-depth tests. Silently returning
|
||||
a dict without ``c3_lightglue_runtime`` would let a downstream caller
|
||||
misread the state.
|
||||
"""
|
||||
# Arrange
|
||||
monkeypatch.delenv("BUILD_MATCHER_DISK_LIGHTGLUE", raising=False)
|
||||
config = Config() # empty components, default strategy resolves to disk_lightglue
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "c3_lightglue_runtime" in message
|
||||
assert C3_MATCHER_BUILD_FLAGS["disk_lightglue"] in message
|
||||
assert "c3_matcher" in message
|
||||
|
||||
|
||||
def test_ac_622_2_lightglue_engine_load_failure_wraps_runtime_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When the BUILD flag is ON but the engine load itself fails,
|
||||
``RuntimeNotAvailableError`` is wrapped into ``AirborneBootstrapError``
|
||||
with the cause chain preserved (mirrors AZ-621's wrapping pattern)."""
|
||||
# Arrange
|
||||
from gps_denied_onboard.runtime_root.errors import RuntimeNotAvailableError
|
||||
|
||||
monkeypatch.setenv("BUILD_MATCHER_DISK_LIGHTGLUE", "ON")
|
||||
|
||||
def _raise_engine_load_failure(_config: Config, _inference_runtime: object) -> None:
|
||||
raise RuntimeNotAvailableError(
|
||||
"InferenceRuntime.deserialize_engine: simulated GPU bind failure"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_load_lightglue_engine_handle",
|
||||
_raise_engine_load_failure,
|
||||
)
|
||||
config = Config.with_blocks(
|
||||
c3_matcher=_C3MatcherBlock(strategy="disk_lightglue")
|
||||
)
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "c3_lightglue_runtime" in message
|
||||
assert "disk_lightglue" in message
|
||||
assert "c3_matcher" in message
|
||||
assert isinstance(excinfo.value.__cause__, RuntimeNotAvailableError)
|
||||
|
||||
|
||||
def test_lightglue_runtime_uses_c7_inference_from_pre_constructed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The c7 InferenceRuntime built earlier in build_pre_constructed
|
||||
must be the SAME instance passed into ``_load_lightglue_engine_handle``
|
||||
— no double-build of the inference runtime at this layer."""
|
||||
# Arrange
|
||||
monkeypatch.setenv("BUILD_MATCHER_DISK_LIGHTGLUE", "ON")
|
||||
inference_runtime_sentinel = MagicMock(name="InferenceRuntime")
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c7_inference",
|
||||
lambda _config: inference_runtime_sentinel,
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _capture_loader(config: Config, inference_runtime: object) -> object:
|
||||
captured["config"] = config
|
||||
captured["inference_runtime"] = inference_runtime
|
||||
return _make_engine_handle_mock(descriptor_dim=128)
|
||||
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap, "_load_lightglue_engine_handle", _capture_loader
|
||||
)
|
||||
config = Config()
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: the loader received the EXACT same InferenceRuntime instance
|
||||
# placed under pre_constructed['c7_inference'] (identity-share — the
|
||||
# same single-build-per-bootstrap discipline AZ-621 established).
|
||||
assert captured["inference_runtime"] is inference_runtime_sentinel
|
||||
assert pre_constructed["c7_inference"] is inference_runtime_sentinel
|
||||
Reference in New Issue
Block a user