"""Shared fixtures for the MUM hardware test suite. WHY THIS FILE EXISTS -------------------- Every test under ``tests/hardware/mum/**`` needs the same three things: 1. The session to be a MUM session (``config.interface.type == "mum"``). 2. A live ``FrameIO`` bound to the session ``lin`` + ``ldf``. 3. The ECU's live NAD, discovered by reading ``ALM_Status``. Before this conftest existed, each test module repeated those fixtures verbatim — 9 copies of ``fio``, 8 of ``alm``, 8 of ``_reset_to_off``, and 8 inline ``if config.interface.type != "mum": pytest.skip(...)`` gates. They are all consolidated here. SCOPE STRATEGY -------------- ``FrameIO``, ``AlmTester``, and the discovered ``nad`` are immutable relative to a session connection. Keeping them at ``scope="session"`` means one NAD discovery per run instead of one per module, and a single shared cache of LDF frame lookups across the whole suite. The only function-scoped fixture is ``_reset_to_off`` — it MUST be per-test so each test starts with the LED in a known state. ACCESS CONTROL -------------- This conftest is at ``tests/hardware/mum/`` deliberately: tests under ``tests/hardware/psu/`` and ``tests/hardware/babylin/`` cannot see ``fio``/``alm``/``nad`` because pytest only walks **upward** through ``conftest.py`` files. A PSU-only test that accidentally requests ``fio`` will fail at collection with "fixture not found" — that is the access-control mechanism. OVERRIDE NOTES -------------- Two files override fixtures here for documented reasons: - ``test_mum_alm_animation_generated.py`` keeps a local ``_reset_to_off`` + ``_force_off`` so its "no AlmTester anywhere" demonstration stays true. The local ``_reset_to_off`` shadows this conftest's. - ``test_overvolt.py`` defines its own ``_reset_to_off`` that ALSO parks the PSU at the nominal voltage. Its override is necessary — without it, both autouse fixtures would run and the LED would be toggled twice per test (harmless but wasteful). """ from __future__ import annotations import pytest from ecu_framework.config import EcuTestConfig from ecu_framework.lin.base import LinInterface from frame_io import FrameIO from alm_helpers import AlmTester # --------------------------------------------------------------------------- # Session-wide gate # --------------------------------------------------------------------------- @pytest.fixture(scope="session", autouse=True) def _require_mum(config: EcuTestConfig) -> None: """Single skip point for the whole MUM suite. Replaces the inline ``if config.interface.type != "mum": pytest.skip(...)`` that used to live inside every ``fio`` fixture. ``autouse=True`` means every test under ``tests/hardware/mum/**`` honors this without having to opt in. """ if config.interface.type != "mum": pytest.skip("interface.type must be 'mum' for tests under tests/hardware/mum/") # --------------------------------------------------------------------------- # Shared MUM-suite fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def fio(lin: LinInterface, ldf) -> FrameIO: """LDF-driven I/O over the session LIN connection. Session-scoped because ``FrameIO`` only holds ``(lin, ldf)`` and caches frame lookups — sharing it across the whole suite is a feature, not a risk. Tests that need a fresh cache can build their own ``FrameIO(lin, ldf)`` inside the test body. """ return FrameIO(lin, ldf) @pytest.fixture(scope="session") def nad(fio: FrameIO) -> int: """Live NAD reported by the ECU's ALM_Status frame. Used as ``LIDFrom`` / ``LIDTo`` in unicast sends and as the slave address bound into ``AlmTester``. Discovered once per session because the address doesn't change while the ECU is powered. Skips cleanly when: - The ECU isn't responding (no ``ALM_Status`` within 1 s) — likely a wiring or power problem. - The reported NAD is outside the valid 0x01-0xFE range — usually means auto-addressing hasn't been performed yet. """ decoded = fio.receive("ALM_Status", timeout=1.0) if decoded is None: pytest.skip("ECU not responding on ALM_Status — check wiring/power") n = int(decoded["ALMNadNo"]) if not (0x01 <= n <= 0xFE): pytest.skip(f"ECU reports invalid NAD {n:#x} — auto-addressing first") return n @pytest.fixture(scope="session") def alm(fio: FrameIO, nad: int) -> AlmTester: """ALM_Node domain helper bound to the live NAD. Session-scoped because ``AlmTester`` is stateless beyond ``(fio, nad)``; per-test state hygiene is handled by ``_reset_to_off`` below, not by rebuilding the helper. """ return AlmTester(fio, nad) # --------------------------------------------------------------------------- # Per-test state reset # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def _reset_to_off(alm: AlmTester): """Drive the LED to OFF before AND after every test. Function-scoped + autouse so that state cannot leak between tests. The post-test ``force_off()`` runs even when the test body fails — that is the contract: regardless of how the test exits, the next one starts on a known baseline. Override this fixture locally in a test module to change the reset semantics (see the OVERRIDE NOTES in the module docstring). """ alm.force_off() yield alm.force_off()