Litmus Architecture #
How Litmus Works #
Vocabulary primer. This page packs several concepts into one diagram. If you haven't seen them yet: part and station are YAML definitions; sidecar is the per-test YAML carrying limits / sweeps / mocks;
verify/context/measureare three of the pytest fixtures Litmus adds — the common per-test entry points (see reference/litmus-fixtures); characteristic is a measurable property on a part; capability is what an instrument can do.
flowchart LR
subgraph Inputs
P[Part spec<br/>parts/*.yaml<br/>pins, chars, bands]
S[Station YAML<br/>stations/*.yaml<br/>instruments, resources]
SC[Sidecar YAML<br/>tests/test_*.yaml<br/>limits, sweeps, mocks, retry, prompts]
T[Test code<br/>tests/test_*.py<br/>verify / context / measure]
end
subgraph Plugin[Litmus pytest plugin]
L[Load specs] --> EX[Expand vectors]
EX --> RUN[Run test code]
RUN --> CHK[Check limits]
end
P --> Plugin
S --> Plugin
SC --> Plugin
T --> Plugin
Plugin --> O[event log<br/>→ results/*.parquet]
O --> A[CLI / UI / Python API / MCP tools]Key Concepts #
| Concept | What It Is | Example |
|---|---|---|
| Part | Spec defining what you're testing | TPS54302 DC-DC converter |
| Characteristic | Measurable property of part | output_voltage: 3.3V ±5% |
| Station | Physical test bench with instruments | Bench 1 with DMM, PSU, ELoad |
| Capability | What an instrument can do | DMM: measure DC voltage |
| Sidecar | YAML alongside a test file declaring limits, sweeps, mocks, retry, prompts | tests/test_power.yaml |
| TestRun | One execution of a test file | Run abc123 on SN001 |
| Measurement | Single data point with pass/fail | VOUT = 3.31V PASS |
System Overview #
flowchart LR
subgraph Definitions["DEFINITIONS (YAML)"]
PS["Part spec<br/>parts/*.yaml"]
ST["Station type<br/>stations/*.yaml"]
TC["Test code + sidecar<br/>tests/test_*.py + .yaml"]
end
subgraph Runtime["RUNTIME"]
UUT["UUT<br/>(serial)"]
SI["Station instance"]
TR["Test run"]
end
subgraph Storage["STORAGE"]
TRR["TestRun results"]
MD["Measurement data"]
end
PS -- "instantiated as" --> UUT
UUT -- "tested in" --> TRR
ST -- "deployed as" --> SI
SI -- "produces" --> MD
TC -- "executed as" --> TR
TR --> TRR
TR --> MDEntity Relationships #
The platform's data model covers three concerns: what you're testing (parts and their specs), how you test it (stations, fixtures, capabilities), and what gets executed and recorded (sidecar configuration and runs). Each diagram below covers one concern. For the full per-model schema with every field, see reference/models and reference/catalog-schema. Click any diagram to expand.
1. Parts & Specs #
What the UUT is, what its measurable characteristics are, and how spec bands attach.
erDiagram
Part {
id string PK
name string
revision string
description string
}
Pin {
name string PK
net string
role string
description string
}
Characteristic {
name string PK
direction enum
function enum
unit string
signals dict
conditions dict
controls dict
attributes dict
}
SpecBand {
when dict
value float
accuracy AccuracySpec
resolution ResolutionSpec
}
Part ||--o{ Pin : "pins[]"
Part ||--o{ Characteristic : "characteristics[]"
Characteristic ||--o{ SpecBand : "bands[]"2. Stations, Fixtures & Capability Matching #
The bench side: physical stations, the instruments they hold, the capabilities those instruments expose, and the optional fixture layer that routes instrument channels to UUT pins.
erDiagram
StationType {
id string PK
description string
}
Station {
id string PK
station_type string FK
location string
}
StationInstrumentConfig {
type string
driver string
resource string
catalog_ref string
mock bool
channels dict
mock_config dict
}
Capability {
function enum
direction enum
signals dict
conditions dict
controls dict
attributes dict
}
Fixture {
id string PK
part_id string FK
}
FixtureConnection {
name string PK
instrument string FK
instrument_channel string
instrument_terminal string
uut_pin string FK
net string
function string
route SwitchRoute
}
Characteristic {
name string PK
direction enum
function enum
}
Pin {
name string PK
net string
role string
}
StationType ||--o{ Station : "deployed as"
Station ||--o{ StationInstrumentConfig : "instruments{}"
StationInstrumentConfig ||--o{ Capability : "capabilities[]"
Fixture ||--o{ FixtureConnection : "connections[]"
FixtureConnection }o--|| Pin : "uut_pin →"
FixtureConnection }o--|| StationInstrumentConfig : "instrument →"
Characteristic ||--|| Capability : "matches (direction-flipped)"3. Test Configuration & Execution #
The sidecar YAML tree on the left, the runtime objects it produces on the right. TestEntry is a recursive node — file-scope, class-scope, and method-scope all share the same shape.
erDiagram
SidecarConfig {
limits dict
sweeps list
mocks list
characteristics list
connections any
retry RetryConfig
prompts dict
tests dict
}
TestEntry {
limits dict
sweeps list
mocks list
characteristics list
connections any
retry RetryConfig
prompts dict
runner string
tests dict
}
UUT {
serial string PK
part_number string
}
TestRun {
id uuid PK
started_at datetime
uut_serial string FK
station_id string FK
outcome enum
}
TestVector {
id uuid PK
index int
params dict
outcome enum
}
Measurement {
name string
value float
unit string
outcome enum
}
Part {
id string PK
}
Station {
id string PK
}
SidecarConfig ||--o{ TestEntry : "tests{}"
UUT }o--|| Part : "instance of"
TestRun }o--|| UUT : "for UUT"
TestRun }o--|| Station : "on station"
TestRun ||--o{ TestVector : "vectors[]"
TestVector ||--o{ Measurement : "measurements[]"Type vs Instance #
| Concept | Type (YAML Definition) | Instance (Runtime) |
|---|---|---|
| What to test | Part | UUT |
| Where to test | StationType | StationConfig |
| What to run | SidecarConfig (file scope) + pytest collection | TestRun |
| Single iteration | TestEntry (per-method scope) | TestVector |
| Expected value | Limit / SpecBand | Measurement |
Core Flows #
1. Spec → Config → Test Flow #
Limits can come from three places — part spec, sidecar override, or inline in the test:
flowchart LR
A["Part spec<br/>parts/*.yaml<br/>characteristic.bands"]
B["Sidecar override<br/>tests/test_*.yaml<br/>limits: {name: {...}}"]
C["Inline limit<br/>measure(name, v, limit=Limit(...))"]
R["Limit resolution<br/>(per measurement)"]
A --> R
B --> R
C --> RPart-spec bands derive a production limit by applying any configured guardband (tightening the spec for manufacturing margin). For example: 3.3V ± 5% (3.135–3.465) with a 10% guardband becomes 3.152–3.449.
Full flow with conditions:
flowchart LR
PS["Part spec<br/>parts/tps54302.yaml<br/>characteristics.output_voltage.bands<br/>(N bands keyed by when:)"]
SC["Sidecar<br/>tests/test_*.yaml<br/>sweeps: [{temp:[25,85], load:[.5,3]}]<br/>characteristics: [output_voltage]"]
TC["Test code<br/>tests/test_*.py<br/>verify('output_voltage', dmm.measure())"]
subgraph Runtime["Runtime (per vector)"]
V["Vector params<br/>{temp:25, load:0.5} ..."]
CR["Resolve limit<br/>(match the spec band for temp + load)"]
VR["verify / measure<br/>checks + records measurement row"]
end
PS -- "matched per vector" --> CR
SC -- "drives sweep" --> V
TC -- "calls verify" --> VR
V --> CR
CR --> VR
VR --> M["Measurement row<br/>(parquet)"]2. Capability Matching #
flowchart LR
PC["Part characteristic<br/>direction: OUTPUT<br/>function: dc_voltage<br/>(UUT outputs voltage)"]
REQ["Required capability<br/>direction: INPUT<br/>function: dc_voltage<br/>(need to measure)"]
SI["Station instrument<br/>provides: INPUT<br/>function: dc_voltage<br/>(DMM can measure)"]
PC -- "direction-flip" --> REQ
REQ -- "matches" --> SI3. Test Execution #
flowchart LR
SC["SidecarConfig + test code<br/>(definition)"]
TR["TestRun<br/>(instance)"]
TV["TestVector<br/>(iteration)"]
M["Measurement<br/>(data point)"]
PQ["Parquet<br/>(storage)"]
SC --> TR --> TV --> M --> PQFile Locations #
| Entity | Location |
|---|---|
| Part specs | parts/*.yaml |
| Station configs | stations/*.yaml |
| Test code | tests/test_*.py |
| Test sidecars | tests/test_*.yaml |
| Fixtures | fixtures/*.yaml |
| Instrument catalog | catalog/**/*.yaml |
| Test results (Parquet) | <data_dir>/runs/{date}/*.parquet |
| Event logs (Arrow IPC) | <data_dir>/events/{date}/{session_id}-{pid}.arrow |
| Channel data (Arrow IPC) | <data_dir>/channels/{date}/{channel}_{session}.arrow |
Data Architecture #
The storage layer uses four complementary stores:
| Store | Purpose | Format |
|---|---|---|
| EventStore | All test activity as typed events | Arrow files (DuckDB-queryable) |
| ChannelStore | Time-series instrument data | Arrow segments |
| FileStore | Captured artifacts (images, video, vendor files) | files + index |
| RunStore | Flat test results, one row per measurement | Parquet files |
Events are the source of truth. The parquet run rows are built from the event stream by the runs daemon when a run ends. See Data stores and Event Log Architecture for details.
See also #
Related quadrants:
- How-to → Overview — how-to entry point for this category
- Reference → Overview — reference entry point for this category
- Integration — integration entry point for this category
- Tutorial — tutorial entry point for this category