Skip to content

KDAB/satchel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Satchel

Overview

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.

Project Structure

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

Rust-Only Examples

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.

Supported Attribute Forms

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 for expected =)
  • #[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)]).

How It Works

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.

Adding Tests in a Consumer Crate

  1. Add Satchel as a Dependency:

    [dependencies]
    satchel = { path = "../../../crates/satchel" }
  2. 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;
        }
    }
  3. 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 }
}
  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.

Building and Running the Example

cd examples/ctest-integration
cmake --preset ctest-example
cmake --build build-ctest-example
cd build-ctest-example
ctest

Running Rust-Only Examples

To run the pure Rust examples:

cargo test

Or to run a specific example crate:

cargo test --package satchel_demo --test satchel_demo -- tests --show-output

To run ignored tests:

cargo test --package satchel_demo --test satchel_demo -- --ignored

To run all tests including ignored ones:

cargo test --package satchel_demo --test satchel_demo -- --include-ignored

Running Macro Compile Tests

cargo test -p satchel --test compile_fail -- --nocapture
cargo test -p satchel --test pass

If diagnostics intentionally changes:

TRYBUILD=overwrite cargo test -p satchel --test compile_fail

To diff without overwriting:

TRYBUILD=diff cargo test -p satchel --test compile_fail

Known Issues

Tests 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.

Workarounds

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"]

About

Crate for the test harness and test management

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages