ecu-tests/tests/hardware/mum/swe5/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

265 lines
10 KiB
Python

"""Migrated from SWE5 COM Management Integration Test Plan.
Source: ``25IMR003_ForSeven_RGB-SWITD_03-COM Management (Integration Test results).xlsm``
Translation strategy
--------------------
The COM tests are about LIN communication: NAD addressing, LDF/baudrate,
ALM_Req_A signal layout, LID-range targeting, and frame periodicity.
- ``Watch color table`` (firmware lookup) is exercised end-to-end:
drive a known RGB at full intensity and verify ``PWM_Frame`` matches the
``rgb_to_pwm.compute_pwm`` calculator (which encodes the same color
table).
- NAD: read ``ALM_Status.ALMNadNo`` and confirm it falls inside the
valid NAD range declared by the LDF.
- Baudrate: physical-layer; not measurable from inside the test runner
(requires a scope) → the step is recorded for traceability and skipped.
- ``ALM_Req_A`` byte-mapping: send a frame with distinctive RGB+I values
and confirm the ECU's response (LED reaches ON, PWM matches) — that
proves byte-level interpretation end-to-end.
- LID-range flag: drive a frame inside vs. outside the node's range and
observe whether the LED reacts.
- 5 ms periodicity: a master-side LIN-master scheduling property that
the slave does not echo back; documented as not directly observable.
Marker: ``COM`` — see ``pytest.ini``.
Run only this module:
pytest -m "COM" tests/hardware/swe5/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 alm_helpers import (
AlmTester,
LedState,
STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
)
pytestmark = [pytest.mark.COM]
# Fixtures (fio, alm, _reset_to_off) and the MUM gate come from
# tests/hardware/mum/conftest.py.
# --- tests -----------------------------------------------------------------
def test_com_itd_0001(alm: AlmTester, rp):
"""
Title: LED color table is configured per spec — PWM_Frame matches the calculator
Description:
Verify the SW configures the LED color table as required by
[SWRS_LIN_0001]. The firmware's color table feeds
rgb_to_pwm.compute_pwm(). Drive a known RGB at full intensity, wait
for LED_ON, then assert PWM_Frame matches
compute_pwm(R,G,B,temp_c=Tj_NTC).pwm_comp within tolerance.
Mismatch implies the on-ECU table differs from the spec.
Requirements: SWRS_LIN_0001
Test ID: COM_ITD_0001
"""
r, g, b = 0, 180, 80
# Step: Drive ALM_Req_A mode=0 RGB at full intensity to this NAD
alm.send_color(red=r, green=g, blue=b)
# Step: Wait for ALMLEDState == LED_ON
reached, elapsed, history = alm.wait_for_state(LedState.LED_ON, timeout=STATE_TIMEOUT_DEFAULT)
rp("led_state_history", history)
rp("on_elapsed_s", round(elapsed, 3))
assert reached, f"LED_ON never reached: {history}"
# Step: Assert PWM_Frame matches the rgb_to_pwm calculator (color-table proxy)
alm.assert_pwm_matches_rgb(rp, r, g, b)
def test_com_itd_0002(alm: AlmTester, rp, ldf):
"""
Title: LDF implementation — NAD and baudrate match the LDF
Description:
Verify the SW implements the LDF from "4SEVEN_LDF.ldf" — confirm
the NAD and the bus baudrate.
NAD: read ALMNadNo via ALM_Status; confirm it is a valid LIN slave
NAD (0x01..0xFE) — i.e. matches the value the LDF declares for ALM_Node.
Baudrate: physical-layer property of the LIN bus; verifying it
requires an oscilloscope on the LIN line, so the step is recorded
for traceability but cannot be asserted from inside the test runner.
Requirements: SWRS_LIN_0001, SWRS_LIN_0002, SWRS_LIN_0003
Test ID: COM_ITD_0002
"""
# Step: Read ALM_Status and confirm ALMNadNo is a valid LIN slave NAD
nad = alm.read_nad()
assert nad is not None, "ALM_Status not received within timeout"
rp("alm_nad", nad)
assert 0x01 <= nad <= 0xFE, (
f"ALMNadNo {nad:#x} is outside the valid LIN slave range 0x01..0xFE"
)
# Step: Confirm the LDF declares the same NAD for ALM_Node (introspection)
# The LdfDatabase wrapper exposes the parsed ldf via .ldf in some
# implementations; fall back to attribute access otherwise.
ldf_nad = None
try:
for node in getattr(ldf.ldf, "slaves", []) or []:
# ldfparser slaves carry .name and .configured_nad
if getattr(node, "name", "").lower().startswith("alm"):
ldf_nad = int(getattr(node, "configured_nad", 0))
break
except Exception as e: # pragma: no cover — best effort
rp("ldf_introspection_error", repr(e))
rp("ldf_declared_nad", ldf_nad)
if ldf_nad is not None:
assert nad == ldf_nad, (
f"Runtime NAD {nad:#x} != LDF-declared NAD {ldf_nad:#x}"
)
# Step: Baudrate verification requires an external scope on the LIN bus.
# LIN baudrate is a physical-layer parameter; the master configures
# it from the LDF when opening the interface, but it is not echoed
# back in any frame the slave publishes. Recording for traceability.
rp("baudrate_check", "requires oscilloscope — not asserted in software")
def test_com_itd_0003(alm: AlmTester, rp):
"""
Title: ALM_Req_A interpretation — distinctive RGBI bytes drive the expected PWM
Description:
Verify the SW correctly interprets the bytes of ALM_Req_A
(AmbLightLIDFrom/To, AmbLightColourRed/Green/Blue, AmbLightIntensity).
Per-byte verification at the firmware level (Byte_3 ==
AmbLightColourRed, etc.) is not LIN-observable. Instead, send a frame
with distinctive R/G/B values, addressed to this node's NAD only,
then verify (a) the LED reaches ON (LIDFrom/To were honoured) and
(b) PWM_wo_Comp matches the calculator for those R/G/B at full
intensity (Red/Green/Blue/Intensity bytes were interpreted correctly).
Requirements: SWRS_LIN_0004, SWRS_LIN_0012, SWRS_LIN_0013, SWRS_LIN_0014, SWRS_LIN_0015
Test ID: COM_ITD_0003
"""
# Distinctive values so a swap of two bytes would be detected.
r, g, b, intensity = 0xA0, 0x40, 0x10, 0xFF
# Step: Disable temperature compensation so PWM_wo_Comp == calculator
alm.send_config(enable_compensation=0)
time.sleep(0.2)
try:
# Step: Send ALM_Req_A LIDFrom=LIDTo=alm.nad, RGBI distinctive values
alm.send_color(red=r, green=g, blue=b, intensity=intensity)
# Step: LIDFrom/To honoured — LED reaches ON
reached, elapsed, history = alm.wait_for_state(LedState.LED_ON, timeout=STATE_TIMEOUT_DEFAULT)
rp("led_state_history", history)
rp("on_elapsed_s", round(elapsed, 3))
assert reached, (
f"ECU did not respond to a frame addressed to its own NAD: {history}"
)
# Step: RGB+Intensity bytes correctly interpreted — PWM_wo_Comp matches calculator
alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
finally:
# Step: Restore EnableCompensation=1
alm.send_config(enable_compensation=1)
time.sleep(0.2)
def test_com_itd_0006(alm: AlmTester, rp):
"""
Title: LID range targeting — broadcast hits, out-of-range frame is ignored
Description:
Verify the SW respects the AmbLightLIDFrom/To range — frames whose
range covers this node's NAD are processed; out-of-range frames are
ignored.
Note: the workbook step ``Watch AmbLightLIDFrom/AmbLightLIDTo range
flag`` refers to a firmware-internal flag that is not echoed on the
LIN bus. The LIN-observable proxy is whether the LED reacts (ON) or
stays at OFF.
Requirements: SWRS_LIN_0053
Test ID: COM_ITD_0006
"""
# Step: Broadcast LIDFrom=0x00, LIDTo=0xFF — node is in range, must react
alm.send_color_broadcast(red=120, green=0, blue=255)
reached_on, elapsed, h_in = alm.wait_for_state(LedState.LED_ON, timeout=STATE_TIMEOUT_DEFAULT)
rp("in_range_history", h_in)
rp("in_range_elapsed_s", round(elapsed, 3))
assert reached_on, f"Node ignored an in-range broadcast: {h_in}"
alm.force_off()
# Step: Out-of-range LID (range that excludes this NAD) — must be ignored
# Pick a non-trivial range that intentionally excludes this node.
if alm.nad <= 0x10:
lid_from, lid_to = 0x80, 0xFE
else:
lid_from, lid_to = 0x01, max(0x02, alm.nad - 1)
alm.send_color(
red=255, green=255, blue=255,
lid_from=lid_from, lid_to=lid_to,
)
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("out_of_range_lid", (lid_from, lid_to))
rp("out_of_range_history", history)
assert LedState.LED_ON not in history and LedState.LED_ANIMATING not in history, (
f"Out-of-range LID frame [{lid_from:#x}..{lid_to:#x}] (NAD={alm.nad:#x}) "
f"unexpectedly drove the LED: {history}"
)
def test_com_itd_0001_b(alm: AlmTester, rp):
"""
Title: Input frame reading periodicity (5 ms) — master-side scheduling, scope-only
Description:
Verify the SW respects 5 ms periodicity for reading input frames.
5 ms is the LIN master's schedule cadence: it is configured by the
master (MUM/BabyLin) when the schedule table is loaded from the LDF,
and is not echoed back by the slave. Confirming the actual on-bus
inter-frame gap requires bus-tracing hardware (oscilloscope or LIN
analyzer). This step is recorded for traceability.
Requirements: SWRS_LIN_0001
Test ID: COM_ITD_0001_b
"""
# Step: Document: 5 ms scheduling is master-side and not asserted from the slave
rp("note", (
"5 ms periodicity is set by the LIN master's schedule table "
"(loaded from the LDF). Slave-side timing of inter-frame gaps "
"requires an external LIN bus analyzer or oscilloscope to "
"verify; pytest cannot observe it directly."
))
# Sanity: confirm we can at least round-trip a frame within a few
# schedule periods, which verifies the bus is up at the configured
# baudrate (a coarse sanity check, not a 5 ms timing assertion).
nad = alm.read_nad(timeout=1.0)
assert nad is not None, "ALM_Status not received — bus may be down"
pytest.skip(
"5 ms inter-frame periodicity is master-side / physical-layer; "
"verify with a LIN bus analyzer or oscilloscope, not pytest."
)