# 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/loader.py` — YAML → dataclasses (re-exported via `ecu_framework.config`)
- `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).
