2026-11-01 19:15:38 +01:00
#!/usr/bin/env bash
# cargo-mutants on Yggdrasil for a revision range: ONE verify job, per-file scoped
# tests, survivors confirmed on the full suite, sharded over free slots, one merged
# survivor list. The one owner of "run cargo-mutants" (no brief, no job script runs
# an inline `cargo mutants`).
#
# SUMMARY=<log> tools/cargo-mutants.sh <local-repo> <rev> <base> [workdir]
# workdir directory inside the repo where cargo runs (a package or workspace
# member, e.g. crates/mind-body; CultLib has no Cargo.toml at its root).
# The mutated range is `git diff --relative <base> <rev>` taken at the
# WORKSPACE ROOT: cargo-mutants matches --in-diff paths, -f paths and
# prints mutant names relative to the workspace root, not to workdir.
# env FILTERS '<path>=<filter>,...' overrides a file's derived test filter
# (path as the mutant names print it); '-' = full suite
# TEST_ARGS harness args, e.g. '--skip flaky_one' (a flaky test would
# make a surviving mutant read as killed in one run)
# NAMES local file of mutant names (one per line, copied from a
# previous MISSED/TIMEOUT list): skips stage 1 and the listing
# and runs only those names on the full suite. This is the rerun
# after killing survivors with tests.
2026-11-01 20:57:04 +01:00
# SHARDS (3) JOBS (2) MIN_TEST_TIMEOUT (20) CONFIRM_TIMEOUT_MULT (5)
2026-11-01 19:15:38 +01:00
# TIMEOUT seconds for the whole job (default 10800)
# everything else tools/stopgap/ygg-verify.sh reads (MEM, CPUS, ...)
# Typical, from an agent: launch as ONE background shell with a fresh log, then
# tools/stopgap/ygg-verify.sh wait <log> (Bash tool timeout >= 280000 ms)
# Run it ONCE per pass, at the end, on the final spelling of the code. Chasing
# survivors is a NAMES rerun, never a second whole-range sweep.
#
# STAGES (all inside the one job and the one host driver):
2026-11-01 20:57:04 +01:00
# plan one container writes the range diff, lists every mutant in it, derives each
# touched file's test filter and counts the tests that filter selects
# (`cargo test -- <filter> --list`, one build for all files).
# stage 1 each touched file whose filter selects tests runs its mutants against that
2026-11-01 19:15:38 +01:00
# file's tests only (`cargo mutants -f <file> -- <filter>`). The filter is
# derived from the path: the module path after the last src/ (a/mod.rs ->
# `a::`, a/b.rs -> `a::b::`); lib.rs, main.rs, bin/ and files outside src/
# have none. FILTERS overrides. Stage 1 decides only CAUGHT (its own green
# filtered baseline guards false kills) and UNVIABLE (a build fact). Its
# MISSED and TIMEOUT are never reported: a mutant that only another module's
2026-11-01 20:57:04 +01:00
# tests or tests/ kill survives a filtered run. A file whose filter selects 0
# tests (tests=0) never runs stage 1: all its mutants go to stage 2.
2026-11-01 19:15:38 +01:00
# stage 2 every name stage 1 did not settle (survivors, files without a filter,
# zero-test files, or NAMES) runs on the full suite in cargo-mutants' own
2026-11-01 20:57:04 +01:00
# baseline, sharded over the containers. Stage 2 ALWAYS runs: that baseline is
# the only full-suite check, so a red suite is a red job even when stage 1
# settled every mutant (one name then rides along; its outcome is dropped).
# The test timeout is max(MIN_TEST_TIMEOUT, CONFIRM_TIMEOUT_MULT x baseline);
# the generous default keeps a slow-but-finishing test from flipping a
# MISSED mutant to TIMEOUT with the host's load. Its outcome is final.
2026-11-01 19:15:38 +01:00
# merge each name counted once; MISSED and TIMEOUT are stage-2 outcomes only.
# Containers per stage: min(SHARDS, SLOTS, units, 1 + free slots now), units being the
# stage's files (stage 1) or names (stage 2). The job holds one slot (ygg-verify's);
# the driver leases further slot locks with flock -n, never waits for one, takes none
# while /proc/pressure/io full avg60 > 20, and releases them when each stage ends. A
# killed job (YGG_KILLED) starts nothing further.
#
# OUTPUT: the YGG-SUMMARY/$YGG_NONCE lines ygg-verify's summary carries; per-container
# logs stay on the host and go with the job.
# FILTER <file> <filter> tests=<n> mutants=<m> one per stage-1 file (zero-test first)
# MUTANTS tested=N caught=C missed=M timeout=T unviable=U | stage1 filtered=F
2026-11-01 19:43:21 +01:00
# survivors=S confirmed_caught=X | containers=K | plan=..s stage1=..s/<k1> stage2=..s/<k2> | <m>m
2026-11-01 19:15:38 +01:00
# MISSED <name> / TIMEOUT <name> final outcomes, at most 50
# ERROR ... a red baseline, a container that failed, mutants with no outcome:
# the job exits non-zero and prints no MUTANTS line
# Exit 0 when every stage ran on a green baseline; survivors do not fail the job (the
# score is not a gate). cargo-mutants exits 2 (missed) and 3 (timeouts) normally; 4 is a
# red baseline; anything else is an error.
#
# DELETION LINE: this file dies with tools/stopgap/ygg-verify.sh when `idunn verify`
# ships a typed mutation step. Modes in this one file: no flag = launcher on the
# workstation; --host = the driver on the Yggdrasil host (ygg-verify.sh RUN_ON_HOST=1);
# --plan / --item = the work inside a container.
set -euo pipefail
SUMTAG = " YGG-SUMMARY ${ YGG_NONCE : +/ $YGG_NONCE } "
sum( ) { printf '%s: %s\n' " $SUMTAG " " $* " ; }
2026-11-01 20:57:04 +01:00
# ---- --escape: mutant names on stdin -> one anchored -F pattern per line ----------------------
# Only regex metacharacters are escaped, so names holding * + ( ) | stay literal; ^...$ pins the whole name.
if [ " ${ 1 :- } " = --escape ] ; then
exec perl -pe 'chomp; s/([\\.+*?()|\[\]{}^\$])/\\$1/g; $_ = "^$_\$\n"'
fi
2026-11-01 19:15:38 +01:00
# ---- launcher (workstation) -------------------------------------------------
if [ " ${ 1 :- } " != --host ] && [ " ${ 1 :- } " != --plan ] && [ " ${ 1 :- } " != --item ] ; then
here = $( cd " $( dirname " $0 " ) " && pwd )
repo = ${ 1 : ?usage : cargo-mutants.sh <local-repo> <rev> <base> [workdir] }
rev = ${ 2 : ?revision } ; base = ${ 3 : ?base revision } ; workdir = ${ 4 :- . }
2026-11-01 20:57:04 +01:00
: " ${ SHARDS : =3 } " " ${ JOBS : =2 } " " ${ MIN_TEST_TIMEOUT : =20 } " " ${ CONFIRM_TIMEOUT_MULT : =5 } " " ${ TIMEOUT : =10800 } "
2026-11-01 19:15:38 +01:00
for v in SHARDS JOBS MIN_TEST_TIMEOUT TIMEOUT; do
case ${ !v } in '' | *[ !0-9] *| 0) echo " cargo-mutants: $v must be a positive integer " >& 2; exit 2 ; ; esac
done
case $CONFIRM_TIMEOUT_MULT in '' | *[ !0-9.] *| .*| *.| *.*.*) echo "cargo-mutants: CONFIRM_TIMEOUT_MULT must be a positive number" >& 2; exit 2 ; ; esac
if [ -n " ${ FILTERS :- } " ] ; then
old = $IFS ; IFS = ,
for f in $FILTERS ; do
case $f in ?*= ?*) ; ; *) IFS = $old ; echo "cargo-mutants: FILTERS entries are <path>=<filter>" >& 2; exit 2 ; ; esac
done
IFS = $old
fi
sha_rev = $( git -C " $repo " rev-parse --verify " $rev ^{commit} " ) || { echo "cargo-mutants: unknown revision" >& 2; exit 2; }
sha_base = $( git -C " $repo " rev-parse --verify " $base ^{commit} " ) || { echo "cargo-mutants: unknown base revision" >& 2; exit 2; }
git -C " $repo " merge-base --is-ancestor " $sha_base " " $sha_rev " || { echo "cargo-mutants: base is not an ancestor of rev (the verify mirror carries only rev's history)" >& 2; exit 2; }
stage = $( mktemp -d)
trap 'rm -rf "$stage"' EXIT
cp " $here /cargo-mutants.sh " " $stage / "
files = ( cargo-mutants.sh)
if [ -n " ${ NAMES :- } " ] ; then
[ -r " $NAMES " ] || { echo "cargo-mutants: cannot read the NAMES file" >& 2; exit 2; }
tr -d '\r' <" $NAMES " >" $stage /names.txt " ; files += ( names.txt)
fi
bundle = $( tar -C " $stage " -cz " ${ files [@] } " | base64 | tr -d '\n' )
# The driver runs on the host: it unpacks the bundle into its own directory (mounted read-only into every container) and removes it when done.
cmd = $( printf 'd=$(mktemp -d) && echo %s | base64 -d | tar -xz -C "$d" && WORKDIR=%q BASE=%q FILTERS=%q TEST_ARGS=%q SHARDS=%q JOBS=%q MIN_TEST_TIMEOUT=%q CONFIRM_TIMEOUT_MULT=%q CM_NAMES=%q CM_DIR="$d" bash "$d/cargo-mutants.sh" --host' \
" $bundle " " $workdir " " $sha_base " " ${ FILTERS :- } " " ${ TEST_ARGS :- } " " $SHARDS " " $JOBS " " $MIN_TEST_TIMEOUT " " $CONFIRM_TIMEOUT_MULT " " ${ NAMES : +1 } " )
RUN_ON_HOST = 1 TIMEOUT = $TIMEOUT " $here /stopgap/ygg-verify.sh " " $repo " " $sha_rev " rust " $cmd "
exit $?
fi
# ---- driver (Yggdrasil host, inside the job's slot) --------------------------
if [ " $1 " = --host ] ; then
: " ${ YGG_WORK : ?not started by ygg-verify.sh RUN_ON_HOST=1 } " " ${ YGG_IMAGE : ? } " " ${ YGG_CNAME : ? } " " ${ YGG_LABEL : ? } " " ${ YGG_SLOTS : ? } " " ${ YGG_ROOT : ? } "
D = ${ CM_DIR : ? }
set +e # every docker step is checked by hand: a container exits non-zero by design
export LC_ALL = C
t0 = $( date +%s)
LEASE_FDS = ( )
unlease( ) { local lf; for lf in " ${ LEASE_FDS [@] } " ; do exec { lf} >& -; done ; LEASE_FDS = ( ) ; }
cleanup( ) {
local p; for p in $( jobs -p) ; do kill " $p " 2>/dev/null; done
unlease
# containers write as root
[ -z " $D " ] || [ " $D " = / ] || sudo rm -rf -- " $D "
}
trap cleanup EXIT; trap 'exit 143' TERM INT HUP
killed( ) { [ -n " ${ YGG_KILLED :- } " ] && [ -e " $YGG_KILLED " ] ; }
now( ) { date +%s; }
2026-11-03 01:50:23 +01:00
# The shared volume is CARGO_HOME itself so sibling containers share cargo's package-cache lock; a registry-only mount left
# the lock in each container's layer and concurrent unpacks removed each other's crate directories (2026-11-03).
2026-11-01 19:15:38 +01:00
# Every container carries the job label (the stopgap's watchdog kills by it) and the same caps.
common = ( --label " $YGG_LABEL " " --cpus= ${ YGG_CPUS :- 4 } " " --memory= ${ YGG_MEM :- 6g } "
-v " $YGG_WORK :/seed:ro " -v " $D :/cm:ro " -v /etc/machine-id:/etc/machine-id:ro
2026-11-03 01:50:23 +01:00
-v eureka-cargo-home:/cargo-home -v eureka-nuget:/root/.nuget/packages -v eureka-sccache:/sccache
2026-11-03 01:57:29 +01:00
-e BASH_ENV = /tmp/.ygg-bash-env -e CARGO_HOME = /cargo-home
2026-11-01 20:57:04 +01:00
-e " WORKDIR= ${ WORKDIR :- . } " -e " BASE= $BASE " -e " TEST_ARGS= ${ TEST_ARGS :- } " -e " FILTERS= ${ FILTERS :- } " -e " CM_NAMES= ${ CM_NAMES :- } " -e " JOBS= $JOBS "
2026-11-01 19:15:38 +01:00
-e " MIN_TEST_TIMEOUT= $MIN_TEST_TIMEOUT " -e " CONFIRM_TIMEOUT_MULT= $CONFIRM_TIMEOUT_MULT "
-e GIT_CONFIG_COUNT = 1 -e GIT_CONFIG_KEY_0 = safe.directory -e 'GIT_CONFIG_VALUE_0=*' )
[ -z " ${ YGG_NONCE :- } " ] || common += ( -e " YGG_NONCE= $YGG_NONCE " )
pre = "printf 'set -o pipefail\n' >/tmp/.ygg-bash-env"
# drun <name> <out dir> [-e flags]: one container (the half named by $MODE), its output to <out>/stdout, its
# status in <out>/hostrc. A killed job starts nothing (125). DOCKER_ARGS is a flag string, split on purpose.
# shellcheck disable=SC2086
drun( ) {
local name = $1 out = $2 rc; shift 2
killed && return 125
mkdir -p " $out "
sudo nice -n 10 docker run --rm --name " $name " " ${ common [@] } " -v " $out :/out " " $@ " -w /src ${ DOCKER_ARGS :- } " $YGG_IMAGE " \
bash -o pipefail -c " $pre ; bash /cm/cargo-mutants.sh $MODE " >" $out /stdout " 2>& 1
rc = $? ; echo " $rc " >" $out /hostrc " ; return " $rc "
}
killed_exit( ) { sum "JOB KILLED by the watchdog before the run finished; no totals" ; exit 124; }
mkdir -p " $D /items " " $D /plan " " $D /q " " $D /m "
# ---- plan
tp = $( now)
MODE = --plan drun " $YGG_CNAME -plan " " $D /plan "
rc = $?
killed && killed_exit
if [ " $rc " != 0 ] ; then
sum " ERROR the plan container exited $rc : the range diff or the mutant listing failed "
tail -n 6 " $D /plan/stdout " 2>/dev/null | sed " s|^| $SUMTAG : ERROR | "
exit 2
fi
tp = $(( $( now) - tp ))
if [ " ${ CM_NAMES :- } " = 1 ] ; then sort -u " $D /names.txt " >" $D /m/universe "
else sort -u " $D /plan/names.txt " >" $D /m/universe " ; fi
2026-11-01 20:57:04 +01:00
for f in s1.caught s1.unviable s1.surv s1.names; do : >" $D /m/ $f " ; done
2026-11-01 19:15:38 +01:00
# lease <units>: the containers of a stage = min(SHARDS, SLOTS, units, 1 + free slots now); sets K and holds the leases
lease( ) {
local want = $1 i lf psi
[ " $want " -le " $SHARDS " ] || want = $SHARDS
[ " $want " -le " $YGG_SLOTS " ] || want = $YGG_SLOTS
K = 1; [ " $want " -gt 1 ] || return 0
psi = $( awk '$1 == "full" { for (i = 2; i <= NF; i++) if ($i ~ /^avg60=/) { sub("avg60=", "", $i); print $i } }' " ${ CM_PSI :- /proc/pressure/io } " 2>/dev/null)
if awk -v p = " ${ psi :- 0 } " 'BEGIN { exit !(p > 20) }' ; then return 0; fi
for i in $( seq 1 " $YGG_SLOTS " ) ; do
[ " $K " -lt " $want " ] || break
exec { lf} >" $YGG_ROOT /slot- $i .lock "
if flock -n " $lf " ; then LEASE_FDS += ( " $lf " ) ; K = $(( K + 1 )) ; else exec { lf} >& -; fi
done
}
# run_stage <stage> <count> <fn>: K workers claim items 1..count by mkdir, in order (largest first)
run_stage( ) {
local stage = $1 count = $2 fn = $3 w pids = ( )
for w in $( seq 1 " $K " ) ; do
( for lf in " ${ LEASE_FDS [@] } " ; do exec { lf} >& -; done # the lease belongs to the driver, never to a child (2026-09-23 inherited-fd scar)
i = 0
while [ " $i " -lt " $count " ] ; do
i = $(( i + 1 ))
killed && break
[ ! -e " $D /failed " ] || break
mkdir " $D /q/ $stage - $i " 2>/dev/null || continue
" $fn " " $i "
done ) &
pids += ( $! )
done
wait " ${ pids [@] } "
}
container_error( ) { # container_error <label> <out dir>
local rc; rc = $( cat " $2 /hostrc " 2>/dev/null)
sum " ERROR $1 container exited ${ rc :- ? } (4 is a red baseline: the unmutated tests failed there) "
2026-11-03 01:19:30 +01:00
tail -n 25 " $2 /stdout " 2>/dev/null | sed " s|^| $SUMTAG : ERROR | "
2026-11-01 19:15:38 +01:00
}
2026-11-01 20:57:04 +01:00
# ---- stage 1: files whose filter selects tests (the plan container derived each file's filter and counted its tests)
2026-11-01 19:15:38 +01:00
S1_FILE = ( ) ; S1_FILTER = ( ) ; s1n = 0
if [ " ${ CM_NAMES :- } " != 1 ] ; then
2026-11-01 20:57:04 +01:00
while IFS = $'\t' read -r cnt file f t; do # files by mutant count, largest first
[ " $t " != 0 ] || continue # a filter that selects no test settles nothing: straight to stage 2
2026-11-01 19:15:38 +01:00
s1n = $(( s1n + 1 )) ; S1_FILE[ $s1n ] = $file ; S1_FILTER[ $s1n ] = $f
awk -F: -v f = " $file " '$1 == f' " $D /m/universe " >" $D /items/s1- $s1n .names "
2026-11-01 20:57:04 +01:00
done <" $D /plan/filters.txt "
2026-11-01 19:15:38 +01:00
fi
s1_item( ) {
local n = $1 rc
MODE = --item drun " $YGG_CNAME -s1- $n " " $D /items/s1- $n /out " -e CM_STAGE = 1 -e " CM_FILE= ${ S1_FILE [ $n ] } " -e " CM_FILTER= ${ S1_FILTER [ $n ] } "
rc = $?
if [ " $rc " != 0 ] && ! killed; then touch " $D /failed " ; fi
}
K1 = 0; ts1 = 0
if [ " $s1n " -gt 0 ] ; then
ts1 = $( now)
lease " $s1n " ; K1 = $K
run_stage s1 " $s1n " s1_item
unlease
killed && killed_exit
ts1 = $(( $( now) - ts1 ))
bad = 0
for n in $( seq 1 " $s1n " ) ; do
o = " $D /items/s1- $n /out "
if [ " $( cat " $o /hostrc " 2>/dev/null) " != 0 ] ; then
[ -e " $D /q/s1- $n " ] || continue # never started: the job is already red for another file
container_error " stage 1 ( ${ S1_FILE [ $n ] } ) " " $o " ; bad = 1; continue
fi
done
if [ " $bad " != 0 ] || [ -e " $D /failed " ] ; then exit 1; fi
fi
# FILTER lines: the anomalies (0 tests) first, then the rest, at most 12 in all
2026-11-01 20:57:04 +01:00
if [ " ${ CM_NAMES :- } " != 1 ] ; then
{ awk -F'\t' '$4 == 0' " $D /plan/filters.txt " ; awk -F'\t' '$4 != 0' " $D /plan/filters.txt " ; } |
awk -F'\t' '{ if (NR <= 12) printf "FILTER %s %s tests=%s mutants=%s\n", $2, $3, $4, $1; else h++ } END { if (h) printf "FILTER ... %d more files\n", h }' |
while IFS = read -r l; do sum " $l " ; done
fi
2026-11-01 19:15:38 +01:00
for n in $( seq 1 " $s1n " ) ; do
2026-11-03 01:50:23 +01:00
o = " $D /items/s1- $n /out "
2026-11-01 19:15:38 +01:00
cat " $D /items/s1- $n .names " >>" $D /m/s1.names "
cat " $o /caught.txt " >>" $D /m/s1.caught " ; cat " $o /unviable.txt " >>" $D /m/s1.unviable "
cat " $o /missed.txt " " $o /timeout.txt " >>" $D /m/s1.surv "
done
# only names of this run's range count; stage 1 settles caught and unviable, nothing else
for f in s1.caught s1.unviable s1.surv s1.names; do
sort -u " $D /m/ $f " | comm -12 - " $D /m/universe " >" $D /m/ $f .t " && mv " $D /m/ $f .t " " $D /m/ $f "
done
cat " $D /m/s1.caught " " $D /m/s1.unviable " | sort -u >" $D /m/s1.settled "
comm -23 " $D /m/universe " " $D /m/s1.settled " >" $D /s2.txt "
2026-11-01 20:57:04 +01:00
# Stage 2 always runs, so cargo-mutants' full-suite baseline always does (cargo-mutants runs no baseline for an
# empty selection). With every name settled it carries one name; the merge drops that name's outcome.
if [ -s " $D /s2.txt " ] ; then bash " $D /cargo-mutants.sh " --escape <" $D /s2.txt " >" $D /s2.re "
else head -n 1 " $D /m/universe " | bash " $D /cargo-mutants.sh " --escape >" $D /s2.re " ; fi
2026-11-01 19:15:38 +01:00
# ---- stage 2: the full suite on everything stage 1 did not settle
2026-11-01 20:57:04 +01:00
s2n = $( wc -l <" $D /s2.re " | tr -d " " )
2026-11-01 19:15:38 +01:00
K2 = 0; ts2 = 0
for l in caught missed timeout unviable; do : >" $D /m/s2. $l " ; done
s2_shard( ) {
local k = $1 rc
MODE = --item drun " $YGG_CNAME -s2- $k " " $D /items/s2- $k /out " -e CM_STAGE = 2 -e " CM_SHARD= $(( k - 1 )) / $K2 "
rc = $?
if [ " $rc " != 0 ] && ! killed; then touch " $D /failed " ; fi
}
if [ " $s2n " -gt 0 ] ; then
ts2 = $( now)
lease " $s2n " ; K2 = $K
run_stage s2 " $K2 " s2_shard
unlease
killed && killed_exit
ts2 = $(( $( now) - ts2 ))
bad = 0
for k in $( seq 1 " $K2 " ) ; do
2026-11-03 01:50:23 +01:00
o = " $D /items/s2- $k /out "
2026-11-01 19:15:38 +01:00
if [ " $( cat " $o /hostrc " 2>/dev/null) " != 0 ] ; then
[ -e " $D /q/s2- $k " ] || continue
container_error " stage 2 (shard $k of $K2 ) " " $o " ; bad = 1; continue
fi
for l in caught missed timeout unviable; do cat " $o / $l .txt " >>" $D /m/s2. $l " ; done
done
if [ " $bad " != 0 ] || [ -e " $D /failed " ] ; then exit 1; fi
fi
# ---- merge: a name's outcome is stage 2's where it ran, else stage 1's; each name once
for l in caught missed timeout unviable; do
sort -u " $D /m/s2. $l " | comm -12 - " $D /s2.txt " >" $D /m/s2. $l .t " && mv " $D /m/s2. $l .t " " $D /m/s2. $l "
done
cat " $D /m/s2.caught " " $D /m/s2.missed " " $D /m/s2.timeout " " $D /m/s2.unviable " | sort -u >" $D /m/s2.all "
nout = $( comm -23 " $D /s2.txt " " $D /m/s2.all " | wc -l | tr -d ' ' )
if [ " $nout " != 0 ] ; then
sum " ERROR $nout mutants selected for the full-suite run have no outcome in any shard (a name unknown to cargo-mutants at this revision, or a shard that died) "
exit 1
fi
C = $( cat " $D /m/s1.caught " " $D /m/s2.caught " | sort -u | wc -l | tr -d ' ' )
U = $( cat " $D /m/s1.unviable " " $D /m/s2.unviable " | sort -u | wc -l | tr -d ' ' )
M = $( wc -l <" $D /m/s2.missed " | tr -d ' ' ) ; T = $( wc -l <" $D /m/s2.timeout " | tr -d ' ' )
S = $( wc -l <" $D /m/s1.surv " | tr -d ' ' ) ; F = $( wc -l <" $D /m/s1.names " | tr -d ' ' )
X = $( comm -12 " $D /m/s1.surv " " $D /m/s2.caught " | wc -l | tr -d ' ' )
K = $K1 ; [ " $K2 " -le " $K " ] || K = $K2
2026-11-01 19:43:21 +01:00
sum " MUTANTS tested= $(( C + M + T + U)) caught= $C missed= $M timeout= $T unviable= $U | stage1 filtered= $F survivors= $S confirmed_caught= $X | containers= $K | plan= ${ tp } s stage1= ${ ts1 } s/ ${ K1 } stage2= ${ ts2 } s/ ${ K2 } | $( awk -v e = " $(( $( now) - t0 )) " 'BEGIN { printf "%.1f", e / 60 }' ) m "
2026-11-01 19:15:38 +01:00
{ sed 's/^/MISSED /' " $D /m/s2.missed " ; sed 's/^/TIMEOUT /' " $D /m/s2.timeout " ; } >" $D /m/surv.txt "
total = $( wc -l <" $D /m/surv.txt " | tr -d ' ' )
head -n 50 " $D /m/surv.txt " | while IFS = read -r l; do sum " $l " ; done
if [ " $total " -gt 50 ] ; then
sum " ... $(( total - 50 )) more MISSED/TIMEOUT names; they follow in the job log as cm-survivor lines "
tail -n +51 " $D /m/surv.txt " | sed 's/^/cm-survivor: /'
fi
exit 0
fi
# ---- inside a container -----------------------------------------------------
set +e
# overridable only so tools/test-tools.sh can run this half against a mock
SRC = ${ CM_SRC :- /src } ; CM = ${ CM_DIR :- /cm } ; OUT = ${ CM_OUT :- /out } ; SEED = ${ CM_SEED :- /seed }
if [ -d " $SEED " ] ; then mkdir -p " $SRC " && cp -a " $SEED /. " " $SRC / " || { echo " cannot copy $SEED into $SRC " ; exit 2; } ; fi
cd " $SRC / ${ WORKDIR :- . } " || { echo "BAD WORKDIR" ; exit 2; }
2026-11-01 20:57:04 +01:00
set -f; read -ra targs <<< " ${ TEST_ARGS :- } " ; set +f
2026-11-01 19:15:38 +01:00
if [ " $1 " = --plan ] ; then
wsroot = $( dirname " $( cargo locate-project --workspace --message-format plain) " ) || { echo "cargo locate-project failed" ; exit 2; }
git -C " $wsroot " diff --relative " $BASE " HEAD >" $OUT /d " || { echo "git diff of the range failed" ; exit 2; }
cargo mutants --list --in-diff " $OUT /d " >" $OUT /names.txt " 2>" $OUT /list.err " || { echo "cargo mutants --list failed" ; tail -n 5 " $OUT /list.err " ; exit 2; }
2026-11-01 20:57:04 +01:00
: >" $OUT /filters.txt "
[ " $CM_NAMES " != 1 ] || exit 0
# stage 1's plan: each touched file's test filter and how many tests it selects (0 = nothing for stage 1 to run)
declare -A fover
if [ -n " ${ FILTERS :- } " ] ; then
IFS = , read -ra fl <<< " $FILTERS "
for e in " ${ fl [@] } " ; do fover[ ${ e %%=* } ] = ${ e #*= } ; done
fi
derive_filter( ) { # derive_filter <path>: the test filter of a file, nothing for the full suite
local p = $1 rest
if [ -n " ${ fover [ $p ]+x } " ] ; then [ " ${ fover [ $p ] } " = - ] || echo " ${ fover [ $p ] } " ; return 0; fi
case $p in */src/*.rs| src/*.rs) ; ; *) return 0 ; ; esac
rest = ${ p ##*src/ } ; rest = ${ rest %.rs }
case $rest in lib| main| bin/*| */bin/*) return 0 ; ; esac
rest = ${ rest %/mod }
echo " ${ rest // \/ / : : } :: "
}
built = 0
while IFS = $'\t' read -r cnt file; do # files by mutant count, largest first
f = $( derive_filter " $file " )
[ -n " $f " ] || continue
if [ " $built " = 0 ] ; then
cargo test --no-run >" $OUT /build.log " 2>& 1 || { echo "cargo test --no-run failed" ; tail -n 5 " $OUT /build.log " ; exit 2; }
built = 1
fi
t = $( cargo test -- " $f " " ${ targs [@] } " --list 2>/dev/null | awk '/^[0-9]+ tests?, / { s += $1 } END { print s + 0 }' )
printf '%s\t%s\t%s\t%s\n' " $cnt " " $file " " $f " " $t " >>" $OUT /filters.txt "
done < <( awk -F: '{ c[$1]++ } END { for (f in c) print c[f] "\t" f }' " $OUT /names.txt " | sort -t " $( printf '\t' ) " -k1,1nr -k2)
2026-11-01 19:15:38 +01:00
exit 0
fi
# --item: stage 1 (CM_FILE with its CM_FILTER) or stage 2 (shard CM_SHARD of the names in $CM/s2.re)
o = ${ CM_TMP :- /tmp } /cmo
2026-11-01 20:57:04 +01:00
args = ( mutants --in-diff " $CM /plan/d " -o " $o " --jobs " $JOBS " --minimum-test-timeout " $MIN_TEST_TIMEOUT " )
2026-11-01 19:15:38 +01:00
tail_args = ( )
if [ " $CM_STAGE " = 1 ] ; then
2026-11-01 20:57:04 +01:00
args += ( -f " $CM_FILE " )
2026-11-01 19:15:38 +01:00
tail_args = ( -- " $CM_FILTER " ) ; [ " ${# targs [@] } " = 0 ] || tail_args += ( -- " ${ targs [@] } " )
else
mapfile -t res <" $CM /s2.re "
for r in " ${ res [@] } " ; do args += ( -F " $r " ) ; done
args += ( --timeout-multiplier " $CONFIRM_TIMEOUT_MULT " )
case $CM_SHARD in */1) ; ; *) args += ( --shard " $CM_SHARD " ) ; ; esac
[ " ${# targs [@] } " = 0 ] || tail_args = ( -- -- " ${ targs [@] } " )
fi
2026-11-03 01:50:23 +01:00
cargo " ${ args [@] } " " ${ tail_args [@] } " >" $OUT /cargo.log " 2>& 1
rc = $?
2026-11-01 19:15:38 +01:00
for l in caught missed timeout unviable; do cp " $o /mutants.out/ $l .txt " " $OUT / $l .txt " 2>/dev/null || : >" $OUT / $l .txt " ; done
echo " $rc " >" $OUT /rc "
tail -n 30 " $OUT /cargo.log "
# 0 clean, 2 missed and 3 timeouts are normal; 4 (red baseline) and everything else are errors
case $rc in 0| 2| 3) exit 0 ; ; *) exit " $rc " ; ; esac