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

14 KiB
Raw Blame History

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.pyconfig, 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)

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)

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.pyFrameIO class (generic LDF-driven I/O)
  • alm_helpers.pyAlmTester class + ALM constants and tolerance utilities
  • psu_helpers.pywait_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).