Satchel is a flexible test collection framework for Rust projects. It allows you to register tests and benchmarks using custom macros (#[test], #[bench]), and provides a programmable interface for discovering and collecting tests outside of Rust’s default test runner.
Satchel is ideal for advanced integration scenarios, such as:
- Integrating Rust test execution into C++ projects via CMake/CTest,
- Running Rust tests from other languages or environments,
- Collecting and filtering test metadata for custom reporting.
Satchel uses distributed slices using the linkme crate to register test cases at compile time, and exposes APIs to enumerate and run tests programmatically. This makes it easy to build your own test harness, integrate with external tools, or embed Rust testing into larger polyglot projects.
Please note that platform support depends on the platforms supported by the linkme crate. See the linkme README for supported platforms.
crates/
satchel/ # Core library for Rust test registration/discovery
satchel-macro/ # Procedural macro for #[test] and #[bench]
examples/
test-runner/ # Shared test runner crate for all examples
ctest-integration/ # Example C++ project using CTest to run Rust tests
somelib/ # Example Rust library with tests
otherlib/ # Another Rust library with tests
rust-examples/ # Pure Rust examples using custom test harnesses
satchel-demo/ # Demonstrates satchel test registration/discovery
Cargo.toml # Cargo workspace manifest
satchel-demo/
Demonstrates how to use satchel for automatic test registration and discovery in a pure Rust crate.
Uses custom #[test] and #[bench] macros, distributed slices, and the shared test runner from examples/test-runner.
Shows how to use #[should_panic] with expected panic messages and #[ignore] for tests that should be skipped by default.
Satchel mirrors many behaviors of Rust's built-in test attributes while remaining explicit about the supported forms:
#[should_panic] variants:
#[should_panic](accept any panic)#[should_panic(expected = "substring")](panic message must contain substring)#[should_panic = "substring"](shorthand forexpected =)#[should_panic("substring")](positional form)
#[ignore] variants:
#[ignore](skip test, no reason)#[ignore = "reason"](skip test, track reason)
Unsupported forms produce a compile error emitted by the procedural macro (e.g. #[ignore(foo)], #[should_panic(bad = 1)]).
Test Registration:
Consumer crates (like somelib, otherlib, or satchel-demo) use the #[test] and #[bench] macros from Satchel to register test functions. These macros use linkme to collect test metadata into a distributed slice at compile time.
Test Harness:
The test harness in satchel exposes a getter for all registered tests in the current crate.
Consumer crates are responsible for providing a test harness and are free to choose any test harness they like.
Our examples export a *_tests_main function that runs all tests using libtest-mimic.
The example crates use the shared test runner from examples/test-runner, which provides a unified API for running tests and benchmarks.
CTest Integration:
CMake builds the Rust libraries as cdylib and links them into the C++ test runner. The C++ main function calls the exported test entry points, and the results are reported to CTest.
-
Add Satchel as a Dependency:
[dependencies] satchel = { path = "../../../crates/satchel" }
-
Write Tests Using the Macros:
use satchel::{test, bench}; #[test] fn my_unit_test() { assert_eq!(2 + 2, 4); } #[test] #[should_panic(expected = "overflow")] fn test_panic_with_message() { panic!("integer overflow detected"); } #[test] #[ignore] fn expensive_test() { assert_eq!(2 + 2, 4); } #[bench] fn my_benchmark() { for i in 0..1000 { let _ = i + 1; } }
-
Export a Test Runner:
use libtest_mimic::Arguments;
use test_runner;
#[no_mangle]
pub extern "C" fn some_tests_main() -> i32 {
let tests = satchel::get_tests!().map(|t| *t).collect::<Vec<_>>();
let args = Arguments::from_args();
if test_runner::run_tests(tests, args) { 0 } else { 1 }
}- Implement
run_tests:
See examples/ctest-integration/somelib/src/lib.rs for a full example, including support for #[should_panic] and expected panic messages.
cd examples/ctest-integration
cmake --preset ctest-example
cmake --build build-ctest-example
cd build-ctest-example
ctestTo run the pure Rust examples:
cargo testOr to run a specific example crate:
cargo test --package satchel_demo --test satchel_demo -- tests --show-outputTo run ignored tests:
cargo test --package satchel_demo --test satchel_demo -- --ignoredTo run all tests including ignored ones:
cargo test --package satchel_demo --test satchel_demo -- --include-ignoredcargo test -p satchel --test compile_fail -- --nocapturecargo test -p satchel --test passIf diagnostics intentionally changes:
TRYBUILD=overwrite cargo test -p satchel --test compile_failTo diff without overwriting:
TRYBUILD=diff cargo test -p satchel --test compile_failTests may be optimized out in staticlib builds:
When building a client crate with crate-type = ["staticlib"], test functions registered via #[linkme::distributed_slice] may be optimized out by the compiler in certain build profiles (especially debug), because they are not explicitly referenced. This results in tests silently not running or being excluded from the final binary.
Use cdylib instead of staticlib:
When using crate-type = ["cdylib"], you're telling Cargo to build a C-compatible dynamic library, which causes the Rust compiler and linker to preserve all #[no_mangle] and exported symbols (and also all statics with internal linkage), because it assumes they might be used externally (e.g., from C or via dlsym).
[lib]
crate-type = ["cdylib"]