2 releases
| new 0.9.2 | May 12, 2026 |
|---|---|
| 0.9.0 | May 12, 2026 |
#230 in Testing
Used in dev-tools
48KB
751 lines
dev-mutate
MUTATION TESTING WITH KILL-RATE GATES
Wraps cargo-mutants; computes kill rate, surfaces surviving mutants, gates against a threshold. Detect tests that pass without asserting anything.
dev-* verification collection.Also available as the
mutate feature of the dev-tools umbrella crate — one dependency, every verification layer.
What it does
dev-mutate wraps cargo-mutants
and emits results as a dev-report::Report.
It answers the question: is your test suite actually testing what
you think it is?
What is mutation testing?
A tool makes small deliberate changes to your code — flipping < to
>, changing + to -, removing a return, swapping a boolean.
Then it runs your tests against each mutation.
- Killed mutant: a test failed. Good — your tests caught the bug.
- Surviving mutant: all tests still passed despite the broken code. Bad — your tests aren't really testing that behavior.
The kill rate is the percent of mutants caught. High coverage with a low kill rate means lots of tests but they don't assert enough.
Quick start
[dependencies]
dev-mutate = "0.9"
One-time tool install:
cargo install cargo-mutants
Drive it from code:
use dev_mutate::{MutateRun, MutateThreshold};
let run = MutateRun::new("my-crate", "0.1.0");
let result = run.execute()?;
let threshold = MutateThreshold::min_kill_pct(70.0);
let check = result.into_check_result(threshold);
println!("{:?} {:?}", check.verdict, check.detail);
# Ok::<(), Box<dyn std::error::Error>>(())
Builder surface
| Method | What it does |
|---|---|
in_dir(path) |
Run cargo mutants from a different directory. |
workspace() |
Pass --workspace (mutate every workspace member). |
jobs(n) |
Pass --jobs <N> (parallel mutation runs). |
timeout(Duration) |
Per-mutant timeout (--timeout <secs>). |
exclude_re(pattern) |
Skip files matching the regex (--exclude-re <pattern>). Repeatable. |
file(pattern) |
Restrict to matching files (--file <pattern>). Repeatable. |
allow(description) / allow_all(iter) |
Reclassify known survivors as killed (e.g. replace + with -). |
Kill rate
kill_pct = killed / (killed + survived) * 100
Timeouts are excluded from both numerator and denominator — they don't reflect test quality, they reflect test speed.
Typical kill-rate targets
| Project type | Reasonable target |
|---|---|
| Library, production | 70–80% |
| Library, mature | 85%+ |
| Application | 50–60% |
| Cryptography / security | 95%+ |
Per-file breakdown
MutateResult::files is a sorted list of FileBreakdown records,
one per source file. Use weakest_files(n) to spotlight the lowest
kill-rate hotspots:
use dev_mutate::MutateResult;
# let result: MutateResult = unimplemented!();
for f in result.weakest_files(5) {
println!("{:<30} {:.1}% (killed {}, survived {})",
f.file, f.kill_pct(), f.killed, f.survived);
}
Allow-list known false positives
use dev_mutate::{MutateRun, MutateThreshold};
let run = MutateRun::new("my-crate", "0.1.0")
.allow("replace `+` with `-`")
.allow_all(["replace `<` with `<=`", "delete `!`"]);
let _result = run.execute()?;
# Ok::<(), Box<dyn std::error::Error>>(())
Allow-listed mutations are reclassified as killed (the user has explicitly declared them acceptable), so the kill rate goes up accordingly.
Producer integration
MutateProducer plugs the run into a multi-producer pipeline driven
by dev-tools:
use dev_mutate::{MutateProducer, MutateRun, MutateThreshold};
use dev_report::Producer;
let producer = MutateProducer::new(
MutateRun::new("my-crate", "0.1.0"),
MutateThreshold::min_kill_pct(70.0),
);
let report = producer.produce();
println!("{}", report.to_json().unwrap());
Subprocess failures map to a single failing CheckResult named
mutate::<subject> with Severity::Critical — the pipeline keeps
running.
Target-dir-lock note
Running MutateRun::execute() from inside another cargo
invocation that already holds the workspace target-dir lock will
deadlock — cargo mutants itself drives cargo test repeatedly.
Use a separate target dir:
CARGO_TARGET_DIR=/tmp/mutate-target cargo run --example basic
CARGO_TARGET_DIR=/tmp/mutate-target cargo test -- --ignored
Wire format
MutateResult, SurvivingMutant, and FileBreakdown are all
serde-derived. JSON uses snake_case field names:
{
"name": "my-crate",
"version": "0.1.0",
"mutants_total": 120,
"mutants_killed": 88,
"mutants_survived": 22,
"mutants_timeout": 10,
"survivors": [
{
"file": "src/parser.rs",
"line": 142,
"description": "replace `<` with `<=`",
"function": "validate_range"
}
],
"files": [
{ "file": "src/parser.rs", "killed": 30, "survived": 10, "timeout": 2 }
]
}
Examples
| File | What it shows |
|---|---|
examples/basic.rs |
Run against the current crate; graceful tool-missing handling. |
examples/with_threshold.rs |
Constructed result; demonstrates meets and weakest_files. |
examples/with_limits.rs |
workspace + jobs + timeout + filters + allow-list. |
examples/producer.rs |
MutateProducer (gated by DEV_MUTATE_EXAMPLE_RUN). |
The dev-* collection
dev-mutate ships independently and is also re-exported by the
dev-tools umbrella crate as
the mutate feature. Sister crates cover the other verification
dimensions:
dev-report— report schema everything emitsdev-fixtures— deterministic test fixturesdev-bench— performance and regression detectiondev-async— async runtime verificationdev-stress— stress and soak workloadsdev-chaos— fault injection and recovery testingdev-coverage— code coverage with regression gatesdev-security— CVE / license / banned-crate auditdev-deps— unused / outdated dep detectiondev-ci— GitHub Actions workflow generatordev-fuzz— fuzz testing workflowdev-flaky— flaky-test detection
Status
v0.9.x is the pre-1.0 stabilization line. Feature-complete for
mutation testing, per-file breakdown, threshold, allow-list, and
producer integration. 1.0 will pin the public API and the
kill-rate computation.
Minimum supported Rust version
1.85 — pinned in Cargo.toml via rust-version and verified by
the MSRV job in CI.
License
Apache-2.0. See LICENSE.
Copyright © 2026 James Gober.
Dependencies
~1.6–3MB
~55K SLoC