Five frameworks dominate manufacturing test automation: the TofuPilot Framework, OpenHTF, pytest, OpenTAP, and NI TestStand. Each makes different tradeoffs between structure, flexibility, cost, and ecosystem. This guide compares them with real code, concrete metrics, and decision criteria so you can pick the right one.
Framework Overview
| Framework | Language | License | Origin | Focus |
|---|---|---|---|---|
| TofuPilot Framework | Python (Rust engine) | MIT (free) | TofuPilot | Manufacturing test with parallel execution |
| OpenHTF | Python | Apache 2.0 (free) | Manufacturing/production test | |
| pytest | Python | MIT (free) | Community | Software testing, adapted for hardware |
| OpenTAP | C# / Python | MPL 2.0 (free) | Keysight | Instrument-heavy test automation |
| NI TestStand | LabVIEW / C / Python | Commercial ($4,310/seat) | NI (Emerson) | Enterprise manufacturing test |
Feature Comparison Matrix
| Feature | TofuPilot Framework | OpenHTF | pytest | OpenTAP | TestStand |
|---|---|---|---|---|---|
| Structured measurements | Built-in (YAML validators) | Built-in (name, value, limits, units) | Manual (assert only) | Plugin-based | Built-in |
| Serial number input | Built-in | Built-in prompt | Manual | Plugin | Built-in |
| Operator UI | Declarative, from the procedure | Built-in web UI | None | Built-in (GUI editor) | Built-in (Sequence Editor) |
| Test sequencing | YAML procedure | Phase ordering | Function ordering (plugins) | Step ordering (GUI) | Sequence files (GUI) |
| Parallel phases | Native (Rust engine) | No | Via xdist | Native | Native |
| Multi-slot fixtures | Native | Limited | Manual | Native | Native |
| Waveform aggregations | Built-in (mean, min, max, std) | No | Manual | Plugin | Manual |
| Per-axis validators | Built-in | No | Manual | No | No |
| Instrument drivers | Plugs (plain Python classes) | Plugs (Python) | Fixtures (Python) | Plugins (C#/Python) | NI drivers (LabVIEW) |
| Deployment to stations | Git push, instant rollback | Your problem | Your problem | Manual | Manual |
| Offline handling | Queues and syncs | Queues and syncs (via CLI) | Your problem | Manual | Database-dependent |
| Version control | Git (YAML + Python) | Git (Python files) | Git (Python files) | Git (XML + code) | Difficult (binary .seq files) |
| CI/CD integration | Native (Python) | Native (Python) | Native (Python) | Possible | Difficult |
| Cross-platform | Linux, macOS, Windows | Linux, macOS, Windows | Linux, macOS, Windows | Linux, Windows | Windows only |
| Community | Newer, smaller | Small (~640 stars) | Massive (11K+ stars) | Small (~200 stars) | Large (NI forums) |
| Learning curve | Low (YAML + plain Python) | Medium | Low | Medium | High |
| Annual cost (5 seats) | $0 | $0 | $0 | $0 | ~$21,550 |
The Same Test in Each Framework
A simple functional test: measure a 3.3V rail voltage, check it's within 3.2V to 3.4V.
TofuPilot Framework
The procedure YAML declares the sequence and the limits. Phases are plain Python functions with no decorators, and plugs are plain Python classes.
procedure.yaml24 lines
name: PCBA Functional Testversion: 1.0.0unit: serial_number: default_value: "PCBA000001" part_number: default_value: "PCBA-100"plugs: - name: dut python: plugs.dut:Dutmain: - name: Power Rail Test python: phases.power_rail measurements: - name: Rail 3V3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4def power_rail(measurements, dut): measurements.rail_3v3 = dut.read_voltage()class Dut: def read_voltage(self) -> float: return 3.31 # Replace with an instrument readRun it from the procedure directory with tofupilot run.
Strengths: Limits live in the YAML as data, so the test plan is readable and diffable separately from the logic. Phases and fixture slots run in parallel on a Rust engine while your code stays plain Python. Operator UI is generated from the procedure with no frontend work. Procedures deploy to stations on a Git push, with instant rollback, and runs queue offline when the network drops.
Weakness: Newer than OpenHTF, so a smaller community and fewer public examples.
OpenHTF
comparison/openhtf_example.py31 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import uploadclass DutPlug(htf.plugs.BasePlug): def setUp(self): self.voltage = 3.31 # Replace with instrument read def tearDown(self): pass@htf.measures( htf.Measurement("rail_3v3") .in_range(3.2, 3.4) .with_units(units.VOLT),)@htf.plug(dut=DutPlug)def test_power(test, dut): test.measurements.rail_3v3 = dut.voltagedef main(): test = htf.Test(test_power, procedure_id="FCT-001", part_number="PCBA-100") test.add_output_callbacks(upload()) test.execute(test_start=lambda: input("Serial: "))if __name__ == "__main__": main()Install the OpenHTF extra with pip install "tofupilot[openhtf]". upload() reads TOFUPILOT_API_KEY from the environment. Alternatively, run the same suite with tofupilot run and the CLI handles streaming and upload with no callback at all.
Strengths: Measurements are structured data (name, value, limits, units). Years of production use at scale. Serial number prompting is built in. Runs natively on the TofuPilot CLI.
Weakness: Phases execute sequentially against one unit. Parallel phases and multi-slot fixtures are orchestrated outside the framework.
pytest
import pytest@pytest.fixturedef dut(): connection = {"voltage": 3.31} # Replace with real connection yield connectiondef test_power_rail(dut): voltage = dut["voltage"] assert 3.2 <= voltage <= 3.4, f"3.3V rail: {voltage}V"Strengths: Familiar to every Python developer. Huge plugin ecosystem. Flexible fixture system. Great for R&D and validation.
Weakness: Measurements are implicit (assert statements). The measured value lives inside an assert and then disappears, so there is no structured data for analytics without extra code.
OpenTAP
comparison/opentap_example.cs21 lines
// OpenTAP C# test stepusing OpenTap;[Display("Power Rail Test", Group: "FCT")]public class PowerRailStep : TestStep{ [Display("Lower Limit")] public double LowerLimit { get; set; } = 3.2; [Display("Upper Limit")] public double UpperLimit { get; set; } = 3.4; public override void Run() { double voltage = 3.31; // Replace with instrument read Results.Publish("rail_3v3", new { Voltage = voltage }); if (voltage < LowerLimit || voltage > UpperLimit) UpgradeVerdict(Verdict.Fail); }}Strengths: GUI step editor for non-programmers. Strong Keysight instrument integration. Plugin architecture for test plans.
Weakness: C# primary language (Python plugin exists but is secondary). Smaller community.
NI TestStand
TestStand uses a visual sequence editor. The equivalent test is a sequence file (.seq) with a "Numeric Limit Test" step configured via GUI: test value = voltage reading, low limit = 3.2, high limit = 3.4.
Strengths: Mature, enterprise-grade. Deep NI hardware integration. Built-in report generation, database logging, parallel execution.
Weakness: $4,310/seat/year. Windows only. Binary sequence files don't version-control well. Tied to NI ecosystem.
Cost Analysis
| TofuPilot Framework | OpenHTF | pytest | OpenTAP | TestStand | |
|---|---|---|---|---|---|
| License (5 seats) | $0 | $0 | $0 | $0 | $21,550/year |
| License (20 seats) | $0 | $0 | $0 | $0 | $86,200/year |
| Runtime deployment | Free | Free | Free | Free | Additional runtime licenses |
| Training | Self-taught (docs) | Self-taught (docs) | Self-taught (docs) | Self-taught (docs) | NI training courses ($2K+) |
| Vendor lock-in | None (MIT, open source) | None | None | Low (Keysight-adjacent) | High (NI ecosystem) |
| Support | Community + TofuPilot | Community (GitHub) | Community (massive) | Community + Keysight | NI support contract |
When to Use Each Framework
| Scenario | Best Choice | Why |
|---|---|---|
| Production test, throughput-bound line | TofuPilot Framework | Parallel phases and multi-slot fixtures cut cycle time |
| Production test, operators need guided workflows | TofuPilot Framework | Declarative operator UI, no frontend work |
| Validating waveforms or sweeps | TofuPilot Framework | Aggregations and per-axis validators check curve shape |
| New stations, many sites | TofuPilot Framework | Deploy on Git push, instant rollback, offline queueing |
| Existing OpenHTF suites in production | OpenHTF | Runs natively on the TofuPilot CLI; migration cost isn't worth it |
| Single pure-Python paradigm, no YAML layer | OpenHTF | Everything in Python decorators |
| R&D validation, firmware CI | pytest | Flexible, fast iteration, CI/CD native, huge ecosystem |
| Multi-vendor instruments, enterprise | OpenTAP | Plugin architecture, GUI editor, Keysight integration |
| Existing NI hardware, large enterprise | TestStand | Deep NI integration, enterprise support, existing infrastructure |
| Mixed (R&D + production) | pytest + TofuPilot Framework | pytest for validation, Framework for production |
Migration Paths
From TestStand to the TofuPilot Framework
| TestStand Concept | TofuPilot Framework Equivalent |
|---|---|
| Sequence file (.seq) | procedure.yaml |
| Step types | Phase functions in phases/ |
| Numeric Limit Test | measurements with validators in YAML |
| String Value Test | validators with operator: "==" |
| Step module (code) | Plug class in plugs/ |
| Process model | Built into the CLI |
| Report generation | TofuPilot dashboard |
| UUT serial number | unit.serial_number in the procedure |
| Parallel sequence execution | execution.slots |
From OpenHTF to the TofuPilot Framework
| OpenHTF Concept | TofuPilot Framework Equivalent |
|---|---|
Phase function with @htf.measures | Plain function, measurements declared in YAML |
.in_range(3.2, 3.4) | validators with >= and <= |
BasePlug subclass | Plain Python class |
@htf.plug(dut=DutPlug) | Plug injected by parameter name |
test.measurements.x = v | measurements.x = v |
htf.Test(phase1, phase2) | main: list in the procedure |
From pytest to the TofuPilot Framework
| pytest Concept | TofuPilot Framework Equivalent |
|---|---|
| Test function | Phase function |
| Fixture | Plug |
| Assert statement | validators in the procedure YAML |
| conftest.py | plugs/ directory |
| JUnit XML | TofuPilot dashboard |
TofuPilot Integration
All five frameworks work with TofuPilot:
| Framework | Integration Method | Effort |
|---|---|---|
| TofuPilot Framework | Native (tofupilot run --upload) | None |
| OpenHTF | Native on the CLI, or upload() callback | None, or 2 lines |
| pytest | Python SDK (TofuPilotClient) | ~10 lines per test |
| OpenTAP | REST API or Python SDK | Medium |
| TestStand | REST API | Medium |
The TofuPilot Framework and OpenHTF both run natively on the CLI, which streams phases, measurements, logs, attachments, and the operator UI to the dashboard with no extra code on either side. For a self-managed pipeline that skips the CLI, OpenHTF also has the upload() output callback shown above.
Decision Flowchart
- Do you have existing TestStand infrastructure? Yes, and it works well: stay with TestStand. Yes, but you want to migrate: move to the TofuPilot Framework.
- Do you already run OpenHTF in production? Yes: keep it. It runs natively on the TofuPilot CLI, and the migration cost isn't worth it unless you need parallel slots.
- Are you building production tests on new stations? Yes: the TofuPilot Framework. Parallel execution, declarative operator UI, and deploy-on-push are built in.
- Are you doing R&D or firmware validation? Yes: pytest. More flexible, better CI/CD.
- Do you need a GUI for non-programmers? Yes: OpenTAP. Visual step editor.
- Unsure? Start with the TofuPilot Framework.
tofupilot run ./procedure.yamlexecutes a procedure on your machine without an account.
For a deeper comparison of the two purpose-built Python options, see TofuPilot Framework vs OpenHTF.
