Rust: parallel workers report surviving mutants as KILLED (cold compile hits the timeout)
A pull request for this has already been merged.
- #13 by @qinyuanj — merged
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Stale
- Domain
- devtools, testing-qa
Research direction
Read engine.py's timeout calculation and workers.py:_execute, then reproduce the reported false kills with the Rust example using default workers and --max-workers 1. The issue also raises separate Python test-discovery and mutation-operator gaps; confirm which work remains, since a linked pull request has been merged, and verify that timeouts or build failures are not counted as killed.
Written by the indexing model from the issue text.
Description
Summary. For a Rust crate, the default run (one worker per core) reports mutants as KILLED that actually survive. With --max-workers 1 or a large --timeout-factor, the same mutants are correctly reported as SURVIVED. cargo-mutants agrees with the single-worker result.
Version. mutator bb9262a (main, 2026-10-02), with crapper ce38952 checked out next to it. macOS 26 on arm64, 18 cores, cargo 1.95 (Homebrew), Python 3.12.
Repro. A fresh cargo new --lib repro with this src/lib.rs:
pub fn clamp_add(a: i64, b: i64, hi: i64) -> i64 {
let s = a + b;
if s > hi { hi } else { s }
}
pub fn is_small(x: i64) -> bool {
x < 10 || x == 42
}
pub fn count_until(v: &[i64], stop: i64) -> usize {
let mut n = 0;
for &x in v {
if x != stop {
n += 1;
} else {
break;
}
}
n
}
and this tests/basic.rs:
use repro::*;
#[test] fn add() { assert_eq!(clamp_add(1, 2, 100), 3); assert_eq!(clamp_add(50, 60, 100), 100); }
#[test] fn small() { assert!(is_small(3)); assert!(!is_small(20)); }
#[test] fn count() { assert_eq!(count_until(&[1, 2, 3], 9), 3); }
Results:
$ mutator --no-coverage --mutate-all src/lib.rs # default workers
KILLED src/lib.rs:3 > -> >=
KILLED src/lib.rs:7 < -> <=
... 8/8 killed
$ mutator --no-coverage --mutate-all --max-workers 1 src/lib.rs
SURVIVED src/lib.rs:3 > -> >=
SURVIVED src/lib.rs:7 < -> <=
... 6/8 killed
$ mutator --no-coverage --mutate-all --timeout-factor 300 src/lib.rs # default workers
2 survived (same two)
$ cargo mutants
MISSED src/lib.rs:3:10: replace > with >= in clamp_add
MISSED src/lib.rs:7:7: replace < with <= in is_small
Applying either mutation by hand and running cargo test passes, so the single-worker result is the correct one.
I saw the same thing on a real 282-line crate. The default run scored 48/50; 4 of those "kills" were false (!=→==, <→<=, and ||→&& in a parser). cargo-mutants and a manual check both showed the mutants surviving.
Cause (as far as I can tell).
engine.pysetstimeout = max(2.0, seconds * timeout_factor), wheresecondsis the warm baseline (cargo teston an already-built tree, about 0.2–0.3 s).- Each worker overlay under
target/mutation-workers/gets its own cargo target dir. So every worker's first mutant compiles the crate and its deps from cold, with N workers compiling at the same time. - That cold build takes longer than the timeout.
workers.py:_executethen returns"killed"forresult.timed_out or result.code != 0. A build failure caused by the harness is also counted as a kill.
Possible fixes.
- Share one
CARGO_TARGET_DIRper worker across all of its mutants and warm it once before timing starts (cargo-mutants builds each scratch dir once, then reuses it). - Or time the baseline in a cold worker dir.
- Or don't start the timeout clock until
cargo test --no-runhas finished: build separately, then time only the test phase. - Report timeouts as a separate outcome, not as killed. A build error should be "unviable" / not counted, not killed.
Two smaller things I noticed while testing:
- In a Python project with no
pytest.ini, no[tool.pytest]and nosetup.cfg, but withtests/conftest.py, the runner falls back topython -m unittest discover. That collects 0 tests and passes, so every mutant on a line that runs at import time is reported SURVIVED, and the rest are reported uncovered. A baseline that ran 0 tests should probably fail the baseline. - The Rust/TS/Python operator table has no
^ >> << % & |swaps and no return-value or body replacement. For example,next_u64in a SplitMix64 RNG gets 0 sites and shows asn/a, not as untested.
- Dominant language
- Python
- Stars
- 55
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from unclebob/mutator
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Workers leave out source in a directory named build, out, dist, target or coverage, so every mutant is reported KILLEDPossibly taken @lohmag claimed this 1 day ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
-
Difficulty 4/5 3-5 days Newbie friendliness 49/100
-
Difficulty 4/5 3-5 days Newbie friendliness 53/100
All issues in unclebob/mutator
Similar issues
-
Support .aac audio uploadsPossibly taken @julien731 claimed this today. Openfeature
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
nimblehq/audio-transcriber#153 ·
-
help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
antonyrag/ragleap-core#1062 ·
-
needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
gke-labs/kube-agents#4321 ·
Maintainers usually reply within 1 day
-
enhancement help wanted
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Git-Hospital/githealthcheck#61 ·
Maintainers usually reply within 1 day
-
changelog investigate
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
ramnes/notion-sdk-py#433 ·