Skip to content
Migrating from Legacy Systems

Migrate from LabVIEW TestExec to Python

A practical migration guide from LabVIEW TestExec to Python-based test automation with OpenHTF and TofuPilot, covering architecture mapping and code examples.

JJulien Buteau
intermediate12 min readMarch 14, 2026

How to Migrate from LabVIEW TestExec to Python with TofuPilot

LabVIEW TestExec served its purpose. But the licensing costs keep climbing, finding LabVIEW developers is getting harder, and your CI/CD pipeline doesn't speak G code. Python gives you the language, OpenHTF gives you the test framework, and TofuPilot gives you the data platform.

Here's how to make the switch without losing your test coverage.

Why Teams Migrate

Pain PointLabVIEW TestExecPython + TofuPilot
LicensingPer-seat, annual renewalFree and open source
Developer poolShrinking, specializedLarge, growing
Version controlBinary VIs, merge conflictsText files, standard Git
CI/CD integrationCustom adapters neededNative pytest/OpenHTF support
Instrument driversNI ecosystem onlyPyVISA, any SCPI instrument
Data storageLocal TDM/TDMS filesCloud database with API

Prerequisites

  • Python 3.8+ installed
  • pip install openhtf tofupilot pyvisa
  • Access to your existing LabVIEW test sequences and specs
  • Instrument documentation (SCPI command references)

Step 1: Map Your TestExec Architecture

LabVIEW TestExec and OpenHTF share similar concepts with different names:

LabVIEW TestExecOpenHTF + TofuPilotNotes
Test Sequencehtf.Test()Top-level test container
Test StepPhase functionDecorated Python function
Limithtf.Measurement validator.in_range(), .at_most(), etc.
Step ResultMeasurement valueStored with pass/fail status
Sequence File (.seq)Python script (.py)Version-controlled text
Operator InterfaceTofuPilot station UIWeb-based, no LabVIEW runtime
ReportTofuPilot dashboardAutomatic, real-time
Station GlobalModule-level config or env varStandard Python patterns

Step 2: Convert Test Steps to Python Phases

A typical LabVIEW TestExec step that reads a voltage becomes a Python phase:

Before (LabVIEW TestExec pseudo-code):

Step: "Read 5V Rail" Type: Numeric Limit Test Instrument: DMM_1 (NI PXI-4072) Command: Measure DC Voltage Low Limit: 4.95 High Limit: 5.05 Units: V

After (Python with OpenHTF):

power_test.py
import openhtf as htf
from openhtf.util import units
import pyvisa

@htf.measures(
    htf.Measurement("voltage_5v_rail")
        .with_units(units.VOLT)
        .in_range(4.95, 5.05)
        .doc("5V rail output voltage"),
)
def read_5v_rail(test, dmm):
    """Read and validate 5V rail voltage."""
    voltage = float(dmm.query("MEAS:VOLT:DC?"))
    test.measurements.voltage_5v_rail = voltage

The pattern is always the same:

  1. Define measurements with limits (replaces the Limit columns)
  2. Write instrument control in the phase body (replaces the VI)
  3. Assign measured values (replaces the step result)

Step 3: Handle Instrument Connections

LabVIEW TestExec uses NI's instrument driver architecture. Python uses PyVISA, which works with NI, Keysight, Rigol, and any SCPI-compatible instrument.

instruments.py
import pyvisa

def connect_instruments():
    """Replace TestExec station globals with PyVISA connections."""
    rm = pyvisa.ResourceManager()

    instruments = {
        "dmm": rm.open_resource("TCPIP::192.168.1.10::INSTR"),
        "psu": rm.open_resource("TCPIP::192.168.1.11::INSTR"),
        "scope": rm.open_resource("USB0::0x0957::0x1796::INSTR"),
    }

    # Configure instruments
    instruments["dmm"].timeout = 5000
    instruments["psu"].write("*RST")

    return instruments

If you're using NI PXI instruments, the NI-VISA backend works with PyVISA. Your GPIB, USB, and TCP/IP instruments work without any NI software.

Step 4: Convert Sequence Flow Control

LabVIEW TestExec has built-in flow control (preconditions, post-actions, branching). In OpenHTF, you use standard Python:

Conditional execution:

conditional_test.py
import openhtf as htf

@htf.measures(
    htf.Measurement("board_variant")
        .with_allowed_values("A", "B", "C"),
)
def detect_variant(test):
    """Read board variant from EEPROM or resistor divider."""
    variant = read_board_variant()
    test.measurements.board_variant = variant
    test.state["variant"] = variant

@htf.measures(
    htf.Measurement("bluetooth_rssi")
        .with_units(units.DECIBEL)
        .at_least(-70),
)
def bluetooth_test(test):
    """Only runs on variant B (Bluetooth-equipped boards)."""
    if test.state.get("variant") != "B":
        return  # Skip for non-BT variants

    rssi = scan_bluetooth()
    test.measurements.bluetooth_rssi = rssi

Setup and teardown:

setup_teardown.py
import openhtf as htf

@htf.PhaseOptions(timeout_s=10)
def setup_fixture(test, fixture_controller):
    """Replaces TestExec Setup step group."""
    fixture_controller.clamp()
    fixture_controller.connect_probes()

@htf.PhaseOptions(run_if=lambda: True)  # Always runs
def teardown_fixture(test, fixture_controller):
    """Replaces TestExec Cleanup step group. Runs even on failure."""
    fixture_controller.release()
    fixture_controller.disconnect_probes()

Step 5: Set Up Data Collection

LabVIEW TestExec stores results in local files (TDM, TDMS, XML). TofuPilot stores everything in the cloud with full traceability.

main_test.py
import openhtf as htf
from tofupilot import TofuPilotClient

def main():
    instruments = connect_instruments()

    test = htf.Test(
        setup_fixture,
        detect_variant,
        read_5v_rail,
        bluetooth_test,
        teardown_fixture,
        phase_kwargs={
            "dmm": instruments["dmm"],
            "psu": instruments["psu"],
        },
    )

    test.add_output_callbacks(
        TofuPilotClient().as_openhtf_callback(
            procedure_id="pcba-fct-v3",
            procedure_name="PCBA FCT (migrated from TestExec)",
        )
    )

    test.execute(test_start=htf.PhaseDescriptor.wrap(
        lambda test: setattr(test, "dut_id", input("Scan serial: "))
    ))

if __name__ == "__main__":
    main()

Migration Strategy

Don't rewrite everything at once. Migrate station by station:

  1. Pick one test station with the simplest sequence
  2. Document the existing test spec (steps, limits, instruments, flow)
  3. Write the Python equivalent following the patterns above
  4. Run both systems in parallel for one production batch
  5. Compare results to verify equivalence
  6. Cut over when results match
  7. Move to the next station

Keep your LabVIEW TestExec sequences as reference documentation. The Python scripts replace them, but having the original spec helps during validation.

Common Gotchas

IssueSolution
NI drivers need LabVIEW runtimePyVISA with NI-VISA backend works without LabVIEW
TestExec parallel step groupsUse Python threading or asyncio
TestExec callbacks (on fail, on pass)OpenHTF phase options and output callbacks
Station model/serial trackingTofuPilot station configuration properties
TestExec report generationTofuPilot generates reports automatically
Operator interface buttonsTofuPilot's web-based station UI or custom Tkinter

The hardest part isn't the code. It's validating that the new tests catch the same failures as the old ones. Run both systems in parallel until you're confident.

More Guides

Put this guide into practice