Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Rust: parallel workers report surviving mutants as KILLED (cold compile hits the timeout)

Open
#1 1 comment 0 reactions 0 assignees View on GitHub

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
Tech stack
python, rust

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.py sets timeout = max(2.0, seconds * timeout_factor), where seconds is the warm baseline (cargo test on 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:_execute then returns "killed" for result.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_DIR per 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-run has 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:

  1. In a Python project with no pytest.ini, no [tool.pytest] and no setup.cfg, but with tests/conftest.py, the runner falls back to python -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.
  2. The Rust/TS/Python operator table has no ^ >> << % & | swaps and no return-value or body replacement. For example, next_u64 in a SplitMix64 RNG gets 0 sites and shows as n/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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from unclebob/mutator

All issues in unclebob/mutator

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.