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 / measure are 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 #

ConceptWhat It IsExample
PartSpec defining what you're testingTPS54302 DC-DC converter
CharacteristicMeasurable property of partoutput_voltage: 3.3V ±5%
StationPhysical test bench with instrumentsBench 1 with DMM, PSU, ELoad
CapabilityWhat an instrument can doDMM: measure DC voltage
SidecarYAML alongside a test file declaring limits, sweeps, mocks, retry, promptstests/test_power.yaml
TestRunOne execution of a test fileRun abc123 on SN001
MeasurementSingle data point with pass/failVOUT = 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 --> MD

Entity 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 #

ConceptType (YAML Definition)Instance (Runtime)
What to testPartUUT
Where to testStationTypeStationConfig
What to runSidecarConfig (file scope) + pytest collectionTestRun
Single iterationTestEntry (per-method scope)TestVector
Expected valueLimit / SpecBandMeasurement

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 --> R

Part-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" --> SI

3. 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 --> PQ

File Locations #

EntityLocation
Part specsparts/*.yaml
Station configsstations/*.yaml
Test codetests/test_*.py
Test sidecarstests/test_*.yaml
Fixturesfixtures/*.yaml
Instrument catalogcatalog/**/*.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:

StorePurposeFormat
EventStoreAll test activity as typed eventsArrow files (DuckDB-queryable)
ChannelStoreTime-series instrument dataArrow segments
FileStoreCaptured artifacts (images, video, vendor files)files + index
RunStoreFlat test results, one row per measurementParquet 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: