Skip to content

Pattern Cookbook

Focused recipes for common hardware-test patterns, all built around the fictional Widget Controller Board (WCB-1) used in the Starter Test Plan. Each recipe assumes the session-scoped interface fixture from The Test Interface and the f3ts fixtures.

The non-interactive recipes below are runnable offline; a complete, tested version lives in examples/cookbook_plan/.

1. Rail voltage against config limits

The canonical check: read a value, record it, assert against test_config limits from config.yml.

python
from pytest_f3ts.utils import log_vars


def test_5v0_rail(interface, test_config, record_property):
    meas = interface.measure_rail("5v0")
    log_vars(record_property)
    assert test_config.min_limit <= meas <= test_config.max_limit
yaml
test_cases:
  test_5v0_rail:
    test_id: "1.1"
    description: "5V0 rail within tolerance"
    min_limit: 4.90
    max_limit: 5.10

2. Many checks in one test (sub-results)

When one test covers several rails or channels, use f3ts_assert so each gets its own pass/fail row. Pass per-channel limits explicitly — test_config maps one entry per function, not per loop iteration.

python
RAIL_LIMITS = {"5v0": (4.90, 5.10), "3v3": (3.20, 3.40), "1v8": (1.71, 1.89)}


def test_all_rails(interface, f3ts_assert):
    for rail, (lo, hi) in RAIL_LIMITS.items():
        meas = interface.measure_rail(rail)
        f3ts_assert(
            assert_value=(lo <= meas <= hi),
            meas=meas,
            min_limit=lo,
            max_limit=hi,
            description=f"{rail} rail",
        )

A test with test_id 2.1 emits sub-results 2.1.1, 2.1.2, 2.1.3 — see Result Reporting.

3. Threshold tables

The same idea for any per-channel threshold map (LED intensities, tone levels, …):

python
LED_MIN = {"r": 200, "g": 200, "b": 200}


def test_led_channels(interface, f3ts_assert):
    for channel, floor in LED_MIN.items():
        intensity = interface.read_led(channel)
        f3ts_assert(
            assert_value=(intensity >= floor),
            meas=intensity,
            min_limit=floor,
            description=f"LED {channel} intensity",
        )

4. Tunable limits and delays

Keep settle times and limits adjustable without code changes — read them from environment variables and from per-slot fixture_config.user_vars:

python
import os
import time

from pytest_f3ts.utils import log_vars


def test_current_draw(interface, fixture_config, record_property):
    boot_delay = float(os.getenv("BOOT_DELAY_S", fixture_config.user_vars["boot_delay_s"]))
    max_ma = float(fixture_config.user_vars["max_current_ma"])
    time.sleep(boot_delay)  # settle after boot before measuring
    meas = interface.measure_current_ma("5v0")
    log_vars(record_property)
    assert meas <= max_ma
yaml
fixture_settings:
  slot_1:
    slot_id: "1"
    user_vars:
      boot_delay_s: 0.0
      max_current_ma: 200.0

5. Serial command / response

Wrap your console protocol in the interface and keep tests readable:

python
from pytest_f3ts.utils import log_vars


def test_firmware_version(interface, record_property):
    response = interface.serial_command("VER?")   # e.g. returns "VER 1.2.3"
    meas = response.removeprefix("VER ").strip()
    log_vars(record_property)
    assert meas == "1.2.3"

A real serial_command frames the payload (for example STX + payload + ETX) and reads until the terminator with a timeout — keep that logic in interface.py, not in the test.

6. Operator prompts and serial-number capture (needs the runner or -s)

Two-way operator interaction. On the Test Runner these are GUI dialogs; locally they fall back to the terminal, so run pytest with -s. Wrap input in a retry loop instead of failing fast:

python
import re

from pytest_f3ts import schemas

SERIAL_RE = re.compile(r"^WCB-\d{4}$")


def test_capture_serial(serial_number, user_dialog):
    for _ in range(3):
        resp = user_dialog.send(schemas.Dialog(
            title="Scan serial",
            message="Scan the board serial (WCB-####)",
            inputType="text",
        ))
        if resp.okClicked and SERIAL_RE.match(resp.inputText):
            serial_number.set(resp.inputText)
            return
    raise AssertionError("No valid serial number scanned")


def test_visual_inspection(user_dialog):
    resp = user_dialog.send(schemas.Dialog(
        title="Visual check",
        message="Are all three status LEDs lit?",
        okButtonText="Pass", cancelButtonText="Fail",
    ))
    assert resp.okClicked

7. Status banners for long steps

Show the operator what's happening, and always clear the banner afterwards:

python
def test_with_status_banner(interface, status_banner, f3ts_assert):
    status_banner.override(True, status="Measuring rails...", color="#f5a623")
    try:
        meas = interface.measure_rail("3v3")
        f3ts_assert(assert_value=(meas > 0), meas=meas, description="3V3 present")
    finally:
        status_banner.override(False)

Offline, status_banner prints the status to the terminal instead of driving the GUI.

8. Safe interface setup and teardown

Open the hardware once per session and always leave the DUT safe, even on failure, by registering teardown with addfinalizer:

python
# conftest.py
import pytest

from interface import WidgetInterface


@pytest.fixture(scope="session")
def interface(request):
    dut = WidgetInterface()
    request.addfinalizer(dut.close)  # safe power-down even if a test fails
    return dut

Put the safe-state logic (rails off in reverse order, GPIO to a known state) in the interface's close() so it runs no matter how the run ends. See The Test Interface.