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.
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_limittest_cases:
test_5v0_rail:
test_id: "1.1"
description: "5V0 rail within tolerance"
min_limit: 4.90
max_limit: 5.102. 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.
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, …):
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:
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_mafixture_settings:
slot_1:
slot_id: "1"
user_vars:
boot_delay_s: 0.0
max_current_ma: 200.05. Serial command / response
Wrap your console protocol in the interface and keep tests readable:
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:
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.okClicked7. Status banners for long steps
Show the operator what's happening, and always clear the banner afterwards:
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:
# 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 dutPut 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.