Litmus Architecture #

How the Framework Works #

Vocabulary primer. This page drops a lot of names into one diagram. If you haven't seen them yet: product and station are YAML definitions; sidecar is the per-test YAML carrying limits / sweeps / mocks; verify / context / logger are three of the 20 pytest fixtures Litmus adds — the common per-test entry points (see reference/litmus-fixtures); characteristic is a measurable property on a product; capability is what an instrument can do.

flowchart LR
    subgraph Inputs
        P[Product spec<br/>products/*.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 / logger]
    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[results/*.parquet<br/>+ event log]
    O --> A[CLI / UI / Python API / MCP tools]

Key Concepts #

ConceptWhat It IsExample
ProductSpec defining what you're testingTPS54302 DC-DC converter
CharacteristicMeasurable property of productoutput_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["Product spec<br/>products/*.yaml"]
        ST["Station type<br/>stations/*.yaml"]
        TC["Test code + sidecar<br/>tests/test_*.py + .yaml"]
    end
 
    subgraph Runtime["RUNTIME"]
        DUT["DUT<br/>(serial)"]
        SI["Station instance"]
        TR["Test run"]
    end
 
    subgraph Storage["STORAGE"]
        TRR["TestRun results"]
        MD["Measurement data"]
    end
 
    PS -- "instantiated as" --> DUT
    DUT -- "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 splits cleanly into three concerns: what you're testing (products 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. Products & Specs #

What the DUT is, what its measurable characteristics are, and how spec bands attach.

erDiagram
    Product {
        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
        units string
        signals dict
        conditions dict
        controls dict
        attributes dict
    }
    SpecBand {
        when dict
        value float
        accuracy AccuracySpec
        resolution ResolutionSpec
    }
 
    Product ||--o{ Pin : "pins[]"
    Product ||--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 DUT 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
        product_id string FK
    }
    FixtureConnection {
        name string PK
        instrument string FK
        instrument_channel string
        instrument_terminal string
        dut_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 : "dut_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, method-scope all share the same shape; the recursion is described in the field list rather than drawn as a self-edge (Mermaid routes self-edges through neighbouring entities and the line reads as a phantom relationship).

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
    }
    DUT {
        serial string PK
        part_number string
    }
    TestRun {
        id uuid PK
        started_at datetime
        dut_serial string FK
        station_id string FK
        outcome enum
    }
    TestVector {
        id uuid PK
        index int
        params dict
        outcome enum
    }
    Measurement {
        name string
        value float
        units string
        outcome enum
    }
    Product {
        id string PK
    }
    Station {
        id string PK
    }
 
    SidecarConfig ||--o{ TestEntry : "tests{}"
    DUT }o--|| Product : "instance of"
    TestRun }o--|| DUT : "for DUT"
    TestRun }o--|| Station : "on station"
    TestRun ||--o{ TestVector : "vectors[]"
    TestVector ||--o{ Measurement : "measurements[]"

Type vs Instance #

ConceptType (YAML Definition)Instance (Runtime)
What to testProductDUT
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 — product spec, sidecar override, or inline in the test:

flowchart LR
    A["Product spec<br/>products/*.yaml<br/>characteristic.bands"]
    B["Sidecar override<br/>tests/test_*.yaml<br/>limits: {name: {...}}"]
    C["Inline limit<br/>logger.measure(name, v, limit=Limit(...))"]
    R["Limit resolution<br/>(per measurement)"]
    A --> R
    B --> R
    C --> R

Product-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["Product spec<br/>products/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/>spec.get_limit(name, when={temp, load})"]
        VR["verify / logger.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["Product characteristic<br/>direction: OUTPUT<br/>function: dc_voltage<br/>(DUT 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
Product specsproducts/*.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}.arrow
Channel data (Arrow IPC)<data_dir>/channels/{date}/{channel}_{session}.arrow

Data Architecture #

The storage layer uses three complementary stores:

StorePurposeFormat
EventStoreAll test activity as typed eventsArrow IPC + DuckDB via Flight
ChannelStoreTime-series instrument dataArrow IPC segments
ParquetBackendDenormalized test resultsParquet files

Events are the source of truth. Parquet files are a materialized view produced by materialize_run_to_parquet(), called from the runs daemon on RunEnded. See Three Stores Architecture and Event Log Architecture for details.

See also #

Related quadrants: