ecu-tests/docs/01_run_sequence.md
Hosam-Eldin Mostafa 73b1338361 docs/01: bring the run-sequence walkthrough up to date
The previous version described the pre-refactor flow only — no
hardware-suite conftest, no helper layer, no PSU resolver, no
settle-then-validate pattern, no junit_family note. Rewritten so it
reflects the current architecture without losing the original
sequence-diagram + text-flow shape.

What's new in the doc:
- Two-layer fixture model (project-wide vs hardware-suite) called
  out at the top.
- Mermaid sequence diagram now shows the session-scoped autouse PSU
  power-up, the helper layer (FrameIO / AlmTester / psu_helpers),
  and the safe-off-on-close at session teardown.
- Text-flow split into PROJECT-WIDE / HARDWARE-SUITE / TEST-BODIES
  sections; describes resolve_port's fallback chain and the
  settle-then-validate behaviour of apply_voltage_and_settle.
- "Where information is fetched from" gains the LDF, rgb_to_pwm,
  and per-machine PSU override paths.
- "Key components" split into project-wide / hardware-suite, listing
  every helper and template file.
- Edge cases gain PSU-side entries: cross-platform port resolution,
  the must-not list (no set_output(False), no close()),
  apply_voltage_and_settle's timeout behaviour, and the
  junit_family=legacy requirement for record_property round-trips.

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

316 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Run Sequence: What Happens When You Start Tests
This document walks through the exact order of operations when you run
the framework with pytest, what gets called, and where
configuration / data is fetched from. The flow has two layers:
- **Project-wide** fixtures in `tests/conftest.py``config`, `lin`,
`ldf`, `flash_ecu`, `rp`. Apply to every test.
- **Hardware-suite** fixtures in `tests/hardware/conftest.py` — a
session-scoped, autouse PSU power-up plus the public `psu` fixture.
Apply only to tests under `tests/hardware/`.
## High-level flow
1. You run pytest from PowerShell (or any shell).
2. pytest reads `pytest.ini` (markers, addopts, `junit_family=legacy`)
and loads `conftest_plugin` for HTML/metadata enrichment.
3. Test discovery collects tests under `tests/`.
4. **Project-wide** session fixtures resolve as needed:
- `config()` loads YAML configuration into typed dataclasses.
- `lin()` selects and connects the LIN adapter (Mock / MUM /
deprecated BabyLIN).
- `ldf()` loads the LDF database (when `interface.ldf_path` is set).
- `flash_ecu()` optionally flashes the ECU.
5. **For hardware tests only**, the session-scoped autouse fixture
`_psu_powers_bench` realizes `_psu_or_none`, which:
- Opens the Owon PSU once via the cross-platform `resolve_port()`,
- Parks it at `config.power_supply.set_voltage` /
`set_current`,
- Enables output and leaves it on for the entire session.
This keeps the ECU powered for every test in the suite — even
tests that don't request `psu` by name.
6. Per-test bodies execute — typically through the helper layer
(`FrameIO`, `AlmTester`, `apply_voltage_and_settle`) rather than
raw `lin.send()` / `lin.receive()`.
7. The `conftest_plugin` parses each test's docstring (Title /
Description / Requirements / Steps / Expected Result) and attaches
the values as JUnit `<property>` entries.
8. At session end the PSU's `safe_off_on_close` sends `output 0`
before releasing the port; reports are written.
## Detailed call sequence (Mermaid)
```mermaid
sequenceDiagram
autonumber
participant U as User (shell)
participant P as pytest
participant PI as pytest.ini
participant PL as conftest_plugin.py
participant T as Test discovery (tests/*)
participant CF as tests/conftest.py
participant HCF as tests/hardware/conftest.py
participant C as Config Loader
participant L as LIN Adapter (mock/MUM/BabyLIN)
participant LD as LDF Database
participant PSU as PSU (session-scoped)
participant FH as Helper layer (FrameIO/AlmTester/psu_helpers)
participant X as HexFlasher (optional)
participant R as Reports (HTML/JUnit/Summary)
U->>P: python -m pytest [args]
P->>PI: addopts, markers, junit_family=legacy
P->>PL: Register custom plugin hooks
P->>T: Collect tests
rect rgba(200,220,255,0.25)
Note over CF: Project-wide fixtures (every test)
P->>CF: Resolve session fixtures
CF->>C: load_config(workspace_root)
C-->>CF: EcuTestConfig
CF->>L: Create interface (per interface.type)
CF->>L: lin.connect()
L-->>CF: ready (MUM also powers ECU via power_out0)
opt interface.ldf_path set
CF->>LD: LdfDatabase(ldf_path)
end
end
rect rgba(220,255,220,0.30)
Note over HCF: Hardware-suite fixtures (tests/hardware/*)
P->>HCF: Realize _psu_powers_bench (autouse, session)
HCF->>PSU: resolve_port + open + set V/I + output ON
PSU-->>HCF: powered, leave on for session
end
alt flash.enabled and hex_path provided
CF->>X: HexFlasher(lin).flash_hex(hex_path)
X-->>CF: ok / fail
end
loop for each test (function scope)
P->>FH: Test body uses FrameIO.send/receive/read_signal,
Note over FH: AlmTester.* and/or apply_voltage_and_settle
FH->>L: send() / receive()
L-->>FH: Frames / None
FH->>PSU: set_voltage() + measure_voltage_v() (voltage tests)
PSU-->>FH: settled value
P->>PL: pytest_runtest_makereport(item, call)
Note over PL: Parse docstring → user_properties
end
P->>R: HTML report with metadata columns
P->>R: JUnit XML (junit_family=legacy → record_property entries)
P->>R: summary.md, requirements_coverage.json
rect rgba(255,220,220,0.30)
Note over PSU: Session teardown
HCF->>PSU: close() → safe_off_on_close sends 'output 0'
end
```
## Text flow (project-wide + hardware-suite layers)
```text
shell → python -m pytest
pytest loads pytest.ini
- addopts: -ra --junitxml=… --html=… --tb=short --cov=…
- junit_family = legacy ← required for record_property() round-trip
- markers registered (hardware, mum, babylin (deprecated),
slow, psu_settling, smoke, …)
pytest collects tests in tests/
─────────────────────────────────────────────────────────────────────
PROJECT-WIDE fixtures (tests/conftest.py) — apply to every test
─────────────────────────────────────────────────────────────────────
Session: config()
→ ecu_framework.config.load_config(workspace_root)
→ precedence: in-memory overrides > ECU_TESTS_CONFIG env >
./config/test_config.yaml > defaults
→ optionally merges config/owon_psu.yaml (or OWON_PSU_CONFIG)
into power_supply
→ returns EcuTestConfig (typed dataclasses)
Session: lin(config)
→ chooses adapter by interface.type:
mock → ecu_framework.lin.mock.MockBabyLinInterface(...)
mum → ecu_framework.lin.mum.MumLinInterface(host, lin_device,
power_device, …)
babylin → ecu_framework.lin.babylin.BabyLinInterface(...) [DEPRECATED]
→ lin.connect()
- MUM also powers the ECU via power_out0 and waits
boot_settle_seconds before sending the first frame
Session: ldf(config)
→ if interface.ldf_path set: LdfDatabase(ldf_path)
→ else: skip (tests requesting `ldf` are skipped with a clear msg)
Session (opt): flash_ecu(config, lin)
→ if flash.enabled and flash.hex_path set
→ HexFlasher(lin).flash_hex(hex_path)
Function: rp(record_property)
→ convenience wrapper that records both as a JUnit property AND
echoes to captured stdout for fast diagnosis
─────────────────────────────────────────────────────────────────────
HARDWARE-SUITE fixtures (tests/hardware/conftest.py) — only for
tests under tests/hardware/
─────────────────────────────────────────────────────────────────────
Session: _psu_or_none(config)
→ if power_supply disabled / port unset / unreachable:
yield None (tolerant; does not raise)
→ else:
params = SerialParams.from_config(power_supply)
port = resolve_port(power_supply.port,
idn_substr=power_supply.idn_substr,
params=params)
↑ tries the configured port verbatim, then its
cross-platform translation (COM7 ↔ /dev/ttyS6 on WSL1),
then /dev/ttyUSB* / /dev/ttyACM* on Linux/WSL,
then a full scan_ports() with optional idn_substr filter
psu = OwonPSU(port, params, eol, safe_off_on_close=True)
psu.open()
psu.set_voltage(set_voltage)
psu.set_current(set_current)
psu.set_output(True) ← bench powered ON for session
yield psu
psu.close() ← end of session: 'output 0' first
Session, autouse: _psu_powers_bench(_psu_or_none)
→ realizes _psu_or_none so the bench is powered up even for
tests that don't request `psu` by name (no-op if PSU absent)
Session: psu(_psu_or_none)
→ public alias: skip cleanly if PSU is unavailable
─────────────────────────────────────────────────────────────────────
TEST BODIES — typically use the helper layer, not lin/psu directly
─────────────────────────────────────────────────────────────────────
Hardware-test helpers (sibling-imported from tests/hardware/):
frame_io.FrameIO(lin, ldf)
high : send / receive / read_signal (by frame and signal name)
mid : pack / unpack (bytes ↔ signals)
low : send_raw / receive_raw (bypass LDF entirely)
intro : frame, frame_id, frame_length
alm_helpers.AlmTester(fio, nad)
force_off, read_led_state, wait_for_state,
measure_animating_window,
assert_pwm_matches_rgb, ← uses vendor/rgb_to_pwm.py
assert_pwm_wo_comp_matches_rgb
psu_helpers
wait_until_settled(psu, target_v, *, tol, interval, timeout)
apply_voltage_and_settle(psu, target_v, *, validation_time, …)
1. psu.set_voltage(target_v)
2. poll measure_voltage_v() until within tol (or raise)
3. sleep validation_time so the firmware-side observer
can detect and republish status
→ returns {settled_s, validation_s, final_v, trace}
Per-file autouse fixtures often layer on a domain baseline:
test_mum_alm_animation.py:_reset_to_off → alm.force_off()
test_overvolt.py:_park_at_nominal → apply_voltage_and_settle(NOMINAL_V) + force_off
_test_case_template_psu_lin.py:_park_at_nominal → same pattern
Reporting plugin (conftest_plugin.py)
→ pytest_runtest_makereport parses docstring (Title / Description /
Requirements / Test Steps / Expected Result)
→ attaches user_properties; pytest-html shows Title + Requirements
columns; junit_family=legacy ensures record_property() round-trips
Reports written
→ reports/report.html (HTML with metadata columns)
→ reports/junit.xml (JUnit XML — properties round-trip)
→ reports/summary.md (machine-friendly run summary)
→ reports/requirements_coverage.json
```
## Where information is fetched from
- pytest configuration: `pytest.ini` (markers, addopts,
`junit_family = legacy`)
- YAML config (default): `config/test_config.yaml`
- YAML override via env var: `ECU_TESTS_CONFIG`
- Per-machine PSU override: `config/owon_psu.yaml` (or
`OWON_PSU_CONFIG`); merged into `power_supply`
- LDF database: `interface.ldf_path` (typically
`vendor/4SEVEN_color_lib_test.ldf`); consumed by the `ldf` fixture
and by `FrameIO`
- RGB→PWM calculator: `vendor/rgb_to_pwm.py`; consumed by
`AlmTester.assert_pwm_*`
- BabyLIN SDF / schedule (DEPRECATED): `interface.sdf_path` and
`interface.schedule_nr`
- Test metadata: parsed from each test's docstring
- Markers: declared in `pytest.ini`, attached in tests via
`@pytest.mark.*` (file-level via `pytestmark = [...]`)
## Key components involved
### Project-wide
- `tests/conftest.py` — defines `config`, `lin`, `ldf`, `flash_ecu`, `rp`
- `conftest_plugin.py` — report customization and metadata extraction
- `ecu_framework/config.py` — YAML → dataclasses
- `ecu_framework/lin/{base,mock,mum,ldf,babylin}.py` — LIN abstraction
and adapters
- `ecu_framework/flashing/hex_flasher.py` — flashing scaffold
- `ecu_framework/power/owon_psu.py` — PSU controller +
`resolve_port()`
### Hardware-suite (`tests/hardware/`)
- `conftest.py` — session-scoped autouse PSU fixture (powers the ECU
for the entire session)
- `frame_io.py``FrameIO` class (generic LDF-driven I/O)
- `alm_helpers.py``AlmTester` class + ALM constants and tolerance
utilities
- `psu_helpers.py``wait_until_settled` /
`apply_voltage_and_settle` (settle-then-validate pattern)
- `_test_case_template.py`, `_test_case_template_psu_lin.py`
copyable starting points (leading underscore → not collected)
## Edge cases and behaviour
### LIN side
- `interface.type == 'babylin'` (deprecated): if the SDK wrapper or
libraries can't load, hardware tests skip cleanly.
- `interface.type == 'mum'`: if `pylin` / `pymumclient` aren't
importable, or `interface.host` is unset, hardware tests skip.
- MUM `receive()` is master-driven: it requires a frame ID;
`receive(id=None)` raises `NotImplementedError`. Diagnostic frames
needing LIN 1.x Classic checksum must go through
`MumLinInterface.send_raw()`.
- `flash.enabled == true` but `hex_path` missing → flashing fixture
skips.
- Invalid frame IDs (outside 0x000x3F) or data > 8 bytes raise in
`LinFrame`.
### PSU / hardware side
- PSU port resolution: if the configured port can't be opened on this
host (e.g. `COM7` on Linux), `resolve_port()` falls back to its
cross-platform translation (`COM7``/dev/ttyS6` on WSL1), then
`/dev/ttyUSB*` / `/dev/ttyACM*`, then a full scan filtered by
`idn_substr`. Returns `None` if nothing responds — the `psu`
fixture then skips cleanly.
- The session-scoped PSU fixture **must not** be closed mid-session.
Tests that perturb voltage **must restore nominal in `finally`**;
they **must not** call `psu.set_output(False)` (would brown out the
ECU and break every later test).
- `apply_voltage_and_settle()` raises `AssertionError` if the rail
doesn't reach the target within `settle_timeout` (default 10 s) —
surfacing real bench problems rather than letting voltage tests
silently assert against the wrong rail.
- `record_property()` / `rp(...)` entries only appear in
`reports/junit.xml` when `junit_family = legacy` is set in
`pytest.ini` (default `xunit2` silently drops them with a
collect-time warning).