ecu-tests/tests/hardware/mum/conftest.py
Hosam-Eldin Mostafa 8fa4cf0be1 refactor(tests): layer fixtures by adapter type (mum/psu/babylin)
Restructures tests/hardware/ so that fixture access is controlled by
directory layout — pytest only walks upward through conftest.py files,
so a PSU test physically cannot request fio/alm/nad.

Layout:
- tests/hardware/conftest.py           (unchanged: PSU fixtures)
- tests/hardware/mum/conftest.py       NEW: _require_mum (session autouse),
                                       fio (session), nad (session),
                                       alm (session), _reset_to_off
                                       (function autouse)
- tests/hardware/mum/**                MUM tests + swe5/ + swe6/
- tests/hardware/psu/**                PSU-only tests
- tests/hardware/babylin/**            deprecated BabyLIN E2E

What this removes (was duplicated before):
- 7 verbatim copies of the `fio` fixture
- 6 copies of the `alm` fixture
- 6 copies of the `_reset_to_off` autouse
- 9 inline `if config.interface.type != "mum": pytest.skip(...)` gates

What this changes by design:
- fio / alm / nad scope: module → session. NAD discovery happens once
  per run instead of once per module. The helpers are immutable beyond
  their constructor args, so sharing them is safe; per-test state is
  reset by the autouse `_reset_to_off`.
- test_overvolt.py: `_park_at_nominal` is now `_reset_to_off`, which
  cleanly overrides the conftest's LED-only version (PSU + LED reset).
- test_mum_alm_animation_generated.py keeps a local `_reset_to_off` +
  `_force_off` so its "no AlmTester anywhere" demonstration is preserved
  via fixture override; the local `nad` is also retained because it
  uses the typed `AlmStatus.receive` API.

Docs:
- docs/24_test_wiring.md NEW — describes the three-layer fixture
  topology, lifecycle sequence diagram, helper class wiring, and the
  playbook for adding a new framework component.
- docs/05_architecture_overview.md: add MCF (mum conftest) node to the
  Mermaid diagram + mention it in the components list.
- docs/19_frame_io_and_alm_helpers.md: replace the per-module
  fixture-wiring example with a request-fixtures-by-name snippet plus
  the override pattern.
- Path references swept across docs/02, docs/14, docs/18, docs/20,
  docs/README to point at the new locations.

Verified: pytest --collect-only collects 93 tests with no errors;
30 unit tests and 10 mock-only smoke tests pass; fixture-per-test
output shows PSU tests cannot see fio/alm/nad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:43:09 +02:00

144 lines
5.4 KiB
Python

"""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()