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>
316 lines
14 KiB
Markdown
316 lines
14 KiB
Markdown
# 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 0x00–0x3F) 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).
|