ecu-tests/tests/hardware/conftest.py
Hosam-Eldin Mostafa eac662b139 tests/hardware: session-scoped PSU fixture so the bench stays powered
On benches where the Owon PSU powers the ECU, every per-file PSU
fixture that closed the port (sending 'output 0' on close) browned
out the bench between modules — every MUM test that ran after a
closed PSU connection failed with "ECU not responding".

New tests/hardware/conftest.py provides three session-scoped
fixtures:

- _psu_or_none: tolerant. Opens the Owon PSU once via resolve_port,
  parks at config.power_supply.set_voltage / set_current, enables
  output. Yields the live OwonPSU or None. Closes (with
  safe_off_on_close=True) at session end — the bench ends safely
  de-energized.

- _psu_powers_bench: autouse=True. Realizes _psu_or_none so even
  tests that don't request `psu` by name benefit from the
  session-level power-up. No-op if PSU isn't configured.

- psu: public. Skips cleanly when the PSU isn't reachable.

Contract for tests:
  - request `psu` if you need to read measurements or change voltage
  - restore nominal voltage in your finally block
  - MUST NOT call psu.set_output(False) (would brown out the bench)
  - MUST NOT call psu.close() (the session fixture owns it)

test_owon_psu.py becomes read-only:
  - removed the local module-scoped psu fixture
  - removed the set_output toggle (would have killed the session)
  - now validates IDN, output_is_on(), and parsed measurements
    against the always-on PSU. Renamed to
    test_owon_psu_idn_and_measurements to reflect the new shape.

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

155 lines
5.8 KiB
Python

"""Session-scoped fixtures for the hardware test suite.
WHY THIS FILE EXISTS
--------------------
On this bench the Owon PSU **powers the ECU** — the MUM only carries
LIN traffic. So the PSU output must stay on for the **entire** test
session, not just for the duration of an individual PSU test. If
each test file opened/closed its own PSU connection (which by
default sends ``output 0`` on close) the bench would brown out
between modules and every subsequent MUM test would fail.
WHAT THIS FILE PROVIDES
-----------------------
- ``_psu_or_none`` : session-scoped, tolerant. Opens the PSU
once at session start, parks it at the
configured nominal voltage, enables output,
and leaves it that way for the whole
session. Yields the live ``OwonPSU`` or
``None`` if the PSU isn't reachable.
- ``_psu_powers_bench`` : session-scoped, ``autouse=True``. Realizes
``_psu_or_none`` so even tests that don't
request the PSU by name benefit from the
power-up. No-op when PSU isn't configured.
- ``psu`` : session-scoped, public. Tests that read
measurements or perturb voltage request
this fixture; it skips cleanly when the
PSU isn't available.
CONTRACT FOR TESTS
------------------
Tests SHOULD:
- request ``psu`` if they need to read measurements or change voltage
- restore the bench to nominal voltage in their ``finally`` block
(the session fixture will not restore between tests)
Tests MUST NOT:
- call ``psu.set_output(False)`` — this kills the ECU power for
every test that follows in the same session
- call ``psu.close()`` — the session fixture owns the lifecycle
"""
from __future__ import annotations
import time
import pytest
from serial import SerialException
from ecu_framework.config import EcuTestConfig
from ecu_framework.power import OwonPSU, SerialParams, resolve_port
# ── nominal supply settings used at session start ─────────────────────────
# Sourced from ``config.power_supply.set_voltage`` / ``set_current`` so the
# bench operator controls them via YAML rather than via Python edits.
# These constants are fallbacks if the YAML omits them.
_FALLBACK_NOMINAL_VOLTAGE = 13.0 # V
_FALLBACK_NOMINAL_CURRENT = 1.0 # A
_PSU_PARK_SETTLE_SECONDS = 0.5 # let the rails stabilize before tests run
@pytest.fixture(scope="session")
def _psu_or_none(config: EcuTestConfig):
"""Open the Owon PSU once per session, park at nominal, leave output ON.
Returns the live :class:`OwonPSU` instance, or ``None`` if the PSU
isn't enabled / configured / reachable. Always yields exactly once
(no exceptions propagate out of this fixture for the unavailable
cases) so tests that don't request it directly can proceed.
The session-end teardown closes the port; with
``safe_off_on_close=True`` that also sends ``output 0`` — the
session ends with the bench safely de-energized.
"""
cfg = config.power_supply
if not cfg.enabled or not cfg.port:
# PSU not configured. Yield None so the autouse fixture and
# the public ``psu`` fixture can both decide what to do.
yield None
return
params = SerialParams.from_config(cfg)
resolved = resolve_port(cfg.port, idn_substr=cfg.idn_substr, params=params)
if resolved is None:
# Configured but not reachable. Treat the same as not present —
# tests that need it will skip via the public ``psu`` fixture.
yield None
return
port, idn = resolved
p = OwonPSU(
port=port,
params=params,
eol=cfg.eol or "\n",
safe_off_on_close=True, # session-end safety net
)
try:
p.open()
except SerialException:
# Race: another process grabbed the port between resolve and
# open. Tests that need the PSU will skip cleanly.
yield None
return
# Park at the configured nominal supply and enable output. Stays
# this way for the whole session unless individual tests perturb
# it (and restore in finally).
nominal_v = float(cfg.set_voltage) if cfg.set_voltage else _FALLBACK_NOMINAL_VOLTAGE
nominal_i = float(cfg.set_current) if cfg.set_current else _FALLBACK_NOMINAL_CURRENT
p.set_voltage(1, nominal_v)
p.set_current(1, nominal_i)
p.set_output(True)
time.sleep(_PSU_PARK_SETTLE_SECONDS)
print(
f"\n[psu] session power on: port={port!r} idn={idn!r} "
f"V={nominal_v} I_lim={nominal_i}"
)
try:
yield p
finally:
# Session-end: close() sends ``output 0`` first because of
# safe_off_on_close=True, then releases the port.
p.close()
@pytest.fixture(scope="session", autouse=True)
def _psu_powers_bench(_psu_or_none):
"""Autouse: realizes :func:`_psu_or_none` so the PSU comes up at
session start even for tests that don't request ``psu`` by name.
Without this, a MUM-only test (which never references ``psu``)
would never trigger PSU setup, and on a bench where the Owon
powers the ECU the test would fail with "ECU not responding".
No assertions, no skips — purely a lifecycle hook.
"""
yield
@pytest.fixture(scope="session")
def psu(_psu_or_none) -> OwonPSU:
"""Public PSU fixture for tests that read measurements or perturb voltage.
Skips cleanly when the PSU isn't configured / reachable so tests
targeting the PSU stay portable across benches that don't have one
wired up.
"""
if _psu_or_none is None:
pytest.skip(
"PSU not available (config.power_supply.enabled=false, "
"no port configured, or port not reachable)."
)
return _psu_or_none