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>
155 lines
5.8 KiB
Python
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
|