Appearance
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_limityaml
test_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.
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_mayaml
fixture_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:
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.okClicked7. 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 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.
9. LED and optical measurement
LEDs and indicators are tested by their light output, not just a voltage. Three approaches are common, trading accuracy against cost and fixturing effort:
- Feasa LED Analyzer — optical fibers positioned next to each LED measure color and luminous intensity. Highest accuracy and repeatability, and it reads many LEDs at once; higher cost, and the fibers are fragile. Supported in
f3ts-hardware-utilsviaf3ts_hardware_utils.feasa.Feasa. - I2C optical sensor (for example an ISL29125 breakout) — a sensor placed near the LED reports intensity over I2C. Low cost and simple, but less accurate and sensitive to sensor placement.
- USB camera + OpenCV — a camera captures the board and software computes per-LED intensity. Good for many LEDs or screens at low cost, but it needs vision code and controlled ambient light.
The Feasa analyzer is the usual production choice. Wrap it in your interface, capture once, then read each channel:
python
# interface.py
from f3ts_hardware_utils.feasa import Feasa
class WidgetInterface:
def __init__(self, feasa_port: str):
self.feasa = Feasa(feasa_port)
def capture_leds(self):
"""Trigger a reading on all Feasa channels."""
self.feasa.capture()
def read_led(self, channel):
"""Return the {r, g, b, i} reading for one Feasa channel."""
return self.feasa.get_rgbi(channel)Assert each indicator against per-channel color/intensity limits, one sub-result per indicator:
python
INDICATOR_MIN = {"r": 250, "g": 0, "b": 0, "i": 20000}
INDICATORS = {0: "D1", 1: "D2"}
def test_indicator_leds(interface, f3ts_assert):
interface.capture_leds() # one capture, then read each channel
for channel, name in INDICATORS.items():
reading = interface.read_led(channel)
in_spec = all(reading[k] >= INDICATOR_MIN[k] for k in INDICATOR_MIN)
f3ts_assert(
assert_value=in_spec,
meas=str(reading),
min_limit=str(INDICATOR_MIN),
description=f"{name} color and intensity",
)