rust-test-tools
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
Use when choosing, adding, or reviewing a Rust test tool beyond plain cargo test, such as loom for hand-rolled atomics and lock-free code, proptest and cargo-fuzz for parsers of untrusted bytes, cargo-careful as an extra lane beside a sanitizer for FFI code that Miri cannot run, cargo-mutants with survived-mutant triage for weak tests, golden or snapshot files for deterministic output, and a differential fuzz target for a port with a reference implementation. Not for the test-first cycle (rust-tdd) or Miri and sanitizer setup (rust-sanitizers-miri).
- Installs packages
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-debuggingSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. About 0 are common, 7 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 31
If cargo-nextest is installed, use it as the runner.
- Very specific: line 43
Run the tests for unsafe code in a debug build at least once.
- Distinctive: line 61
Before you approve a change, confirm each item that applies:
- Very specific: line 90
Read references/cargo-careful.md when you set up the lane (nightly and rust-src), need the list of checks, or a flag from …
- Distinctive: line 107
Test one primitive per test, with two or three threads.
- Very specific: line 120
Read references/loom.md when you set up loom in a crate, gate a static atomic or an UnsafeCell, need a worked example with its failure …
- Distinctive: line 126
Write properties for any function that takes bytes, text, or a configuration value and produces a parsed or validated result.
- Very specific: line 219
When a port's reference runs in-process (a C library, the previous Rust version, a spec model), run a differential fuzz target.
- Distinctive: line 276
If a test must catch the change, write a test that asserts the exact value or side effect.
- Very specific: line 277
If the code cannot be tested in a useful way (Display or Debug output, logging, FFI glue), exclude it with exclude_re in …
- Distinctive: line 281
Do not block every pull request on a full run: it takes minutes to hours, and some mutants are equivalent to the original code.
- Very specific: line 284
Read references/mutation-testing.md when you write .cargo/mutants.toml, pick -j or --dir, need the full flag or exit-code table, suspect a …
- Very specific: line 293
Put the fixtures in tests/golden/ of the crate that owns the output, and the test in tests/golden_contracts.rs.
- Distinctive: line 322
Never set the bless variable in CI.
- Very specific: line 329
Do not build a golden file or an insta::assert_debug_snapshot!
- Very specific: line 341
If the crate already uses insta, use its macros and cargo insta review.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Rust Test Tools
A green cargo test does not find undefined behavior (UB), atomic-ordering bugs, parser edge
cases, weak assertions, or output drift. Pick the tool by the risk that the change adds. Run it
on the crate that the change touches while you iterate. Run the full lane once before merge.
Pick the tool
| The change adds | Run | A green run does not prove |
Pure-Rust unsafe: raw pointers, transmute, MaybeUninit, from_raw_parts | Miri. The rust-sanitizers-miri skill owns the flags and the aliasing-model policy | UB freedom on paths that the tests do not reach |
unsafe next to foreign code: FFI, JNI, libc, syscalls | ASan on the host, or HWASan or MTE on a device; TSan when the code shares state across threads. cargo-careful is a cheap extra lane, not a substitute. Also Miri on the pure-Rust helpers, with the foreign calls stubbed under #[cfg(miri)]; the stub dereferences every pointer that the foreign side stores | UB freedom: a sanitizer does not know the Rust aliasing rules, and careful checks only a few UB classes |
| Hand-rolled atomics, a spinlock, a lock-free structure, a publish flag | loom | freedom from bugs that need load buffering or more preemptions than the bound |
| A parser, a decoder, or any function that reads untrusted bytes | proptest, then cargo-fuzz | freedom from bugs that the strategy or the corpus never reaches |
| A port whose reference a test can call in-process | a differential fuzz target | equivalence outside the inputs that the fuzzer reached |
| A refactor of tested logic, or new tests for a critical change | cargo-mutants on the diff, or on the source files under test when the diff only adds tests | that the asserted values are the correct ones |
| Output that must be deterministic: serialized records, generated code, reports, images | golden files | that the first blessed baseline is correct |
| None of the above | the normal test run | behavior that no test asserts |
Code that uses only std::sync::Mutex or RwLock, or data-parallel code (for example rayon)
with no hand-rolled atomics, does not need loom.
Lanes and cadence
If cargo-nextest is installed, use it as the runner. It runs each test in its own process, so
one abort or segfault does not hide the rest of the suite. Nextest does not run doctests, so run
them separately. Keep --locked so that a test run cannot rewrite Cargo.lock.
cargo nextest run --locked -p <crate> --no-fail-fast
cargo test --locked -p <crate> --doc
# Without nextest.
cargo test --locked -p <crate> --no-fail-fastRun the tests for unsafe code in a debug build at least once. Since Rust 1.78, std checks unsafe
preconditions (for example the null and alignment rules of slice::from_raw_parts) when debug
assertions are on. A --release build removes these checks.
| Lane | Command | When |
| Tests, proptest, golden files | cargo nextest run --locked --workspace and cargo test --locked --workspace --doc | every pull request |
| cargo-careful | cargo +nightly careful test --locked -p <ffi-crate> --no-fail-fast | pull requests that touch unsafe or FFI crates |
| Miri, sanitizers | the rust-sanitizers-miri skill | pull requests that touch unsafe, and on a schedule |
| loom | RUSTFLAGS="--cfg loom" cargo test --locked --release --test 'loom_*' | pull requests that touch hand-rolled atomics |
| cargo-fuzz | cargo +nightly fuzz run <target> -- -max_total_time=900 for each cargo fuzz list entry | nightly or weekly schedule, not every pull request |
| cargo-mutants | --in-diff run on demand, full run on a schedule | after a refactor, and weekly |
The cargo-workflows skill owns nextest profiles and the GitHub Actions rules (actions pinned to
a commit SHA, node24 action majors).
Review gate
Before you approve a change, confirm each item that applies:
- New pure-Rust
unsafehas a Miri run when a nightly toolchain with themiricomponent is installed; otherwise the report names it as not checked by Miri. This includesunsafehelpers inside an FFI crate, with the foreign calls stubbed under#[cfg(miri)], and the stub dereferences every pointer that the foreign side stores (therust-sanitizers-miriskill has the stub rules). A careful run alone is not UB evidence. - New
unsafeacross FFI has an ASan run on the host, or a HWASan or MTE run on a device, and a TSan run when it shares state across threads. - Every new hand-rolled atomic or lock-free primitive has a loom test that calls the crate's own type. The loom lane selects only the loom targets.
- Every new parser or decoder has a never-panics property, a roundtrip property where a roundtrip exists, and committed proptest regression files.
- Every fuzz crash is a committed, minimized regression test.
- A port with an in-process reference has a differential fuzz target.
- A refactor of tested logic has a cargo-mutants run on the diff. Each missed mutant has a test or a recorded exclusion.
- Deterministic output has committed golden files. Each fixture change is a reviewed, deliberate commit.
cargo-careful
cargo-careful rebuilds the standard library with debug assertions and runs code that Miri cannot
run (foreign calls, system calls). It is a cheap extra lane, not a UB proof: it does not check
aliasing, general reads of uninitialized memory, use-after-free, or data races. A pure-Rust
unsafe helper inside an FFI crate still needs a Miri run. The foreign paths still need an ASan
run on the host, or a HWASan or MTE run on a device; the rust-sanitizers-miri skill owns that
setup.
Read references/cargo-careful.md when you set up the lane (nightly
and rust-src), need the list of checks, or a flag from target.<triple>.rustflags is missing
in the careful run.
loom
loom runs a test body under every thread interleaving that the preemption bound allows, and it models the C11 memory orderings. A stress test samples a few schedules. loom enumerates them. Know its limits before you trust a result:
- loom checks only the operations on its own types. Gate the atomics and a shared
UnsafeCelltoloom::sync::atomicandloom::cell::UnsafeCellundercfg(loom), and spawn threads withloom::thread::spawn. A data race on astdtype is invisible to loom, so the run can pass. - loom treats
SeqCstloads and stores asAcqRel. Code that needsSeqCstaccesses can fail under loom and still be correct. loom modelsfence(SeqCst)correctly. - loom does not explore load-buffering executions. A green loom run can hide such a bug.
- The cost grows exponentially with the number of atomic operations and the preemption bound. Test one primitive per test, with two or three threads.
- loom's scheduler is not fair. Under
cfg(loom), every spin or retry loop must callloom::thread::yield_now()(orloom::hint::spin_loop()). Otherwise the model fails with "Model exceeded maximum number of branches" (LOOM_MAX_BRANCHES, default 1000).
Declare loom under [target.'cfg(loom)'.dependencies], because the library itself imports it,
and add a check-cfg entry for cfg(loom). Do not use a loom Cargo feature: --all-features
turns it on, and every ordinary test that touches the primitive then panics. Make the loom test call the crate's own type, not a
copy of the algorithm. Select only the loom targets (--test 'loom_*'). With --tests or a bare
cargo test, every other test that touches the primitive runs outside loom::model and panics
with "cannot access Loom execution state from outside a Loom model". Gate such tests with
#[cfg(not(loom))] if they must share a run.
Read references/loom.md when you set up loom in a crate, gate a static
atomic or an UnsafeCell, need a worked example with its failure output, or must bound a slow
run.
proptest
Write properties for any function that takes bytes, text, or a configuration value and produces a parsed or validated result. Assert invariants, not single examples.
use proptest::prelude::*;
proptest! {
// Total function: no panic for any byte string.
#[test]
fn parse_never_panics(buf in prop::collection::vec(any::<u8>(), 0..1024)) {
let _ = Header::parse(&buf);
}
// Roundtrip: serialize a parsed header and get the consumed prefix back.
#[test]
fn parse_then_serialize_roundtrips(buf in prop::collection::vec(any::<u8>(), 0..4096)) {
if let Ok(hdr) = Header::parse(&buf) {
let mut out = Vec::new();
hdr.write_to(&mut out);
prop_assert_eq!(&out, &buf[..hdr.len()]);
}
}
// Structured roundtrip: generate a valid value, encode it, and decode it back.
#[test]
fn decode_roundtrips_valid_input(value in arb_valid_message()) {
let decoded = decode(&value.encode()).expect("valid input must decode");
prop_assert_eq!(decoded, value);
}
// Text: "(?s).*" also generates '\n'. ".*" and any::<String>() never do.
#[test]
fn text_parser_never_panics(s in "(?s).*") {
let _ = parse_document(&s);
}
}- Random bytes seldom get past a magic number or a checksum. The structured strategy reaches the deeper code, so write one for every format with such a check.
- A line-based parser tested with
".*"orany::<String>()never sees a newline (probe on proptest 1.11.0). Use"(?s).*". - proptest saves each failing case and runs it first on the next run. A test in
src/saves toproptest-regressions/at the crate root. An integration test saves totests/<name>.proptest-regressions. Commit these files. - For a deeper local run, raise the case count:
PROPTEST_CASES=10000 cargo test --locked -p <crate> parse_.
A new parser without a never-panics property is incomplete.
cargo-fuzz
proptest finds the bugs that a strategy can reach. A coverage-guided fuzzer also finds the bugs that only a mutated corpus reaches, above all in binary protocol and container decoders.
# Setup, once per crate. Building and running a target needs nightly.
cargo install --locked cargo-fuzz
cargo fuzz init
cargo fuzz add parse_header// fuzz/fuzz_targets/parse_header.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let _ = my_crate::wire::parse_header(data);
});cargo +nightly fuzz run parse_header -- -max_total_time=600
cargo fuzz list
# Reproduce a crash from a saved artifact, then shrink it.
cargo +nightly fuzz run parse_header fuzz/artifacts/parse_header/crash-<id>
cargo +nightly fuzz tmin parse_header fuzz/artifacts/parse_header/crash-<id>- Fuzz the outermost entry point that takes untrusted bytes: a frame or header decoder, an archive reader, a decompressor, a document parser.
- A length field that drives an allocation is the usual out-of-memory crash. Cap the length in the parser, not in the fuzz target.
- A recursive parser needs a nesting-depth limit. Deep nesting overflows the stack. The
report shows
AddressSanitizer: stack-overflowin the default ASan build, orhas overflowed its stackwithout ASan. Cap the depth in the parser, not in the fuzz target. - Turn every crash into a regression test. Shrink it with
tmin, commit the input (for example undertests/regressions/), and add a test that feeds it to the entry point.
Differential testing
When a port's reference runs in-process (a C library, the previous Rust version, a spec
model), run a differential fuzz target; a Python or JavaScript original gets the rust-port
recorded comparison. Feed both the same bytes and compare. A port that is memory-safe and
passes its own tests can still disagree with the reference.
// fuzz/fuzz_targets/decode_matches_reference.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let ours = my_crate::decode(data).ok();
let reference = reference_impl::decode(data).ok();
assert_eq!(ours, reference, "decoders disagree on {data:?}");
});- Compare acceptance and the value. Compare the error kind too when callers depend on it.
- Seed
fuzz/corpus/<target>/with non-ASCII text and invalid UTF-8: a lone continuation byte (0x80), an overlong encoding (0xC0 0xAF), and a truncated 4-byte sequence. Ports often differ on non-ASCII and invalid UTF-8 input.
cargo-mutants
cargo-mutants changes the code (it replaces a function body with a default value, flips a
comparison, or deletes a call) and reruns the tests for each change. A mutant that survives
marks code that the tests run but do not check. It also exposes a new test that never calls the
changed function; the rust-tdd skill owns that rule.
cargo install --locked cargo-mutants
# Fastest useful loop: only the lines that the branch changes.
git diff origin/main...HEAD > /tmp/pr.diff
cargo mutants --test-tool nextest --cargo-arg=--locked --in-diff /tmp/pr.diff --package <crate> -j2 --output target/
# One whole crate.
cargo mutants --test-tool nextest --cargo-arg=--locked --package <crate> -j2 --output target/Without cargo-nextest, drop --test-tool nextest; cargo-mutants then runs cargo test.
- A diff that only adds or changes tests gives no mutants, because
--in-diffmatches only the code under test. For new tests of existing code, runcargo mutants --test-tool nextest --cargo-arg=--locked --file <source-file>(or--package <crate>). - cargo-mutants runs cargo without
--locked. Pass--cargo-arg=--locked, or setadditional_cargo_args = ["--locked"]in.cargo/mutants.toml, not both: cargo rejects a repeated--locked. - Results go to
<output>/mutants.out/. Exit code 2 means some mutants survived. Exit code 4 means the tests fail before any mutation, so fix the suite first.
Triage mutants.out/missed.txt. It is the gate, not a score: cargo-mutants defines no
threshold.
- For each missed mutant, read the function and the change.
mutants.out/diff/holds each change as a diff. - If a test must catch the change, write a test that asserts the exact value or side effect.
- If the code cannot be tested in a useful way (
DisplayorDebugoutput, logging, FFI glue), exclude it withexclude_rein.cargo/mutants.tomlor with#[mutants::skip]. Record the reason next to the exclusion. Do not write a meaningless test to raise the count.
Do not block every pull request on a full run: it takes minutes to hours, and some mutants are equivalent to the original code.
Read references/mutation-testing.md when you write
.cargo/mutants.toml, pick -j or --dir, need the full flag or exit-code table, suspect a
false positive (for example a doctest-only behavior under nextest), or set up the scheduled CI
workflow.
Golden files
A golden test compares output with a committed file. Use it for output that another party
reads: serialized records, exported reports, generated code, protocol frames, rendered images.
Put the fixtures in tests/golden/ of the crate that owns the output, and the test in
tests/golden_contracts.rs. Cargo builds each .rs file directly under tests/ as a test
binary, and it skips a subdirectory that has no main.rs.
use std::path::Path;
/// Compares `actual` with `tests/golden/<name>`. `BLESS_GOLDENS=1` rewrites the file.
fn assert_golden(name: &str, actual: &str) {
// env! resolves at compile time, so the path does not depend on the working directory.
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden").join(name);
if std::env::var("BLESS_GOLDENS").as_deref() == Ok("1") {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, actual).unwrap();
return;
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|error| {
panic!("missing golden {}: {error}; bless it with BLESS_GOLDENS=1", path.display())
});
assert_eq!(actual, expected, "golden mismatch for {}", path.display());
}cargo nextest run --locked -p <crate> --test golden_contracts # compare
BLESS_GOLDENS=1 cargo nextest run --locked -p <crate> --test golden_contracts # bless
git diff -- tests/golden/ # review- Never set the bless variable in CI. A missing fixture must fail the run, and the helper panics for that reason.
- Bless only an intended output change. Read the fixture diff before you commit. Never bless to turn a red run green.
- Make the output deterministic before you compare it. Replace timestamps, generated IDs, ports,
temporary paths, and host names with placeholders. Iterate a
HashMaporHashSetin sorted order, or use aBTreeMap: std hash iteration order changes from one process to the next. - Do not build a golden file or an
insta::assert_debug_snapshot!fromDebug({:?}) output. std does not keep that format stable, and Rust 1.98 escapes more characters in strings and chars. Serialize with the real output format instead. - Text: compare byte for byte. Add
**/tests/golden/** -textto the root.gitattributes(ortests/golden/** -textto a.gitattributesin the crate directory), so that a checkout withcore.autocrlfdoes not rewrite the line endings. A pattern with a slash is anchored to the directory of its.gitattributesfile. - Binary: compare the bytes from
std::fs::read. For a PDF or an archive, remove the embedded timestamps first. - Raster images: compare decoded pixels, exactly or with a stated per-channel tolerance. Do not compare encoded PNG bytes: another encoder version or setting gives other bytes for the same pixels.
- If the crate already uses
insta, use its macros andcargo insta review. Do not add a second golden mechanism.
Related skills
These skills own the adjacent topics, when they are installed:
rust-sanitizers-miri: Miri flags and model policy;#[cfg(miri)]stubs for foreign calls; ASan, TSan, MSan, and HWASan.memory-model: the choice of atomic orderings that a loom test checks.rust-unsafe: SAFETY contracts and theunsafeaudit surface.rust-tdd: the test-first cycle, and tests that call the changed code.cargo-workflows: nextest profiles and CI workflow rules.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

