What Are Test Sockets and How to Use Them
A test socket is one position on a fixture that holds a unit under test, together with the software context that tracks it. A four-position fixture has four sockets, and the test executive runs four copies of the test, one per socket, each with its own unit, its own measurements, and its own result.
The term comes from NI TestStand, where sockets are the mechanism behind parallel and batch models. The TofuPilot Framework calls the same concept a slot. This guide explains what sockets do, what a socket identifier is for, and how to configure them.
Why the Concept Exists
Running the same test on four units at once creates an identity problem. Four voltage measurements arrive, and every one of them is called supply_voltage. Without a per-position identity, there is no way to say which unit failed, which fixture position is drifting, or which serial number to attach to a result.
A socket solves this by being the thing that owns the unit for the duration of the test:
| A socket owns | Example |
|---|---|
| The unit under test | Serial SN-2024-003, in position 3 |
| Its instrument channels | DMM channel 3, PSU output 3 |
| Its measurements | The voltage read from that unit |
| Its outcome | Pass, independent of the other positions |
| Its result record | One run on the dashboard |
Four sockets means four independent test contexts sharing one station, one procedure, and one operator.
Sockets are the mechanism behind parallel and batch execution; for choosing between those strategies, see Sequential vs Parallel vs Batch Testing.
The Socket Identifier
Every socket carries an identifier your test code can read. This is the single most important thing a socket gives you, because it is how a generic test becomes position-aware.
The problem it solves: your phase code is written once but runs N times. Something has to tell instance 3 to talk to instrument channel 3. That something is the socket identifier.
Most phases never need it. When each socket has its own instrument, the plug is socket-scoped and the phase stays generic:
def measure_voltage(measurements, power_supply): measurements.voltage = power_supply.read_voltage()When a phase does need to know its position — addressing a channel on a shared instrument, say — it reads run.slot_id, the key declared in execution.slots:
CHANNELS = {"s1": 101, "s2": 102, "s3": 103, "s4": 104}def measure_rail(measurements, run, dmm): measurements.rail_voltage = dmm.measure_dc_volts(CHANNELS[run.slot_id])Keep per-socket wiring in configuration and lookup tables rather than branching on position inside the test logic. That is what keeps the procedure readable as the fixture grows from four positions to eighty.
Configuring Sockets
In the TofuPilot Framework, sockets are declared as slots under execution:
execution: slots: - name: USB0 - name: USB1 - name: USB2 - name: USB3Each entry takes a name and an optional key, the identifier phases see. When the key is omitted it is derived from the name. Naming slots after the physical thing they correspond to — a USB port, a nest number, a probe head — is what makes a failure report actionable, because "USB2 has failed 40 times today" points at hardware.
For a fixture with many identical positions, give a count:
execution: slots: 80The count expands to keys slot_01 through slot_80, zero-padded so positions sort correctly everywhere. The count is capped at 1024, and the engine bounds slots times phases at 10,000 jobs per run. Use the list form when positions have meaningful names.
Per-Socket Unit Identification
Each socket identifies its own unit. For a station where an operator scans each position, the unit prompt appears per socket. For a rack where serials are derived from position, use the {slot} and {slot_name} placeholders:
execution: slots: - key: s01 name: Nest 1 - key: s02 name: Nest 2unit: auto_identify: true serial_number: default_value: "BURNIN-{slot}" part_number: default_value: "PCB-MAIN-V2" batch_number: default_value: "{slot_name}"With auto_identify: true the rack starts with no operator input at all. Without it, the expanded value pre-fills each socket's prompt, which is the useful middle ground: the operator confirms rather than types.
Single-slot procedures never see the placeholders and keep them as literal text, so adding them early does not break a single-position station.
One Run Per Socket
Each socket uploads its own run: its unit, its phases plus any shared setup and teardown, its measurements, logs, attachments, and its own outcome.
The runs from one start share an execution_id, and each carries its slot_key and slot_name. That gives two views of the same data:
- Group by
execution_idto see everything that happened in one cycle. - Filter by
slot_keyto see one fixture position over time.
The second view is what catches a bad position. If nest 3 fails 12 percent while the other seven sit at 2 percent, the problem is the fixture, not the product — and no amount of aggregate yield analysis would have shown it.
Socket-Scoped and Shared Resources
Instruments are attached with a scope that decides how many instances exist:
| Scope | Instance | Use for |
|---|---|---|
slot (default) | One per socket | Per-position PSU channel, per-position serial port |
execution | One shared by all sockets | A single DMM behind a mux, a chamber |
station | One per station, held across runs | Slow-to-open VISA or TCP connections |
plugs: - name: power_supply python: plugs.psu:PowerSupply scope: slot - name: chamber python: plugs.chamber:ThermalChamber scope: executionA plug instance runs one method call at a time. When several sockets call the same execution or station plug, the calls queue and execute one at a time. This is the main throughput trap in socket-based testing: a shared instrument method that takes two seconds costs eight seconds across four sockets, and the parallelism you configured quietly does not exist. Keep shared plug methods short and put long waits in phases. Share Instruments Across Parallel Slots covers this in detail.
Failure Isolation
Sockets fail independently. A failing phase under the default stop behavior, a phase.stop(), an error, or a timeout skips the remaining phases of that socket and runs its socket-scoped teardown, while the other sockets continue. Each socket gets its own outcome, and the run outcome is the worst of them.
Only three things stop every socket: a failure in a phase shared by all of them (setup or teardown with scope: execution), a plug that fails to initialize, or the operator stopping the run.
This creates one rule worth stating plainly, because violating it produces intermittent failures that are very hard to diagnose:
A socket-scoped teardown can run while its neighbours are still testing. Anything it touches must belong to that socket alone. A rack power supply, a chamber, a fixture lock, a shared mux belongs in a teardown phase with scope: execution, which runs once after every socket is done.
teardown: - name: Discharge This Position python: phases.discharge - name: Power Down Rack Supply python: phases.rack_off scope: executionIf Power Down Rack Supply were socket-scoped, the first socket to finish would cut power to every unit still under test. The symptom is a handful of unexplained failures on the slowest positions, which looks like a product problem and is not.
Common Pitfalls
Hardcoding an instrument address in a socket-scoped plug. Four sockets then talk to the same physical channel, and four units report the first unit's measurements. Pass the address through per-socket plug config.
Assuming sockets speed up a shared instrument. They do not. Queuing on an execution-scoped plug serializes those calls by design.
Naming sockets by index only. slot_03 is harder to act on than Nest 3 or Probe C. Names appear in every failure report.
Forgetting that shared setup failure stops everything. A chamber that will not reach temperature correctly aborts all sockets. That is the intended behavior, but it means shared setup phases deserve retries and clear error messages more than per-socket phases do.
Key Points
- A test socket is one fixture position plus the software context that owns its unit, measurements, and outcome.
- The socket identifier, read from
run.slot_id, is what makes one generic test position-aware. - In the TofuPilot Framework, sockets are
execution.slots, and each uploads its own run tagged withslot_keyandexecution_id. - Filtering results by socket is how you catch a bad fixture position that aggregate yield hides.
- Shared instruments serialize across sockets, and shared resources must use
scope: executionin teardown.