ecu-tests/tests/hardware/mum/swe6/test_com_management.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

211 lines
7.9 KiB
Python

"""Migrated from SWE6 COM Management Validation Test Plan.
Source: ``25IMR003_ForSeven-SWVTD_01-COM Management (Validation Test Plan).xlsm``
Translation strategy
--------------------
Both qualification tests in this workbook reference frames and signals
that are **not present in the current production LDF**
(``vendor/4SEVEN_color_lib_test.ldf``):
- ``ALM_NodeSelection`` (per-NAD selection bytes)
- ``ALM_Req_B`` (a second request frame for LED-on/-off commit)
- ``ALM_LED_Idx`` (per-LED bitmask within ``ALM_Req_A``)
The current LDF uses a different addressing scheme: each ALM_Node has a
single LED, addressed by the ``AmbLightLIDFrom``/``AmbLightLIDTo`` range
inside ``ALM_Req_A``. Until the LDF is updated to expose the workbook's
signals (or the workbook is updated to match the deployed LDF), these
tests cannot be executed end-to-end.
Each test below performs a **real LDF probe** for the missing signals.
If the LDF later starts exposing them, the probe stops skipping and the
remaining steps execute against the real bus. The skip reason names the
exact missing signal so a reviewer can see what's blocking.
Marker: ``COM_VTD`` — see ``pytest.ini``.
Run only this module:
pytest -m "COM_VTD" tests/hardware/swe6/test_com_management.py
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import pytest
_HW_DIR = Path(__file__).resolve().parent.parent
if str(_HW_DIR) not in sys.path:
sys.path.insert(0, str(_HW_DIR))
from frame_io import FrameIO
from alm_helpers import (
AlmTester,
LED_STATE_ON,
STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
)
pytestmark = [pytest.mark.COM_VTD]
# Fixtures (fio, alm, _reset_to_off) and the MUM gate come from
# tests/hardware/mum/conftest.py.
def _require_signals_in_frame(fio: FrameIO, frame_name: str, signal_names: list[str]) -> None:
"""Skip if the LDF doesn't define ``frame_name`` with all ``signal_names``.
Lets the test become live automatically when the LDF is updated.
"""
try:
f = fio.frame(frame_name)
except Exception as e:
pytest.skip(f"LDF does not define frame {frame_name!r}: {e!r}")
return
declared = {s.name for s in getattr(f, "signals", []) or []}
missing = [s for s in signal_names if s not in declared]
if missing:
pytest.skip(
f"LDF frame {frame_name!r} is missing signal(s) {missing!r}; "
f"this validation test cannot run against the current LDF."
)
# --- tests -----------------------------------------------------------------
def test_com_vtd_0001(fio: FrameIO, alm: AlmTester, rp):
"""
Title: SW responds only when its NAD bit is set in ALM_NodeSelection
Description:
Verify the SW responds only when the current NAD's bit is set in
the ``ALM_NodeSelection`` bytes; the LED-on/-off command is then
committed by a follow-up ``ALM_Req_B`` frame.
Steps from workbook:
1. Send ALM_Req_A with current NAD bit = 1 + LED_0 ON parameters
2. Send ALM_Req_B → expect LED_0 ON
3. Send ALM_Req_A with current NAD bit = 0 + LED_0 OFF parameters
4. Send ALM_Req_B → expect LED_0 still ON (command was not addressed)
Status: ``ALM_NodeSelection`` and ``ALM_Req_B`` are NOT in the
current production LDF (``vendor/4SEVEN_color_lib_test.ldf``);
addressing is currently done via ``AmbLightLIDFrom``/
``AmbLightLIDTo`` inside ``ALM_Req_A``. The probe below names the
exact missing artifact and the test becomes live automatically
when the LDF is updated.
Requirements: SWRS_LIN_0008
Test ID: COM_VTD_0001
"""
# Step: Probe LDF for the validation-spec frames/signals.
# If/when the LDF is updated to expose these, the skip below
# disappears and the remaining steps will execute.
_require_signals_in_frame(fio, "ALM_Req_A", ["ALM_NodeSelection"])
_require_signals_in_frame(fio, "ALM_Req_B", []) # frame existence
# The steps below are ready to run as soon as the LDF exposes the
# missing signals. They mirror the workbook procedure.
# Step 1: ALM_Req_A with this NAD selected + LED_0 ON parameters
fio.send(
"ALM_Req_A",
ALM_NodeSelection=(1 << (alm.nad - 1)), # bitmask for this NAD
ALM_LED_Idx=0x01, # LED_0
AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
)
# Step 2: ALM_Req_B commits the command — expect LED_0 ON
fio.send("ALM_Req_B")
reached, _, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
rp("on_history", history)
assert reached, f"LED_0 did not turn ON after ALM_Req_B commit: {history}"
# Step 3: ALM_Req_A with this NAD NOT selected + LED_0 OFF parameters
fio.send(
"ALM_Req_A",
ALM_NodeSelection=0,
ALM_LED_Idx=0x01,
AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=0,
AmbLightIntensity=0,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
)
# Step 4: ALM_Req_B — LED_0 must remain ON (un-addressed command)
fio.send("ALM_Req_B")
deadline = time.monotonic() + 1.0
history = []
while time.monotonic() < deadline:
st = alm.read_led_state()
if not history or history[-1] != st:
history.append(st)
time.sleep(STATE_POLL_INTERVAL)
rp("post_history", history)
assert LED_STATE_ON in history, (
f"LED_0 turned off — un-addressed command was wrongly applied: {history}"
)
@pytest.mark.parametrize(
"led_idx,description",
[
pytest.param(0x00, "all OFF", id="led_idx_0x00"),
pytest.param(0xAA, "LEDs 1,3,5,7 ON; 0,2,4,6 OFF", id="led_idx_0xAA"),
pytest.param(0x55, "LEDs 0,2,4,6 ON; 1,3,5,7 OFF", id="led_idx_0x55"),
pytest.param(0xFF, "all ON", id="led_idx_0xFF"),
],
)
def test_com_vtd_0002(fio: FrameIO, alm: AlmTester, rp, led_idx, description):
"""
Title: SW interprets ALM_LED_Idx as a per-LED bitmask
Description:
Verify ``ALM_LED_Idx`` is interpreted as a bitmask, one bit per LED:
- 0x00 → all LEDs OFF
- 0xAA → LEDs 1,3,5,7 ON, LEDs 0,2,4,6 OFF
- 0x55 → LEDs 0,2,4,6 ON, LEDs 1,3,5,7 OFF
- 0xFF → all LEDs ON
Status: ``ALM_LED_Idx`` is NOT in the current production LDF; the
deployed ECU exposes a single LED via ``AmbLightLIDFrom/To``.
Per-LED verification additionally requires either an extended
ALM_Status frame or external optical instrumentation — individual
ON/OFF states of 8 LEDs are not LIN-observable from the current
ALM_Status payload.
The probe below skips the test naming the exact missing signal so
the test becomes live automatically when the LDF is updated.
Requirements: SWRS_LIN_0009, SWRS_LIN_0010, SWRS_LIN_0011
Test ID: COM_VTD_0002
"""
# Step: Probe LDF for ALM_LED_Idx and ALM_NodeSelection
_require_signals_in_frame(fio, "ALM_Req_A", ["ALM_LED_Idx", "ALM_NodeSelection"])
# Step: Send ALM_Req_A with this NAD selected, ALM_LED_Idx=<bitmask>
fio.send(
"ALM_Req_A",
ALM_NodeSelection=(1 << (alm.nad - 1)),
ALM_LED_Idx=led_idx,
AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
AmbLightIntensity=255,
AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
)
# Step: Verify per-LED ON/OFF pattern matches mask
rp("led_idx_mask", f"0x{led_idx:02X}")
rp("expected_pattern", description)
# When the LDF is extended with a per-LED status frame, replace
# this skip with an actual signal read + bit-by-bit assertion.
pytest.skip(
"Per-LED state is not exposed in the current ALM_Status frame; "
"individual LED verification requires either an extended status "
"frame or external optical instrumentation."
)