ecu-tests/tests/hardware/mum/swe6/test_com_management.py
Hosam-Eldin Mostafa 08247f9321 refactor(tests): AlmTester as the single contributor-facing API
Extends ``tests/hardware/alm_helpers.py`` into the full surface that
hardware tests use, so contributors write intent (``alm.send_color``,
``alm.read_led_state``, ``alm.wait_for_led_on``) and never touch
``fio.send("ALM_Req_A", AmbLight…=…)`` or LDF schema details.

What landed:

- AlmTester gains ~16 methods:
    read_nad, read_voltage_status, read_thermal_status, read_nvm_status,
    read_sig_comm_err, read_ntc_kelvin, read_ntc_celsius, read_pwm,
    read_pwm_wo_comp, send_color, send_color_broadcast, save_color,
    apply_saved_color, discard_saved_color, send_config, plus
    wait_for_led_on / wait_for_led_off / wait_for_animating wrappers.
- The six IntEnum classes that ALM tests need (LedState, Mode, Update,
  NVMStatus, VoltageStatus, ThermalStatus) are defined directly in
  alm_helpers.py — tests get them via `from alm_helpers import …`.
- All ALM test files migrated:
    test_mum_alm_animation.py, test_mum_alm_cases.py, test_overvolt.py,
    swe5/test_anm_management.py, swe5/test_com_management.py
    each now go through AlmTester for every common pattern.
- swe6/test_com_management.py: stays on `fio` (these tests probe
  schema features not in the current production LDF and skip when
  the LDF doesn't declare them) — change limited to LedState enum.
- test_mum_alm_animation_generated.py deleted — its "no-AlmTester"
  demonstration loses its point now that AlmTester is the
  recommended path.
- docs/19_frame_io_and_alm_helpers.md reframed: AlmTester is the
  contributor surface; FrameIO is implementation detail. New API
  reference + Cookbook examples + a note that the maintenance pact
  is "LDF changes → AlmTester updates".

Verified: pytest --collect-only collects 87 tests cleanly; 40 unit
+ mock smoke tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:23:52 +02:00

217 lines
8.3 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,
LedState,
STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
)
# These validation tests deliberately stay on `fio` (not the AlmTester
# facade): they exercise frames and signals (``ALM_Req_B``,
# ``ALM_NodeSelection``, ``ALM_LED_Idx``) that are NOT in the current
# production LDF — the AlmTester surface doesn't model them by design.
# The tests skip when the LDF doesn't expose those signals.
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(LedState.LED_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 LedState.LED_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."
)