Xilinx Test Runner
A test runner for FPGA verification work: build the bitstream, keep the old ones, run the tests on the board, compare against the baseline.
One tool for the whole loop, with a console, a terminal interface and a window over the same run.
The window follows the desktop theme. The same run on a dark one: window-dark.png.
What it does
Verifying a design on an FPGA prototype is the same loop every day: get the current sources, build a bitstream, run tests in simulation and on the board, compare the behaviour against the documentation, fix something, then prove that nothing which used to work has broken.
Every step of that already has a tool. What is usually missing is the thing that holds them together and remembers what happened. This is that thing.
It is not a simulator, not a replacement for UVM and not a continuous integration system. It sits in front of those and keeps the results.
Installation
Python 3.10 or newer. Nothing else is required; the window needs Qt.
git clone https://github.com/shchuchkin-pkims/xilinx-test-runner
cd xilinx-test-runner
./bin/xtr --help # runs straight from the checkout
pip install . # or install it properly
pip install ".[gui]" # with PyQt6, for the window
Every release also carries a wheel and a source archive on its releases page.
python3 -m xtr works anywhere the xtr command does.
Quick start
A demo project is included. It builds a fake bitstream with a shell script, so it runs with no vendor tools installed at all:
cd examples/demo_project
xtr list # what is here
xtr run # run it in the console
xtr artifacts build # "synthesise" and store a variant
xtr run --accept # accept these results as the baseline
xtr --gui # or the window
For your own project:
cd /path/to/project
xtr init # writes xtr.toml and an example test
xtr doctor # is everything where the config says it is
xtr run
xtr --gui and xtr --tui also open with no project at all, and can point
themselves at one from the File menu.
How a test is written
A test is any executable file. Its exit code is the verdict, and a few prefixed lines carry everything else. A script that has never heard of this runner works as it is.
#!/bin/bash
# TEST: nack_timing
# GROUP: Hardware
# DESC: NACK must start inside the window the standard allows
# NEEDS: board # one board: never two of these at once
# REQUIRES: bitstream up to date # skip me if the build is stale
# RETRY: 1 # flaky connection; the report will say so
# TARGET: lab_pc # run me on the machine with the board
# COVERS: ISO7816-3 8.3
command -v xsdb >/dev/null || { echo "SUMMARY: no debugger here"; exit 2; }
echo "PROGRESS: 30 programming the device"
etu=$(measure_etu)
echo "METRIC: nack_window $etu etu tol=0.5"
echo "CHECK: nack inside the window $( [ "$etu" -le 12 ] && echo pass || echo fail )"
[ "$etu" -le 12 ] || { echo "SUMMARY: NACK at $etu etu, limit is 11.5"; exit 1; }
echo "SUMMARY: NACK at $etu etu, inside the window"
Five verdicts, because two are not enough
| Exit code | Verdict | Meaning |
|---|---|---|
0 |
PASS |
checked, behaves as expected |
1 |
FAIL |
checked, the design is wrong |
2 |
SKIP |
could not check: no board, no licence, no tool |
3 |
ERROR |
the test setup broke, not the design |
4 |
KNOWN |
a known open defect, reproducing exactly as recorded |
A missing cable must not paint the run red, and an open defect must not hide
under green. KNOWN has its own counter and does not fail the run.
Lines a test can print
| Line | Purpose |
|---|---|
SUMMARY: ... |
the one line that lands in the results table |
PROGRESS: 40 message |
moves the progress bar |
TRACE: ... |
verbose output, hidden until asked for, always kept |
METRIC: etu 66673 cycles tol=50 |
a number compared between runs |
CHECK: name pass |
a named verdict inside a test |
ARTIFACT: /path/file |
a file to keep with the result |
COVERS: ISO7816-3 8.3 |
which requirement this test checks |
The full contract is in docs/writing-tests.md.
What the runner adds
Bitstreams are objects, not a file that happens to be there. Every build is stored as a named variant together with a hash of the sources it was built from. Switching between variants is a file copy, so comparing a fix against the previous build takes seconds instead of another synthesis run. Because the provenance is a hash of the content, copying a variant around cannot make an old build look current. A check based on file dates can, and that mistake is how results from two different designs end up mixed in one report.
Regression means comparison, not a demand for perfection. Tests report measurements and named checks; the runner keeps the accepted values and reports what moved. A test that used to fail and still fails in exactly the same way is reported as a known defect rather than noise. A test that starts passing is pointed out rather than silently absorbed.
The board is usually on another machine. A test can declare where it runs, and the runner drives it over SSH. Stopping a run kills the whole remote process tree, which is what keeps a cancelled run from leaving a debugger attached and the cable locked for the next hour.
Random stimulus stays reproducible. Each run has a seed, and each test gets
a stable seed derived from it. Both are recorded, so a failure found with
random data can be replayed with --seed.
Reports carry their context. Tool versions, source revision, active bitstream, machine name, seed. A report without those cannot be interpreted a month later.
Also included: dependencies between tests, setup tests that stop a section cleanly, retries that are always visible in the report, exclusive resources that hold even between separate runs of the program, a configuration matrix with a comparison report, and reports in text, JSON, JUnit XML and HTML.
Configuring a project
One file in the project root. TOML is read by the standard library; YAML works too if PyYAML is installed.
[project]
name = "uart_7816 on EBAZ4205"
tests = "tests"
[tools.vivado]
path = "/opt/Xilinx/2025.1/Vivado/bin/vivado" # or omit it and let it be found
[targets.lab_pc] # the board is on another machine
type = "ssh"
host = "lab@10.0.0.2"
identity = "~/.ssh/id_ed25519_lab"
root = "/home/lab/project" # the same tree, over there
[artifacts.bitstream]
store = "fpga/bitstreams/variants"
active = "fpga/bitstreams"
files = ["design_1_wrapper.bit"]
extra_files = ["design_1_wrapper.xsa"]
sources = ["rtl/**/*.v"] # what the provenance hash covers
build = "bitstream"
[flows.bitstream]
plugin = "vivado"
project = "fpga/EBAZ4205/EBAZ4205.xpr"
tcl = "fpga/scripts/build.tcl"
output = "fpga/EBAZ4205/EBAZ4205.runs/impl_1/design_1_wrapper.bit"
tail = ["fpga/EBAZ4205/EBAZ4205.runs/impl_1/runme.log"]
[vcs.rtl]
path = "rtl"
From that, the runner generates the steps a project would otherwise script by hand: check whether the bitstream matches the sources, build a new one, switch between variants, show the source revision, fetch and pull it, and verify that the tools are installed where the configuration says.
Machine specific paths belong in xtr.local.toml next to it, which is not
committed. Every key is documented in
docs/configuration.md.
The interfaces
All three drive the same run and share the same state.
| Command | Interface |
|---|---|
xtr run |
console; exits non zero on failure, for scripts and pipelines |
xtr --tui |
terminal interface; needs only a terminal, works over SSH |
xtr --gui |
window: tests, artifacts, run log, history |
The window has four tabs. Tests shows the tree, the live output and the measurements next to the accepted values. Artifacts shows the stored bitstreams, which one is active and whether it still matches the sources. Run log collects everything every test printed, verbose lines included, which is where a long synthesis run is watched. History lists previous runs and how a measurement moved between them.
Command line
xtr # run everything, print a table
xtr run sim --jobs 4 # only what matches "sim", four at a time
xtr run --matrix bitstream=fix,old hw # the same tests on two builds, compared
xtr run --seed 1234 # replay a random run exactly
xtr run --accept # record these results as expected
xtr artifacts list # what is stored, what is active
xtr artifacts activate old_build
xtr history trend etu # is that measurement drifting
xtr issue # a ticket draft from the last failure
xtr doctor # check tools, targets and paths
xtr selftest # the runner's own tests
A complete example on real hardware
examples/pynq_z1 is a full project for a Digilent
PYNQ-Z1: a written specification with seven numbered requirements, the RTL, a
simulation testbench, and tests that run on the board. It is programmable logic
only, so no software has to be built for the processor and no SD card is
involved; the host talks to the design over the same JTAG cable that programs
it.
ok program 31.3 s device configured, VIO answering, counter running
ok bitstream up to date 0.0 s built from the current sources (9eda4dd157f06f4b)
ok syntax 1.0 s 2 file(s) compile clean, 0 warning(s)
ok step_counter_tb 5.1 s 16 checks, all as specified
ok lfsr 27.7 s 16 values match the model, the seed reload works
ok steps 27.7 s reset, edge stepping, up, down, hold and wrap behave as specified
Documentation
| Document | Contents |
|---|---|
| Getting started | installation, first run, adopting scripts you already have |
| Writing tests | exit codes, output protocol, header fields, environment, seeds |
| Configuration | every key, and which of the four files it belongs in |
| Artifacts | bitstream variants, provenance, the configuration matrix |
| Remote execution | SSH targets, shared trees, stopping remote processes |
| Baseline and history | how a regression is decided, when to accept, trends |
| Architecture | the three layers and how to extend them |
| Recipes | Vivado, XSim, Cadence HAL, a board over SSH, CI |
| Firmware results | self describing results from C running on the target |
Russian translations of the main documents are in docs/ru/, and the overview is in README.ru.md.
Requirements
| Python | 3.11 or newer, standard library only; on 3.10, tomli for TOML |
| Operating system | Linux (POSIX process groups and file locks) |
| Window | PyQt6 or PyQt5, optional |
| YAML configuration | PyYAML, optional; TOML needs nothing |
| Vendor tools | whatever your tests use; the runner only has to find them |
Nothing is downloaded at run time and no usage data is collected.
Design decisions
These are deliberate, and they are the reason the tool stays small.
- The core knows nothing about FPGAs. Everything vendor specific is either configuration or a test. If that stops being true, the tool is no longer portable to the next project, and a shell script would do the job instead.
- No external test manifest. Test metadata lives in the header of the test file, so adding a test means adding a file, and nothing can drift out of sync with a file that does not exist.
- Skipping is not failing. Absent hardware, an unlicensed tool and a dead connection are three different situations, and none of them is a defect in the design.
- A retry that hides instability is worse than no retry. The report always states that a test needed a second attempt.
- Accepting a baseline is a deliberate act. A baseline that updates itself compares a run against itself.
- No job queue, no agents, no scheduler. Those products exist. This one runs on the machine in front of the hardware.
Contributing
Bug reports, recipes for tools nobody has tried yet, and patches are all welcome. The suite that guards the behaviour above runs with no dependencies:
xtr selftest # 94 tests
CONTRIBUTING.md describes the layering rule and what a change is expected to come with.
License and trademarks
MIT. See LICENSE.
This project is independent and is not affiliated with, endorsed by or sponsored by AMD, Cadence, Siemens or Intel. Product names are used only to describe which third party tools the runner can drive. No vendor artwork is included: the icon and the logo are original work. See TRADEMARKS.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xilinx_test_runner-0.1.0.tar.gz.
File metadata
- Download URL: xilinx_test_runner-0.1.0.tar.gz
- Upload date:
- Size: 100.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
036abd46aa1d6465ee3a6bf4318811456035801cee5622783cfa00c2d314baff
|
|
| MD5 |
01cf9c28b042cf08d08ddb3133806136
|
|
| BLAKE2b-256 |
5982d0dd61c426980f093ab97ae294f3d46ea3b68dfb9c8a6f472270c1660a21
|
Provenance
The following attestation bundles were made for xilinx_test_runner-0.1.0.tar.gz:
Publisher:
publish.yml on shchuchkin-pkims/xilinx-test-runner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xilinx_test_runner-0.1.0.tar.gz -
Subject digest:
036abd46aa1d6465ee3a6bf4318811456035801cee5622783cfa00c2d314baff - Sigstore transparency entry: 2386920289
- Sigstore integration time:
-
Permalink:
shchuchkin-pkims/xilinx-test-runner@0121932a72b6d1799612891170a7d312291cd4f2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/shchuchkin-pkims
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0121932a72b6d1799612891170a7d312291cd4f2 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file xilinx_test_runner-0.1.0-py3-none-any.whl.
File metadata
- Download URL: xilinx_test_runner-0.1.0-py3-none-any.whl
- Upload date:
- Size: 114.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d043998e1b626dc91d8fef577ad30c1b375bbf93db36fa5edb70e471bbda645
|
|
| MD5 |
45450ac82e155a8ff7d343a501cc7c77
|
|
| BLAKE2b-256 |
490b2573893b18826eec7cbe32db3b2e44f9304a15bec9907d1619a3cb33efa4
|
Provenance
The following attestation bundles were made for xilinx_test_runner-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on shchuchkin-pkims/xilinx-test-runner
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xilinx_test_runner-0.1.0-py3-none-any.whl -
Subject digest:
6d043998e1b626dc91d8fef577ad30c1b375bbf93db36fa5edb70e471bbda645 - Sigstore transparency entry: 2386920298
- Sigstore integration time:
-
Permalink:
shchuchkin-pkims/xilinx-test-runner@0121932a72b6d1799612891170a7d312291cd4f2 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/shchuchkin-pkims
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0121932a72b6d1799612891170a7d312291cd4f2 -
Trigger Event:
workflow_dispatch
-
Statement type: