cargo-rigtest
A Cargo plugin for infrastructure and acceptance testing in Rust.
cargo-rigtest runs each test in its own subprocess, giving you process-level isolation, parallel execution, structured output, and first-class support for shared infrastructure setup — without the overhead of spinning up a full test harness.
Overview
Most Rust projects have two test layers covered: unit tests with #[test],
and integration tests that compile and run a local binary. The third layer
— acceptance tests against a deployed system — is almost always handled
by reaching for another tool: pytest, Postman, shell scripts.
cargo-rigtest closes that gap. It is a Cargo plugin for running
acceptance and end-to-end tests against real, deployed infrastructure —
the kind of tests you run after a deploy to staging to confirm the system
behaves as intended under real conditions, with real traffic and real
service dependencies.
The motivating use case: a service is deployed to a staging
environment. Before promoting to production, you want to verify that the
signup flow completes, authenticated requests are accepted, and data
persists correctly. These aren't unit tests — the binary is already
compiled and running. They aren't local integration tests — there's nothing
to mock. They're acceptance tests, and cargo-rigtest lets you write them
in Rust.
Unlike a Python test harness bolted onto a Rust project, cargo-rigtest
lives entirely inside your Cargo workspace. Test code can import your own
types, reuse your HTTP client configuration, and be checked by the same
compiler and CI pipeline as the rest of your project.
Features
cargo-rigtest is built around a few core ideas: tests that can't interfere with each other, infrastructure that's set up once and shared cleanly, and output that tells you exactly what failed without making you dig through logs.
- Process isolation — each test runs in its own subprocess; a panic or crash cannot affect other tests
- Parallel execution — tests run concurrently by default, configurable
with
--jobs - Global setup & teardown —
#[global_setup]and#[global_teardown]provision and clean up shared infrastructure once per suite - Per-test lifecycle —
TestContextprovides scopedsetupandteardownhooks for resources that belong to a single test - Serial, timeout, and retry — opt individual tests into exclusive execution, a hard time limit, or automatic retries
- Captured output — held per test and printed only on failure, nextest-style
- Runtime skip —
rigtest::skip!("reason")lets a test opt out gracefully at runtime
Installation
Getting started is two steps: install the cargo rigtest command, then
add the rigtest library to your project.
Install the CLI
From crates.io (builds from source — requires a Rust toolchain):
cargo install cargo-rigtest
Pre-built binaries are available for macOS, Linux, and Windows on the
releases page.
macOS and Linux releases are .tar.gz archives — extract and place
cargo-rigtest somewhere on your PATH. The Windows release is a plain
.exe — download it, rename it if desired, and place it on your PATH.
macOS note: The release binaries are ad-hoc signed but not notarized or Developer ID signed. Gatekeeper may block the binary on first launch with a security warning. You can bypass this by right-clicking the binary in Finder and choosing Open, or by running
xattr -d com.apple.quarantine /path/to/cargo-rigtestin your terminal. The Homebrew method below handles this automatically and is the recommended install path on macOS.
Homebrew (macOS and Linux):
brew tap anthonyoteri/tap
brew install cargo-rigtest
Add the library
[]
= "0.1"
If your tests make HTTP calls, enable the http-client feature to get a
pre-built reqwest::Client available as ctx.client in every test:
[]
= { = "0.1", = ["http-client"] }
You can also make HTTP calls without this feature — just bring your own client library and construct it in your tests.
Quick start
1. Add a test target
In your Cargo.toml, add a [[test]] section with harness = false:
[[]]
= "acceptance"
= "tests/acceptance.rs"
= false
2. Write the test file
use Arc;
use *;
use ;
async
async
async
3. Run
cargo rigtest run
Test attributes
#[testcase]
Registers an async function as a test case. The function must have this signature:
async
Optional flags can be combined in any order:
| Flag | Description |
|---|---|
serial |
Run this test exclusively — no other test runs concurrently with it |
timeout = <Duration> |
Hard-kill the test subprocess and report failure if it exceeds the duration |
retries = <N> |
Retry a failing test up to N additional times before reporting failure |
Note on timeout and teardown: when a timeout fires, the subprocess is terminated — on Linux and macOS a graceful signal is sent first, with a short window for the process to exit cleanly before a hard kill follows; on Windows the process is hard-killed immediately. Either way, teardown registered with
ctx.teardown(...)will not run. Resources that must be released regardless of outcome should be handled in#[global_teardown], which runs outside the test subprocess.
#[global_setup]
Runs once before any test in the suite. The return value is serialized and passed to each test subprocess. At most one may be defined.
async
The return type must implement serde::Serialize and serde::Deserialize
— the state is serialized to cross the process boundary into each test
subprocess. This means it can only hold serializable values: URLs, ports,
credentials, identifiers. Live resources such as connection pools, file
descriptors, and socket handles cannot survive the round-trip — store the
configuration needed to recreate them instead.
#[global_teardown]
Runs once after all tests finish. Receives the value produced by
#[global_setup]. At most one may be defined.
async
Per-test setup and teardown
TestContext provides setup and teardown hooks for resources with a
clear per-test lifecycle. The global argument in both closures is the
deserialized state from #[global_setup] — use it to create live
resources within the test.
async
Skipping tests
Use rigtest::skip! to skip a test at runtime with an optional reason:
async
Skipped tests appear in the summary as SKIP and do not count as failures.
Running tests
cargo rigtest run [OPTIONS]
| Flag | Description |
|---|---|
--jobs <N> |
Maximum parallel test jobs (default: number of CPUs) |
--seed <N> |
Fix the random order seed for reproducible runs |
--filter <STRING> |
Only run tests whose name contains STRING |
--test <NAME> |
Only run the named test target (repeatable: --test a --test b) |
--package <NAME> |
Package containing the test targets |
--no-capture |
Print test output in real time instead of capturing it (implies --jobs 1) |
The seed is printed at the start of every run so a failing order can be reproduced exactly:
cargo rigtest run --seed 12345678
Output
cargo-rigtest produces nextest-style output. In a TTY, running tests show live spinners; results are printed as they complete:
── global setup
PASS [0.142s] homepage_returns_200
SKIP [0.031s] requires_live_database: DATABASE_URL not set
FAIL [0.089s] creates_a_record: assertion failed at tests/acceptance.rs:42
── stdout
created record with id 99
expected count 1, got 2
────────────────────────────────────────────────────────────
Summary [0.21s] 3 tests run: 1 passed, 1 skipped, 1 failed
── global teardown
In CI or piped output, spinners are replaced with plain lines so no output is lost.
Multiple test targets
If a package has more than one rigtest test target, all of them are discovered and run in sequence automatically:
cargo rigtest run # run all rigtest targets
cargo rigtest run --test smoke # run one
cargo rigtest run --test smoke --test e2e # run two
cargo-rigtest identifies rigtest test targets automatically and ignores any
other harness = false binaries in the package.
Crate layout
| Crate | Description |
|---|---|
cargo-rigtest |
The cargo rigtest CLI plugin |
rigtest |
Runtime library — add this to [dev-dependencies] |
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.