A test sequencer runs ordered phases, decides which to skip, collects operator input, and records the outcome. This guide shows how to build one with OpenHTF and log structured results to TofuPilot.
Prerequisites
- Python 3.8+
- TofuPilot account and API key
- OpenHTF installed:
pip install openhtf tofupilot
What a Test Sequencer Does
A sequencer is more than a list of test steps. It controls:
| Responsibility | Example |
|---|---|
| Phase ordering | Power-on before functional tests |
| Skip logic | Skip RF calibration if hardware variant lacks antenna |
| Operator interaction | Prompt for serial number, confirm visual inspection |
| Result aggregation | Single pass/fail outcome from all phases |
| Teardown | Power off even when a phase fails |
OpenHTF maps directly to these responsibilities. Each phase is a Python function. The test object sequences them, evaluates measurements, and streams results to TofuPilot.
Step 1: Define Phases
Each phase is a decorated function. Add measurements to capture numeric results with limits.
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement('supply_voltage')
.in_range(minimum=4.75, maximum=5.25)
.with_units(units.VOLT)
)
def power_on_test(test):
voltage = read_supply_voltage()
test.measurements.supply_voltage = voltage
@htf.measures(
htf.Measurement('loop_back_result').equals(True)
)
def uart_loopback_test(test):
result = send_uart_loopback()
test.measurements.loop_back_result = result
@htf.measures(
htf.Measurement('output_current')
.in_range(minimum=0.9, maximum=1.1)
.with_units(units.AMPERE)
)
def load_test(test):
current = measure_output_current()
test.measurements.output_current = currentStep 2: Use PhaseGroups for Setup and Teardown
PhaseGroup guarantees teardown runs even when a test phase fails. This is critical for hardware tests that hold relays, power supplies, or fixtures.
from openhtf import PhaseGroup
def power_on(test):
enable_power_supply()
test.logger.info('Power supply enabled')
def power_off(test):
# Runs even if functional_tests fails
disable_power_supply()
test.logger.info('Power supply disabled')
# Build the group: setup -> main -> teardown
test_group = PhaseGroup(
setup=[power_on],
main=[power_on_test, uart_loopback_test, load_test],
teardown=[power_off],
)teardown phases run regardless of outcome. setup failures skip main but still run teardown.
Step 3: Inject Plugs for Hardware Access
Plugs encapsulate instrument or fixture communication. Inject them with @htf.plug.
import openhtf as htf
class PowerSupplyPlug(htf.plugs.BasePlug):
def setUp(self):
self._conn = open_supply_connection()
def read_voltage(self):
return self._conn.query('MEAS:VOLT?')
def tearDown(self):
self._conn.close()from openhtf.util import units
from plugs.supply import PowerSupplyPlug
@htf.plug(supply=PowerSupplyPlug)
@htf.measures(
htf.Measurement('supply_voltage')
.in_range(minimum=4.75, maximum=5.25)
.with_units(units.VOLT)
)
def power_on_test(test, supply):
test.measurements.supply_voltage = supply.read_voltage()Note the @htf.plug(name=PlugClass) form. Don't use type hints for plug injection.
Step 4: Add Skip Logic for Conditional Execution
Return htf.PhaseResult.SKIP to bypass a phase based on DUT variant.
import openhtf as htf
@htf.measures(
htf.Measurement('rf_output_power')
.in_range(minimum=18.0, maximum=22.0)
)
def rf_calibration_test(test):
if not has_rf_module():
test.logger.info('No RF module detected, skipping RF calibration')
return htf.PhaseResult.SKIP
power = measure_rf_output()
test.measurements.rf_output_power = powerKeep skip decisions data-driven rather than hardcoded. Read hardware configuration in an earlier phase or from a config file.
Step 5: Build Multi-SKU Sequences
Different product variants need different phase sets. Compose sequences dynamically from a SKU map.
from openhtf import PhaseGroup
# Base phases run on every SKU
BASE_PHASES = [power_on_test, uart_loopback_test]
# Extra phases per SKU
SKU_PHASES = {
'MODEL_A': [load_test],
'MODEL_B': [load_test, rf_calibration_test],
'MODEL_C': [], # base only
}
def build_sequence(sku: str):
extra = SKU_PHASES.get(sku, [])
return PhaseGroup(
setup=[power_on],
main=BASE_PHASES + extra,
teardown=[power_off],
)Step 6: Log Results to TofuPilot
Wrap the test execution with TofuPilot. It captures every phase, measurement, and outcome.
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def main(sku: str = 'MODEL_A'):
sequence = build_sequence(sku)
test = htf.Test(
sequence,
test_name=f'Functional Test {sku}',
)
with TofuPilot(test):
test.execute(test_start=lambda: input('Enter serial number: '))
if __name__ == '__main__':
import sys
sku = sys.argv[1] if len(sys.argv) > 1 else 'MODEL_A'
main(sku)Phase Outcome Reference
| Return value | Effect |
|---|---|
htf.PhaseResult.CONTINUE | Phase passed, continue sequence |
htf.PhaseResult.SKIP | Phase skipped, continue sequence |
htf.PhaseResult.STOP | Phase failed, stop sequence (run teardown) |
| Measurement out of range | Phase fails automatically |
| Unhandled exception | Phase fails, teardown runs |
Full Example
import sys
import openhtf as htf
from openhtf import PhaseGroup
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
# -- Phases --
@htf.measures(
htf.Measurement('supply_voltage')
.in_range(minimum=4.75, maximum=5.25)
.with_units(units.VOLT)
)
def power_on_test(test):
test.measurements.supply_voltage = read_supply_voltage()
@htf.measures(
htf.Measurement('uart_loopback').equals(True)
)
def uart_loopback_test(test):
test.measurements.uart_loopback = send_uart_loopback()
@htf.measures(
htf.Measurement('output_current')
.in_range(minimum=0.9, maximum=1.1)
.with_units(units.AMPERE)
)
def load_test(test):
test.measurements.output_current = measure_output_current()
@htf.measures(
htf.Measurement('rf_output_power')
.in_range(minimum=18.0, maximum=22.0)
)
def rf_calibration_test(test):
if not has_rf_module():
return htf.PhaseResult.SKIP
test.measurements.rf_output_power = measure_rf_output()
def power_on(test):
enable_power_supply()
def power_off(test):
disable_power_supply()
# -- Sequence builder --
BASE_PHASES = [power_on_test, uart_loopback_test]
SKU_PHASES = {
'MODEL_A': [load_test],
'MODEL_B': [load_test, rf_calibration_test],
}
def build_sequence(sku):
return PhaseGroup(
setup=[power_on],
main=BASE_PHASES + SKU_PHASES.get(sku, []),
teardown=[power_off],
)
# -- Entry point --
def main():
sku = sys.argv[1] if len(sys.argv) > 1 else 'MODEL_A'
test = htf.Test(build_sequence(sku), test_name=f'Functional Test {sku}')
with TofuPilot(test):
test.execute(test_start=lambda: input('Enter serial number: '))
if __name__ == '__main__':
main()