"""Unit tests for the runner conftest's skip / xfail enforcement. We exercise `pytest_collection_modifyitems` directly with a fake config and a synthetic item list, then assert the post-conditions (marker added, etc.). This catches regressions where someone changes the skip rules without updating the traceability matrix — see `_docs/02_document/tests/traceability-matrix.md` § Uncovered Items Analysis. """ from __future__ import annotations import sys from pathlib import Path from types import SimpleNamespace import pytest _E2E_ROOT = Path(__file__).resolve().parents[1] if str(_E2E_ROOT) not in sys.path: sys.path.insert(0, str(_E2E_ROOT)) from runner.conftest import pytest_collection_modifyitems # noqa: E402 class _Marker(SimpleNamespace): pass class _FakeKeywords(set): """Mimic pytest.Item.keywords (a set-with-`in` semantics over marker names).""" class _FakeItem: def __init__( self, keywords: set[str] | None = None, markers: dict[str, _Marker] | None = None, callspec: SimpleNamespace | None = None, ) -> None: self.keywords = _FakeKeywords(keywords or set()) self._markers = markers or {} self.callspec = callspec self.added_markers: list[_Marker] = [] def get_closest_marker(self, name: str) -> _Marker | None: return self._markers.get(name) def add_marker(self, marker: _Marker) -> None: self.added_markers.append(marker) class _FakeConfig: def __init__(self, chamber: bool = False, build_kind: str = "production", allow_no_reason: bool = False) -> None: self._chamber = chamber self._build_kind = build_kind self._allow_no_reason = allow_no_reason def getoption(self, name: str) -> object: return { "--enable-chamber": self._chamber, "--build-kind": self._build_kind, "--allow-no-skip-reason": self._allow_no_reason, }[name] def _skip_reasons(item: _FakeItem) -> list[str]: out: list[str] = [] for m in item.added_markers: # pytest.mark.skip(reason=...) returns a MarkDecorator with .mark.kwargs; # in our shim we have a SimpleNamespace from pytest.mark.skip itself. # Easiest: stringify and look for the reason inside. out.append(str(m)) return out def test_tier2_only_skipped_on_tier1(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") item = _FakeItem(keywords={"tier2_only"}) pytest_collection_modifyitems(_FakeConfig(), [item]) assert any("Tier-2 only" in r for r in _skip_reasons(item)) def test_tier2_only_runs_on_tier2(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier2-jetson") item = _FakeItem(keywords={"tier2_only"}) pytest_collection_modifyitems(_FakeConfig(), [item]) assert not item.added_markers, "tier2_only test should run when TIER=tier2-jetson" def test_chamber_only_skipped_without_flag(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier2-jetson") item = _FakeItem(keywords={"chamber_only"}) pytest_collection_modifyitems(_FakeConfig(chamber=False), [item]) assert any("Chamber" in r for r in _skip_reasons(item)) def test_chamber_only_runs_with_flag(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier2-jetson") item = _FakeItem(keywords={"chamber_only"}) pytest_collection_modifyitems(_FakeConfig(chamber=True), [item]) assert not item.added_markers, "chamber_only test should run with --enable-chamber" def test_vins_mono_skipped_on_production(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") callspec = SimpleNamespace(params={"vio_strategy": "vins_mono"}) item = _FakeItem(callspec=callspec) pytest_collection_modifyitems(_FakeConfig(build_kind="production"), [item]) assert any("research-build-only" in r for r in _skip_reasons(item)) def test_vins_mono_runs_on_research(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") callspec = SimpleNamespace(params={"vio_strategy": "vins_mono"}) item = _FakeItem(callspec=callspec) pytest_collection_modifyitems(_FakeConfig(build_kind="research"), [item]) assert not item.added_markers, "vins_mono should run on research builds" def test_deferred_ac_without_reason_blocks_collection(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") marker = _Marker(args=(), kwargs={}) item = _FakeItem(markers={"deferred_ac": marker}) pytest_collection_modifyitems(_FakeConfig(allow_no_reason=False), [item]) assert any("without reason=" in r for r in _skip_reasons(item)) def test_deferred_ac_with_reason_emits_skip(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") marker = _Marker(args=(), kwargs={"reason": "AC-7.1 — see traceability matrix"}) item = _FakeItem(markers={"deferred_ac": marker}) pytest_collection_modifyitems(_FakeConfig(), [item]) assert any("AC-7.1" in r for r in _skip_reasons(item)) def test_deferred_ac_xfail_verdict_emits_xfail(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("TIER", "tier1-docker") marker = _Marker(args=(), kwargs={"reason": "AC-8.6 scene-change PARTIAL", "verdict": "xfail"}) item = _FakeItem(markers={"deferred_ac": marker}) pytest_collection_modifyitems(_FakeConfig(), [item]) # The xfail decorator object stringifies differently from skip; just # verify some marker was added. assert item.added_markers, "deferred_ac(verdict=xfail) must mark the item"