"""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