How-ToSDKMade with Zelos app 26.0.9 · SDK 0.0.12

Run bench tests with pytest and open the failure

Check live signals from pytest, read which sample broke the rule, and open the failing test's trace in the app.

Michael Jaradah3 minute read
Eight trace files, one per test, each a cell voltage over a dashed 3.0 V floor; file [3] is violet, and its line drops below the floor. The run reads 1 failed, 7 passed.
Contents7 sections

A failed bench test gives you a message, and you also want the data behind it. This guide checks live signals from pytest, saves one trace per test, and opens the failing test's trace in the app so you can see what the signal did.

This guide runs on simulated data. Install the CLI shows how to start it.

How it fits together

The agent is the program that collects your bench's signals. The Python SDK adds a pytest plugin with a check fixture. Each check sends a rule to the agent, which judges it against the signals it holds. After each test, a short fixture asks the agent to save that test's data as a trace. The failing test's trace opens in the app.

A DEMO tile for zelos live demo sends signals to the agent. pytest sends checks to the agent and gets verdicts back. The agent writes one trace per test, and the failing test's trace opens in the app.
What runs where. pytest sends the checks, and the agent judges them and records the traces.

A simulated battery pack stands in for hardware. On a real bench, an extension or your own code feeds the agent instead.

Before you start

  • The Zelos app.
  • uv.

Step 1: Write the test and run it

Make a project and add the SDK with its test extra, which brings pytest and rich. The plugin loads by itself.

uv init bench
cd bench
uv add "zelos-sdk[test]"

The test checks that every cell stays above 3.0 V.

test_pack.py
import pytest

@pytest.mark.parametrize("cell", range(8))
def test_cell_above_floor(agent, check, cell):
    voltage = agent.signal(f"bus0/BMS_message/cells.cell_{cell}")
    check.that(voltage, ">", 3.0, last="60s")

last="60s" makes the rule hold for every sample in the last 60 s. Eight parameters make eight tests. In this example, cell_3 dips under 3.0 V near the end of every minute.

Run uv run pytest in the bench folder. Seven cells pass and cell 3 fails. This output comes from a run with the step 2 files in place, cut to the lines that matter:

test_pack.py::test_cell_above_floor[2] PASSED                                                [ 37%]
test_pack.py::test_cell_above_floor[3] FAILED                                                [ 50%]
---------------------------------------- live log teardown -----------------------------------------
INFO     zelos-checker:board.py:111
                           Zelos Checkerboard
                        test_cell_above_floor[3]
┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
┃          time           ┃          check           ┃ window ┃ result ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
│ 2026-10-06 18:20:46.337 │ cells.cell_3 (2.999) > 3 │ always │ FAILED │
└─────────────────────────┴──────────────────────────┴────────┴────────┘
...
=================================== 1 failed, 7 passed in 1.37s ====================================

The table names the first sample that broke the rule: 2.999 V.

Step 2: Save a trace for every test

Two pieces save each trace. The first is pytest.ini.

pytest.ini
[pytest]
addopts = --zelos-trace --zelos-local-artifacts-dir=artifacts
log_cli = true
log_cli_level = INFO

--zelos-trace connects the run to the agent's data stream. --zelos-local-artifacts-dir keeps one results file per test in artifacts, and every check in a test runs before the test fails. log_cli prints each test's result table.

The second is a short fixture that saves each test's data from the agent as a trace. A local export needs a paid plan. Record a trace file from your own Python script covers export.

conftest.py
from datetime import datetime, timedelta, timezone
import pytest

@pytest.fixture(autouse=True)
def bench_trace(request, agent):
    start = datetime.now(timezone.utc) - timedelta(seconds=60)
    yield
    path = request.config.zelos_local_artifacts_dir / f"{request.node.name}.trz"
    agent.export(str(path), start=start, overwrite=True)

The export starts 60 s before the test, to match the check's window. After a run, artifacts holds a .trz and a .checks.json per test.

Step 3: Open the failing test's trace

On Home, choose Open trace and pick test_cell_above_floor[3].trz. With an agent connected, choose Open trace, then File, in Data on the right. The trace opens as its own workspace.

Search the signal tree for cells and drag the cells folder onto the empty tab, then drag the plot's bottom corner down to give it room. Cell 3 slides below 3 V near the end of the minute. Right-click the dip, and the legend shows each cell at the cursor. Cell 3 reads 2.908 V there, and the other cells read about 3.6 V.

Plot the cells, right-click the dip.

Before you use it on your bench

  • The window needs data. A check on a signal with no data in its window fails.
  • A remote agent needs two flags: --zelos-agent-url and --zelos-trace-url.

Point it at your bench

Copy the files from zelos-examples, swap the cell paths for your own signals, and set your real limit. The testing docs list every plugin option, and the checks page covers windows and operators. To look at the data while you write the rules, turn a notebook into a pass/fail test instead.