Migrating from Legacy Systems

Manufacturing Test Frameworks Compared

Compare TofuPilot Framework, OpenHTF, pytest, OpenTAP and NI TestStand for manufacturing test, with real code, feature matrix and cost analysis.

JJulien Buteau
intermediate14 min readMarch 14, 2026
Validate one test before a wider rollout.
Existing test
Connect results
Compare one run
Roll out

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

FrameworkLanguageLicenseOriginFocus
TofuPilot FrameworkPython (Rust engine)MIT (free)TofuPilotManufacturing test with parallel execution
OpenHTFPythonApache 2.0 (free)GoogleManufacturing/production test
pytestPythonMIT (free)CommunitySoftware testing, adapted for hardware
OpenTAPC# / PythonMPL 2.0 (free)KeysightInstrument-heavy test automation
NI TestStandLabVIEW / C / PythonCommercial ($4,310/seat)NI (Emerson)Enterprise manufacturing test

Feature Comparison Matrix

FeatureTofuPilot FrameworkOpenHTFpytestOpenTAPTestStand
Structured measurementsBuilt-in (YAML validators)Built-in (name, value, limits, units)Manual (assert only)Plugin-basedBuilt-in
Serial number inputBuilt-inBuilt-in promptManualPluginBuilt-in
Operator UIDeclarative, from the procedureBuilt-in web UINoneBuilt-in (GUI editor)Built-in (Sequence Editor)
Test sequencingYAML procedurePhase orderingFunction ordering (plugins)Step ordering (GUI)Sequence files (GUI)
Parallel phasesNative (Rust engine)NoVia xdistNativeNative
Multi-slot fixturesNativeLimitedManualNativeNative
Waveform aggregationsBuilt-in (mean, min, max, std)NoManualPluginManual
Per-axis validatorsBuilt-inNoManualNoNo
Instrument driversPlugs (plain Python classes)Plugs (Python)Fixtures (Python)Plugins (C#/Python)NI drivers (LabVIEW)
Deployment to stationsGit push, instant rollbackYour problemYour problemManualManual
Offline handlingQueues and syncsQueues and syncs (via CLI)Your problemManualDatabase-dependent
Version controlGit (YAML + Python)Git (Python files)Git (Python files)Git (XML + code)Difficult (binary .seq files)
CI/CD integrationNative (Python)Native (Python)Native (Python)PossibleDifficult
Cross-platformLinux, macOS, WindowsLinux, macOS, WindowsLinux, macOS, WindowsLinux, WindowsWindows only
CommunityNewer, smallerSmall (~640 stars)Massive (11K+ stars)Small (~200 stars)Large (NI forums)
Learning curveLow (YAML + plain Python)MediumLowMediumHigh
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.yaml
24 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.4
phases/power_rail.py
def power_rail(measurements, dut):    measurements.rail_3v3 = dut.read_voltage()
plugs/dut.py
class Dut:    def read_voltage(self) -> float:        return 3.31  # Replace with an instrument read

Run 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.py
31 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

comparison/pytest_example.py
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.cs
21 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 FrameworkOpenHTFpytestOpenTAPTestStand
License (5 seats)$0$0$0$0$21,550/year
License (20 seats)$0$0$0$0$86,200/year
Runtime deploymentFreeFreeFreeFreeAdditional runtime licenses
TrainingSelf-taught (docs)Self-taught (docs)Self-taught (docs)Self-taught (docs)NI training courses ($2K+)
Vendor lock-inNone (MIT, open source)NoneNoneLow (Keysight-adjacent)High (NI ecosystem)
SupportCommunity + TofuPilotCommunity (GitHub)Community (massive)Community + KeysightNI support contract

When to Use Each Framework

ScenarioBest ChoiceWhy
Production test, throughput-bound lineTofuPilot FrameworkParallel phases and multi-slot fixtures cut cycle time
Production test, operators need guided workflowsTofuPilot FrameworkDeclarative operator UI, no frontend work
Validating waveforms or sweepsTofuPilot FrameworkAggregations and per-axis validators check curve shape
New stations, many sitesTofuPilot FrameworkDeploy on Git push, instant rollback, offline queueing
Existing OpenHTF suites in productionOpenHTFRuns natively on the TofuPilot CLI; migration cost isn't worth it
Single pure-Python paradigm, no YAML layerOpenHTFEverything in Python decorators
R&D validation, firmware CIpytestFlexible, fast iteration, CI/CD native, huge ecosystem
Multi-vendor instruments, enterpriseOpenTAPPlugin architecture, GUI editor, Keysight integration
Existing NI hardware, large enterpriseTestStandDeep NI integration, enterprise support, existing infrastructure
Mixed (R&D + production)pytest + TofuPilot Frameworkpytest for validation, Framework for production

Migration Paths

From TestStand to the TofuPilot Framework

TestStand ConceptTofuPilot Framework Equivalent
Sequence file (.seq)procedure.yaml
Step typesPhase functions in phases/
Numeric Limit Testmeasurements with validators in YAML
String Value Testvalidators with operator: "=="
Step module (code)Plug class in plugs/
Process modelBuilt into the CLI
Report generationTofuPilot dashboard
UUT serial numberunit.serial_number in the procedure
Parallel sequence executionexecution.slots

From OpenHTF to the TofuPilot Framework

OpenHTF ConceptTofuPilot Framework Equivalent
Phase function with @htf.measuresPlain function, measurements declared in YAML
.in_range(3.2, 3.4)validators with >= and <=
BasePlug subclassPlain Python class
@htf.plug(dut=DutPlug)Plug injected by parameter name
test.measurements.x = vmeasurements.x = v
htf.Test(phase1, phase2)main: list in the procedure

From pytest to the TofuPilot Framework

pytest ConceptTofuPilot Framework Equivalent
Test functionPhase function
FixturePlug
Assert statementvalidators in the procedure YAML
conftest.pyplugs/ directory
JUnit XMLTofuPilot dashboard

TofuPilot Integration

All five frameworks work with TofuPilot:

FrameworkIntegration MethodEffort
TofuPilot FrameworkNative (tofupilot run --upload)None
OpenHTFNative on the CLI, or upload() callbackNone, or 2 lines
pytestPython SDK (TofuPilotClient)~10 lines per test
OpenTAPREST API or Python SDKMedium
TestStandREST APIMedium

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

  1. 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.
  2. 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.
  3. Are you building production tests on new stations? Yes: the TofuPilot Framework. Parallel execution, declarative operator UI, and deploy-on-push are built in.
  4. Are you doing R&D or firmware validation? Yes: pytest. More flexible, better CI/CD.
  5. Do you need a GUI for non-programmers? Yes: OpenTAP. Visual step editor.
  6. Unsure? Start with the TofuPilot Framework. tofupilot run ./procedure.yaml executes a procedure on your machine without an account.

For a deeper comparison of the two purpose-built Python options, see TofuPilot Framework vs OpenHTF.

More Guides

Put this guide into practice