Litmus fixtures #
The bundled pytest plugin registers 20 public fixtures, defined in src/litmus/pytest_plugin/__init__.py. Take any of them in a test's signature; pytest resolves and injects them by name. Names beginning with _ (e.g. _route_manager, _litmus_push_params) are internal and may change without notice.
This page is the comprehensive reference. For a guided introduction see the tutorial; for the seven @pytest.mark.litmus_* markers see Litmus markers.
At a glance #
Grouped by what you reach for the fixture for:
| Group | What you'd reach for it for | Fixtures |
|---|---|---|
| Recording measurements | Write a measurement row, resolve a limit, raise on FAIL, prompt the operator | verify, logger, limits, prompt |
| Talking to instruments | Get a driver instance, route a signal, hit a DUT pin | instruments, instrument, instrument_records, dut, pins, routes, fixture_manager |
| Reading per-test state | Active sweep params, observations, the connection currently being iterated | context, connections |
| Reading loaded configuration | The typed YAML / CLI that shaped this run | product_context, station_config, fixture_config, run_context, mock_instruments |
| Flow control | Drive the test body's iteration / synchronization | vectors, sync |
Plus one role-named fixture per instrument the station YAML declares (e.g. dmm, psu, scope). See Per-role auto-fixtures.
Every fixture above is available in every test — pytest will resolve any of them by name. The "what you'd reach for it for" column is intent, not availability. Several have meaningful "no project state" defaults (product_context returns None, instruments returns {}, connections returns None, etc.) so taking one in a vanilla project is safe.
Recording measurements #
The verbs you write into test bodies. Most tests need verify and nothing else from this group.
verify — function #
Callable: verify(name, value, limit=None, characteristic=None). Records the measurement row (value, units, limits, traceability), resolves a limit from the active chain (sidecar / inline marker / product spec), stamps measurement_outcome, and raises AssertionError when the value is out of range.
limit= accepts either a Limit model or a dict literal — verify coerces dicts via Limit.model_validate(...).
def test_rail(dmm, verify):
verify("output_voltage", dmm.measure_dc_voltage()) # limit resolves from sidecar/marker
def test_rail_inline(dmm, verify):
verify("vout", dmm.measure_dc_voltage(),
limit={"low": 3.2, "high": 3.4, "units": "V"}) # inline dict literalSame record-side effect as logger.measure; the only difference is verify raises on FAIL. Use verify when a fail should stop the line. With no resolvable limit, verify raises MissingLimitError — unless the active profile sets verify_requires_limit: false, in which case it falls back to logger.measure semantics (record-only, Outcome.DONE).
logger — session, autouse #
Yields a TestRunLogger. Autouse, so every test gets logging behind verify even when it doesn't take logger itself. Opens the event log at session start, flushes it at session end; the runs daemon materializes the per-run parquet on RunEnded.
def test_voltage(dmm, logger):
v = dmm.measure_dc_voltage()
logger.measure("output_voltage", v, limit={"low": 3.2, "high": 3.4, "units": "V"})logger.measure(name, value, *, limit=None, outcome=Outcome.DONE, allow_repeat=False) records a measurement row without raising. limit= accepts either a Limit model or a dict literal. Same recording path as verify, just no FAIL-side effect — use it when a failing measurement shouldn't abort the test (characterization mode, sweeps you want to plot post-hoc, etc.).
limits — function #
Read-only name → Limit mapping for the current test, resolved from the same chain as verify. Use for ad-hoc pythonic assertions:
def test_inline_check(dmm, limits):
v = dmm.measure_dc_voltage()
assert v in limits["output_voltage"]limits[name] raises KeyError when no limit is configured — there is no silent default.
prompt — function #
Returns a callable that resolves operator prompts declared via @pytest.mark.litmus_prompts:
@pytest.mark.litmus_prompts(
inspect={"message": "Verify LED is GREEN", "prompt_type": "confirm"},
)
def test_visual(prompt, verify):
prompt("inspect") # blocks until operator responds
verify("led_state", read_led_color())See litmus_prompts for the marker shape.
Talking to instruments #
These fixtures need a station YAML to produce useful results. Without one they return empty dicts / None.
instruments — session #
Yields dict[role_name, driver_instance]. Connects every instrument declared in the station YAML at session start, disconnects at session end. Auto-mocks when --mock-instruments is on. Identity and calibration are checked against config for real hardware.
def test_voltage(instruments):
dmm = instruments["dmm"]
assert dmm.measure_dc_voltage() > 3.0In most tests you take role names directly as fixtures (def test_x(dmm, psu)) — see Per-role auto-fixtures — and never need instruments itself.
instrument — function #
Returns an InstrumentAccessor for role-keyed access with grouping:
def test_one(instrument):
dmm = instrument("dmm")
def test_all(instrument):
dmms = instrument.by_type("pymeasure.instruments.keithley.Keithley2000")instrument_records — session #
Returns dict[role_name, InstrumentRecord] — the resolved instrument metadata (driver class, resource string, calibration cert, mocked flag) before connection. Useful for tests that need identity or calibration info without taking the live driver.
dut — session #
Yields the connected DUT driver (resolved from Product.driver + FixtureConfig.dut_resource), or None when the product has no driver. Mocked when --mock-instruments is on.
def test_firmware(dut):
assert dut.get_version().startswith("2.")pins — session #
Returns a PinAccessor for UUT-centric pin access. Looks up the instrument that the fixture YAML maps to each DUT pin, transparently activates the route if any switch is in the path.
def test_output(pins):
pins["VIN"].set_voltage(5.0)
pins["VIN"].enable_output()
assert pins["VOUT"].measure_voltage() > 3.0Raises pytest.UsageError if no fixture config or instruments are loaded.
routes — function #
Yields a RouteManager for explicit switch routing, or None when no routes exist:
def test_vout(dmm, routes):
with routes.for_pin("VOUT"):
v = dmm.measure_voltage()routes.deactivate_all() runs automatically at test teardown.
fixture_manager — session #
Returns the FixtureManager directly, for the rare test that needs advanced lookup (e.g. net-name → connection) beyond what pins exposes:
def test_lookup(fixture_manager):
conn = fixture_manager.get_connection_for_net("VOUT_3V3")
inst = fixture_manager.get_instrument_for_connection(conn.name)Reading per-test state #
The active vector's params, observations, and currently-bound connection.
context — function #
Returns a Context exposing the run / DUT / station / vector state for the active test. Resolves on every test, with empty defaults when there's nothing to expose.
| Method | Returns | Purpose |
|---|---|---|
context.get_param(name, default=None) | Any | Read a sweep / parametrize value. |
context.params | dict | All active params for this row. |
context.changed(key) | bool | True if key differs from prior iteration. |
context.last(key, default=None) | Any | Prior iteration's value for key. |
context.observe(key, value) | None | Record a free-form observation. |
context.observations | dict | All recorded observations. |
context.product | ProductContext | None | Active product context (= product_context fixture). |
context.station | StationConfig | None | Active station config (= station_config fixture). |
context.run | TestRun | None | The current TestRun. |
context.limits | LimitsView | Read-only limits mapping (= limits fixture). |
context.characteristics | tuple[str, ...] | Active characteristic IDs from litmus_characteristics. |
def test_rail(context, psu, dmm, verify):
psu.set_voltage(context.get_param("vin", 5.0))
verify("vout", dmm.measure_dc_voltage())connections — function #
Returns the ConnectionIterator resolved from litmus_characteristics / litmus_connections markers, or None when no markers are declared.
def test_per_pin(connections, dmm):
for conn in connections:
v = dmm.measure_voltage()Reading loaded configuration #
Typed accessors over the YAML / CLI that shaped this run. Each one resolves to its model OR None (or an empty dict / bool) — taking one in a vanilla project is safe.
product_context — session #
Returns a ProductContext loaded from products/*.yaml, or None if no products/ directory or no match.
Resolution chain (first match wins):
--product <id-or-path>—<id>looks upproducts/<id>.yaml;<path>is used directly.--dut-part-number <pn>— content match againstproduct.part_number:acrossproducts/*.yaml.- Single-file fallback when
products/holds exactly one product file. None.
def test_spec(product_context, dmm, verify):
if product_context:
limit = product_context.get_limit("output_voltage", temperature=25)
verify("output_voltage", dmm.measure_dc_voltage())station_config — session #
Returns the StationConfig resolved from --station / stations/*.yaml, or None. Also publishes the value to the active-station ContextVar so context.station works without taking the fixture.
fixture_config — session #
Returns the FixtureConfig resolved from --fixture / fixtures/*.yaml, or None. In worker mode (multi-slot), extracts just this slot's connections and dut_resource.
run_context — session #
Returns the RunContext carried on the active TestRunLogger. Use it to attach run-level metadata that persists across tests:
def test_setup(run_context):
run_context.set("operator_badge", "EMP-12345")
run_context.set("fixture_serial", "FIX-001")For per-test or per-vector state, use context instead.
mock_instruments — session #
Returns bool. True when --mock-instruments was passed or LITMUS_MOCK_INSTRUMENTS=1 is set. The same flag drives the instruments fixture's behavior; tests rarely take it directly except for diagnostic branches.
Flow control #
Two fixtures that drive the test body's iteration shape, not just expose data. vectors collapses pytest's per-row case multiplication into one in-body loop; sync blocks the body until peer workers reach the same named point.
vectors — function #
Taking vectors in the test signature switches collection to self-loop mode: every source of vectors (@pytest.mark.parametrize, litmus_sweeps, sidecar sweeps:, profile overrides) is consolidated into one matrix at collection time, and the test runs as a single pytest case. The test body iterates the matrix itself:
def test_sweep(vectors, psu, dmm, logger):
for v in vectors:
psu.set_voltage(v["vin"])
logger.measure("vout", dmm.measure_dc_voltage())Each for iteration pushes the row's params + index into active state so logger.measure, verify, and context see the same row-scoped context they would in normal (one-case-per-row) mode. The fixture fails the test at teardown if the matrix is non-empty but the body iterated zero times.
Choose self-loop mode when an outer setup (thermal soak, supply ramp) shouldn't repeat per row; choose normal parametrize mode when you want pytest to report one case per row.
sync — session #
Yields a SyncPoint for multi-DUT coordination when running in worker mode (_LITMUS_SLOT_ID is set), or None in single-slot mode. sync.wait(name, timeout=...) blocks until every slot reaches the same name:
def test_measure_hot(dmm, sync):
if sync:
sync.wait("thermal_soak", timeout=300)
v = dmm.measure_voltage()Per-role auto-fixtures #
When the plugin finds a station YAML at pytest_configure, it dynamically registers one session-scoped fixture per instruments: role. A station YAML like
instruments:
dmm: keithley_dmm_001
psu: keysight_psu_002
scope: tek_dpo_003exposes dmm, psu, and scope as fixtures, each returning the connected driver for that role:
def test_rail(dmm, psu, verify):
psu.set_voltage(5.0)
verify("vout", dmm.measure_dc_voltage())These names are not hard-coded — they come from your station YAML at session start. Source: src/litmus/pytest_plugin/hooks.py:232–274.
See also #
- Litmus markers — the seven
@pytest.mark.litmus_*decorators and their sidecar equivalents - pytest-native reference — how the bundled plugin uses pytest's own collection / fixtures / markers
- Models —
Limit,MeasurementLimitConfig,ProductContext,StationConfig,FixtureConfigfield shapes - Test vectors & sweeps —
litmus_sweeps,parametrize, and thevectorsself-loop fixture - Spec-driven testing —
litmus_characteristics+connectionsworkflow