Metadata-Version: 2.4
Name: litmus-test
Version: 0.1.0
Summary: Hardware test platform for the AI-assisted era
Project-URL: Repository, https://github.com/pragmatest-dev/litmus
Project-URL: Issues, https://github.com/pragmatest-dev/litmus/issues
Author: Pragmatest
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: hardware-test,instrumentation,mcp,openhtf,pytest,pyvisa,scpi,test-framework
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Manufacturing
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Requires-Dist: click>=8.1
Requires-Dist: duckdb>=1.5.0
Requires-Dist: fastapi>=0.110
Requires-Dist: fastmcp>=2.0
Requires-Dist: filelock>=3.12
Requires-Dist: httpx>=0.28.1
Requires-Dist: jinja2>=3.1
Requires-Dist: nicegui>=2.0
Requires-Dist: numpy>=1.24
Requires-Dist: orjson>=3.9
Requires-Dist: platformdirs>=4.0
Requires-Dist: pyarrow>=14.0
Requires-Dist: pydantic>=2.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: pytz>=2025.2
Requires-Dist: pyvisa-sim>=0.6
Requires-Dist: pyvisa>=1.14
Requires-Dist: pyyaml>=6.0
Requires-Dist: ruamel-yaml>=0.19.1
Requires-Dist: uvicorn[standard]>=0.27
Provides-Extra: all-exporters
Requires-Dist: asammdf>=8.0; extra == 'all-exporters'
Requires-Dist: h5py>=3.0; extra == 'all-exporters'
Requires-Dist: nptdms>=1.0; extra == 'all-exporters'
Requires-Dist: semi-ate-stdf>=0.1; extra == 'all-exporters'
Provides-Extra: grafana
Requires-Dist: buenavista>=0.5.0; extra == 'grafana'
Requires-Dist: duckdb>=1.0; extra == 'grafana'
Provides-Extra: hdf5
Requires-Dist: h5py>=3.0; extra == 'hdf5'
Provides-Extra: lxi
Requires-Dist: zeroconf>=0.80; extra == 'lxi'
Provides-Extra: mdf4
Requires-Dist: asammdf>=8.0; extra == 'mdf4'
Provides-Extra: ni
Requires-Dist: nidaqmx>=0.9; extra == 'ni'
Provides-Extra: pdf
Requires-Dist: weasyprint>=62.0; extra == 'pdf'
Provides-Extra: pymeasure
Requires-Dist: pymeasure>=0.14; extra == 'pymeasure'
Provides-Extra: sbom
Requires-Dist: cyclonedx-python-lib>=11.7; extra == 'sbom'
Provides-Extra: stdf
Requires-Dist: semi-ate-stdf>=0.1; extra == 'stdf'
Provides-Extra: tdms
Requires-Dist: nptdms>=1.0; extra == 'tdms'
Description-Content-Type: text/markdown

# Litmus

[![PyPI](https://img.shields.io/pypi/v/litmus-test.svg)](https://pypi.org/project/litmus-test/)
[![Python](https://img.shields.io/pypi/pyversions/litmus-test.svg)](https://pypi.org/project/litmus-test/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](./LICENSE)
[![CI](https://github.com/pragmatest-dev/litmus/actions/workflows/ci.yml/badge.svg)](https://github.com/pragmatest-dev/litmus/actions/workflows/ci.yml)

**Python hardware test platform for electronics production and validation.**

Litmus handles the parts of hardware testing that aren't your test: instrument management, result storage, limit checking, traceability, operator UI. You write pytest functions for your specific hardware. Everything else is config files and convention.

New products start fast. Results are consistent across product lines. Every measurement records which instrument took it.

```python
def test_rail_3v3(context, psu, dmm, verify):
    """Verify 3.3V output under load."""
    psu.set_voltage(context.get_param("vin"))
    psu.enable_output()
    verify("rail_3v3", float(dmm.measure_dc_voltage()))
```

```yaml
# config.yaml — limits and vectors easily modified without changing code
test_rail_3v3:
  vectors:
    expand: product
    vin: [3.3, 5.0, 12.0]
    load: [0.1, 0.5, 1.0]
  limits:
    test_rail_3v3:
      ref: products.power_board.rail_3v3
      guardband_pct: 10
```

Nine test vectors. Limits from your product spec with 10% guardband. Every measurement logged with instrument serial number, cal due date, and firmware version. If a unit fails in the field, you can trace back to the exact instrument and cal state that tested it.

## What you get

- **Instrument fixtures from station config** — Define roles once (`dmm`, `psu`, `eload`). They become pytest fixtures. No conftest.py boilerplate. Swap benches with `--station=bench_2`.
- **Develop without hardware** — `pytest --mock-instruments` returns configurable values per vector. Write and debug at your desk, plug in real instruments at the bench.
- **Limits from product specs** — Define specs once (nominal + tolerance), derive limits with optional guardbanding. Spec changes propagate everywhere.
- **Per-step instrument traceability** — Every result row records which instrument (serial, cal date, firmware) took that measurement. Not per-run — per-step.
- **Operator UI** — `litmus serve` gives operators a browser UI to pick sequences, enter serial numbers, and watch results. No CLI knowledge needed.
- **Capability matching** — Describe what signals your product needs. Litmus tells you which instruments in your catalog cover them.

## Quick start

```bash
pip install litmus-test            # or: uv add litmus-test
litmus init quick_start --starter && cd quick_start
pytest                             # runs with mock instruments out of the box
```

Prefer working from source:

```bash
git clone https://github.com/pragmatest-dev/litmus.git
cd litmus && uv sync
```

That's it. You have a working project with example tests, a station config, and mock instruments.

### What `--starter` generated

```yaml
# stations/starter_station.yaml — mock instruments for getting started
id: starter_station
name: Starter Station
instruments:
  psu:
    type: psu
    resource: "TCPIP::192.168.1.100::INSTR"
    mock: true
    mock_config:
      set_voltage: null
      enable_output: null
      measure_voltage: 5.0
  dmm:
    type: dmm
    resource: "TCPIP::192.168.1.101::INSTR"
    mock: true
    mock_config:
      measure_dc_voltage: 3.3
```

```python
# tests/test_example.py
def test_output_voltage(context, psu, dmm, verify):
    """Verify output voltage is within spec."""
    vin = context.get_param("vin", 5.0)
    psu.set_voltage(vin)
    psu.enable_output()
    verify("output_voltage", float(dmm.measure_dc_voltage()))
```

`psu` and `dmm` come from your station config. `context` and `verify`
come from the Litmus pytest plugin. No conftest.py needed.

### Next steps

Ready for real hardware? See [From Mocks to Hardware](docs/tutorial/from-mocks-to-hardware.md).

```bash
litmus discover                 # scan for real instruments
litmus station init             # assign roles interactively
litmus new-test output_voltage  # scaffold a new test
pytest --mock-instruments       # develop without hardware
pytest --station=my_bench       # run against real instruments
litmus runs                     # see results
```

### What results look like

Every measurement row in Parquet:

| Column | Example |
|---|---|
| `step_name` | `test_output_voltage` |
| `value` | `5.017` |
| `units` | `V` |
| `limit_low` / `limit_high` | `4.5` / `5.5` |
| `pass_fail` | `PASS` |
| `vin` | `12.0` |
| `instr_serial` | `["MY12345678"]` |
| `instr_cal_due` | `["2026-08-15"]` |
| `dut_serial` | `UNIT042` |

Open in pandas, DuckDB, or anything that reads Parquet.

## Project layout

```
products/*.yaml           → Product characteristics and tolerances
catalog/*.yaml            → Instrument capabilities and accuracy
stations/*.yaml           → Which instruments are at this bench
fixtures/*.yaml           → How DUT pins connect to instruments
sequences/*.yaml          → What to test and in what order
tests/*.py                → Test code
results/*.parquet         → Measurements with full traceability
```

Everything is files. That means it goes in git. You get diffs on limit changes, code review on test sequences, and a history of every config change.

## `verify()` vs plain `assert`

Plain `assert` for pass/fail checks:

```python
def test_power_on(psu):
    psu.enable_output()
    assert psu.get_status() == "ON"
```

`verify()` when you need recorded measurements:

```python
def test_rail_3v3(context, psu, dmm, verify):
    psu.set_voltage(context.get_param("vin"))
    psu.enable_output()
    verify("rail_3v3", float(dmm.measure_dc_voltage()))
    # → limit-checked against sidecar / product spec
    # → logged to Parquet with instrument identity
```

Vectors (`@pytest.mark.parametrize` or sidecar `vectors:`), limits,
mocks, and retries are all driven by the pytest plugin and the sidecar
YAML next to the test — no decorator needed.

## Capability matching

"We're bringing up a new board — do we have the instruments to test it?"

```python
litmus_match(requirements=[
    {"function": "dc_voltage", "direction": "input", "range_max": 50, "units": "V"},
    {"function": "dc_current", "direction": "output", "range_max": 3, "units": "A"},
])
# → Keysight 34461A covers dc_voltage input
# → Keysight E36312A covers dc_current output
```

Works from the CLI, MCP tools, or HTTP API.

## AI integration

Litmus exposes your test system as MCP tools. Optional, not a dependency.

```bash
litmus setup claude-code    # Add to Claude Code
litmus mcp serve            # Any MCP client
```

An agent can read a datasheet, extract specs, recommend instruments, generate configs, write tests, and run them — all through tool calls.

Convention-driven frameworks also produce better LLM output. When the pattern is always "return a measurement, limits come from config, instruments are fixtures," there's less room for the model to improvise poorly.

## Compared to alternatives

| | **TestStand** | **OpenHTF** | **In-house scripts** | **Litmus** |
|---|---|---|---|---|
| Language | LabVIEW/C# | Python | Varies | Python (pytest) |
| Config | Proprietary | Code | Scattered | Declarative files |
| License | $$$ | Free | — | Free |
| Instrument mgmt | Built-in | None | Manual | Config + catalog |
| Mock mode | Limited | Manual | Manual | `--mock-instruments` |
| Results | Proprietary | Protobuf | CSV/Excel | Parquet |
| AI tooling | No | No | No | MCP |
| Learning curve | Steep | Moderate | None (you wrote it) | pytest |

Closest to OpenHTF in spirit. pytest instead of a custom executor, config files instead of Python objects, Parquet instead of Protobuf.

## CLI

```bash
litmus init <name> [--starter]  # New project (--starter for full example)
litmus discover [--visa]        # Scan for instruments
litmus station init             # Interactive station setup
litmus new-test <name>          # Scaffold a test file
litmus serve [--reload]         # Operator UI
litmus runs / show <id>         # Results
litmus instrument list / show   # Instrument inventory
litmus mcp serve                # MCP server
litmus setup <tool>             # AI tool integration
```

## Docs

- [Quick start](./docs/quickstart.md) — First project in 5 minutes
- [Architecture overview](./docs/architecture-erd.md) — How things connect
- [docs/](./docs/) — Guides, tutorial, reference

## License

Apache 2.0
