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>
211 lines
7.9 KiB
Python
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."
|
|
)
|