BKPT LabsDOCS/VIEWALYZER SDK/TESTS AND CI VIEWALYZER · PYTHON SDK
VIEWALYZER · PYTHON SDK

Tests and CI

A trace is a measurement, so it can be asserted on. The SDK turns a capture into a pytest fixture and the recording's statistics into plain assertions; fingerprints and compare give you a regression gate without hand-picked thresholds.

A fixture per capture

# conftest.py
import pytest
from viewalyzer_sdk import ViewAlyzer

PROBE = "0033004B3033510735393935"

@pytest.fixture(scope="session")
def va():
    return ViewAlyzer()                                   # VIEWALYZER env, then PATH, then the install

@pytest.fixture(scope="session")
def rec(va, tmp_path_factory):
    out = tmp_path_factory.mktemp("trace") / "run.vadb"
    return va.record({"transport": "stlink-rambuf", "target-device": "STM32G474RE", "stlink-serial": PROBE},
                     output=out, duration_s=10, elf="build/rambuf/firmware.elf",
                     extra_flags=["--no-register"])
# test_timing.py
def test_capture_is_a_measurement(rec):
    assert rec.is_clean                                   # no lost events, no corrupt bytes, no sequence gaps
    assert rec.info["duration_us"] > 9_000_000

def test_systick_budget(rec):
    lanes = {t["name"]: t for t in rec.timeline()["tasks"]}
    assert lanes["ISR:SysTick"]["p99_slice_us"] < 5.0
    assert lanes["ISR:SysTick"]["max_jitter_us"] < 10.0

def test_no_priority_inversion(rec):
    assert rec.inversions()["inversions"] == []

def test_sine_channel(rec):
    c = rec.user_traces()
    sine = next(v for k, v in c["channels"].items() if c["trace_legend"][k]["name"] == "Sine Wave")
    assert 400 < sine["rate_hz"] < 600
    assert sine["min"] >= -100 and sine["max"] <= 100

is_clean first, always: a capture that lost events is not a measurement of the firmware. extra_flags=["--no-register"] keeps the runner's recording index from filling up; pin the probe serial on any machine that can have two probes.

Fingerprints as the baseline

def test_against_baseline(rec):
    res = rec.compare("ci/app.vafp.json")
    assert res["verdict"] in ("pass", "warn"), [r for r in res["results"] if r["status"] in ("fail", "missing")]

Make the baseline once from a known-good run (or several, to learn the envelope) and commit it:

good = va.open("good1.vadb")
good.fingerprint(runs=["good2.vadb", "good3.vadb"], out="ci/app.vafp.json")

The sections, tolerances and the file format are on the CLI's CI page; the file is hand-editable.

Hardware trace in a test

def test_isr_load_and_console(va, tmp_path):
    rec = va.record("nucleo_g474_swo.vacf", output=tmp_path / "hw.vadb", duration_s=10, elf="firmware.elf",
                    extra_flags=["--dwt", "--dwt-exc", "--dwt-pc", "16384"])
    exc = {e["name"]: e for e in rec.dwt_exc()["data"]["exceptions"]}
    assert 9_500 <= exc["SysTick"]["enter"] <= 10_500           # 1 kHz tick over 10 s
    assert rec.swo_load()["data"]["overflows"] == 0
    lines = [l["text"] for p in rec.itm_console(port=0)["data"]["ports"] for l in p["lines"]]
    assert any("boot ok" in l for l in lines)

Preflight and skipping

import pytest
from viewalyzer_sdk import find_viewalyzer, ViewAlyzer

pytestmark = pytest.mark.skipif(find_viewalyzer() is None, reason="no viewalyzer-cli on this machine")

def test_probe_present():
    probes = ViewAlyzer().list_probes()["probes"]
    if not any(p["serial"] == PROBE for p in probes):
        pytest.skip("bench probe not attached")

viewalyzer-doctor from a shell does the same check for a job step (exit 2: no binary; exit 3: the environment check failed).

Without hardware

For the code around the trace (parsers, report generators) run against a saved recording: ViewAlyzer().open("fixtures/good.vadb") gives the same Recording, every query works, and connect() opens the SQLite file read-only. The SDK's own test suite runs on a fake CLI (tests/fake_viewalyzer.py) that answers every verb with the real shapes, and a real-binary suite gated on VIEWALYZER_SDK_REAL_CLI=<path> (with VIEWALYZER_SDK_REAL_CONFIG and VIEWALYZER_SDK_REAL_ELF for the capture round trip), which is what runs on the bench.

A job

jobs:
  trace:
    runs-on: [self-hosted, bench-g474]
    steps:
      - uses: actions/checkout@v4
      - run: pip install viewalyzer-sdk pytest
      - run: viewalyzer-doctor
      - run: python build.py --flash --serial ${{ vars.PROBE_SERIAL }}
      - run: pytest tests/trace -q --junitxml=trace.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: trace, path: "trace.xml" }