Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,27 @@
# every file here is xtask's.
#
# THE ONE THING THIS LINE CANNOT SAY is which PACKAGES a run examines — the
# configuration has no key for it, so the selection is an argument. MEASURED:
# `cargo mutants` with no selection takes the root package alone, this glob
# matches nothing inside it, and the run finds no mutant and exits 0. It does say
# so, on a WARN line — and a warning that fails nothing is what a job reads as
# success, which is the whole difference between a diagnostic and a refusal.
# `cargo mutants --workspace` is therefore the invocation, on a working machine
# exactly as in the hosted job, and the job COUNTS what it examined rather than
# trusting that it examined something.
# configuration has no key for it. `cargo xtask mutation-campaign` owns the one
# admitted positive campaign invocation and supplies `--workspace`; it accepts
# no pass-through arguments. The sole raw invocation is the workflow's planted
# empty-scope reversal, whose deliberately excluded source population challenges
# the report guard rather than producing campaign evidence. MEASURED: a raw
# `cargo mutants` with no package selection takes the root package alone, this
# glob matches nothing inside it, and the run finds no mutant and exits 0. It
# does say so, on a WARN line — and a warning that fails nothing is what a job
# reads as success, which is the whole difference between a diagnostic and a
# refusal. The fixed launcher also binds scratch-compiled tests to the exact
# clean commit and tree they judge, so neither workflow shell nor cargo-mutants'
# current directory becomes repository identity.
examine_globs = ["xtask/**/*.rs"]

# CHOSEN: the changed code is compiled in cargo-mutants' VCS-free scratch copy.
# Copying `.git` would make the mutant a tracked difference, so committed-
# snapshot tests would catch every mutation for the same unrelated reason. The
# launcher supplies a separate exact clean subject root, commit, and tree to
# tests instead; no mutation is made in that subject checkout.
copy_vcs = false

# CHOSEN: cap the lints, and this is the setting that decides what the run
# MEASURES. `.cargo/config.toml` makes a surviving warning fatal, so without
# this a mutant that merely trips `unused_variables` or `clippy::let_and_return`
Expand All @@ -78,15 +89,6 @@ examine_globs = ["xtask/**/*.rs"]
# a scratch directory and no build of this tree.
cap_lints = true

# CHOSEN: write the report inside `target`, which is already ignored. The
# default puts `mutants.out` beside `Cargo.toml`, and the qualification road
# ends by refusing a checkout that does not match what is committed — so the
# default would make a working machine's mutation run fail the next `cargo xtask
# qualify` for a reason that has nothing to do with the tree. A tool that writes
# into the checkout is a tool that has to be remembered; one that writes into
# `target` does not.
output = "target"

# NOT SET, ON THE RECORD: `test_tool`. nextest would run these suites faster,
# and `.config/nextest.toml` configures it two directories away. It is left at
# cargo deliberately: a mutation result must not depend on whether the second
Expand Down
133 changes: 83 additions & 50 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
# run reads. This file is the caller.
#
# WHAT A RED HERE MEANS. Nothing about a change in flight, because no change can
# trigger this workflow. A red means either that the alarm did not operate or
# that a timed-out mutant left its outcome uncertain. SURVIVING MUTANTS DO NOT
# TURN IT RED. They are the finding, they are printed in full, and they are
# evidence debt owed at the home of the source that carries them; a job that
# went red on the first survivor would be red from the day it landed, and an
# alarm that is always red is an alarm nobody reads.
# trigger this workflow. A red means that the alarm did not operate, that a
# timed-out mutant left its outcome uncertain, or that external interruption
# prevented a final observation. SURVIVING MUTANTS DO NOT TURN IT RED. They are
# the finding, they are printed in full, and they are evidence debt owed at the
# home of the source that carries them; a job that went red on the first
# survivor would be red from the day it landed, and an alarm that is always red
# is an alarm nobody reads.
name: mutation

on:
Expand All @@ -34,7 +35,68 @@ concurrency:
permissions:
contents: read

env:
CARGO_MUTANTS_VERSION: "27.0.0"

jobs:
# THE PLANTED REVERSAL. This is a sibling of the campaign, with no `needs`
# edge in either direction. A campaign runner that disappears therefore
# cannot erase the cheap observation that the empty scope is still empty.
# Whole-workflow cancellation may stop both jobs; no workflow topology can
# make a running job survive cancellation of its own run.
empty-scope-reversal:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
# The reversal builds no baseline after its deliberately empty selection.
# This is an operational hang ceiling, not a population claim.
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Install the pinned toolchain
run: |
rustup toolchain install
rustup show

- name: Install cargo-mutants
run: cargo install cargo-mutants --version "$CARGO_MUTANTS_VERSION" --locked

- name: Record the exact reversal basis
run: |
echo "repository SHA: $(git rev-parse HEAD)"
echo "repository tree: $(git rev-parse HEAD^{tree})"
cargo mutants --version

# The real campaign's positive control cannot establish that its empty-
# scope guard bites: a run that silently selects nothing also exits zero.
# Excluding every source selected by `.cargo/mutants.toml` plants that
# exact defect. cargo-mutants v27 writes an empty inventory and empty
# rosters but no outcomes document, and the typed adapter admits exactly
# that producer posture.
- name: The planted reversal must examine nothing
run: |
mkdir -p target
set +e
cargo mutants --workspace -e 'xtask/**' --output target/mutation-reversal
code=$?
set -e
cargo xtask mutation-report reversal "$code" target/mutation-reversal/mutants.out

# This says only that the reversal's checkout remained clean. It is not
# evidence about the independent campaign runner or its checkout.
- name: The reversal checkout remains clean
if: always()
run: |
dirty=$(git status --porcelain=v2 --untracked-files=all)
if [ -n "$dirty" ]; then
echo "::error::the empty-scope reversal changed Git-visible checkout bytes" >&2
printf '%s\n' "$dirty" >&2
exit 1
fi
echo "the empty-scope reversal left its Git-visible checkout clean"

xtask:
# NAMED BY WHAT IT RUNS ON, never by what it skips, and the triggers above
# already allow nothing else — so this condition is redundant TODAY and is
Expand Down Expand Up @@ -64,7 +126,7 @@ jobs:
# decision deferred to whoever resolves next. This is the version MEASURED
# on a working machine when `.cargo/mutants.toml` was written.
- name: Install cargo-mutants
run: cargo install cargo-mutants --version 27.0.0 --locked
run: cargo install cargo-mutants --version "$CARGO_MUTANTS_VERSION" --locked

- name: Record the exact evidence basis
run: |
Expand All @@ -82,56 +144,27 @@ jobs:
# this step counts what was examined instead of trusting that something
# was.
#
# The typed report adapter owns the producer's exit-code and report
# contract. In particular, survivors are evidence debt and keep the alarm
# green after validation, while a timeout prints its full receipt and then
# leaves this step red because the outcome is uncertain.
# The fixed xtask launcher first validates this clean checkout as the
# campaign's exact subject, then passes its absolute root, commit, and tree
# to tests compiled in cargo-mutants' VCS-free scratch copy. It owns the
# producer exit and invokes the typed report adapter afterward. Survivors
# are evidence debt and keep the alarm green after validation, while a
# timeout prints its full receipt and then leaves this step red because
# the outcome is uncertain.
#
# `-j 2` builds in two scratch directories at once. More would trade the
# runner's disk for a speed this job does not need, since nothing waits on
# it.
- name: The mutation run
run: |
mkdir -p target
set +e
cargo mutants --workspace -j 2 --output target/mutation-run
code=$?
set -e
cargo xtask mutation-report run "$code" target/mutation-run/mutants.out

# THE PLANTED REVERSAL. The step above establishes that the alarm worked
# today. It cannot establish that its own guard bites, because a guard that
# quietly stopped counting prints the same numbers as one that counted
# everything — and what it guards against costs nothing by MEASUREMENT: a
# scope matching no source finds no mutant, warns, writes empty rosters,
# and exits 0.
#
# So the same run is made deliberately wrong, by excluding every source the
# scope selects, and this step fails the job when that run comes back with
# a mutant. It writes to its own report directory so the finding above is
# not overwritten by a run that found nothing. MEASURED: it finishes in
# seconds, because a run with nothing to test builds no baseline.
#
# WHAT THIS COVERS: that an empty scope really is silent, and that a run
# over one is distinguishable from a run over the real one by the typed
# report contract both roads consume.
- name: The planted reversal must examine nothing
if: always()
run: |
mkdir -p target
set +e
cargo mutants --workspace -e 'xtask/**' --output target/mutation-reversal
code=$?
set -e
cargo xtask mutation-report reversal "$code" target/mutation-reversal/mutants.out
run: cargo xtask mutation-campaign

- name: The checkout remains clean
# This can run after an ordinary command refusal. It cannot run after the
# hosted runner itself disappears, so cleanliness is claimed only when
# this step has an executed record.
- name: The campaign checkout remains clean
if: always()
run: |
dirty=$(git status --porcelain=v2 --untracked-files=all)
if [ -n "$dirty" ]; then
echo "::error::the mutation alarm changed Git-visible checkout bytes" >&2
echo "::error::the mutation campaign changed Git-visible checkout bytes" >&2
printf '%s\n' "$dirty" >&2
exit 1
fi
echo "the mutation alarm left the Git-visible checkout clean"
echo "the mutation campaign left its Git-visible checkout clean"
2 changes: 2 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
//! checks themselves.

mod checks;
mod mutation_campaign;
Comment thread
heyoub marked this conversation as resolved.
mod mutation_report;
mod repository;
mod qualification;
Expand Down Expand Up @@ -62,6 +63,7 @@ fn main() -> Result<(), Box<dyn Error>> {
};
match command.as_str() {
"check" => run_checks(&root),
"mutation-campaign" => mutation_campaign::run(&root, std::env::args().skip(2)),
"mutation-report" => mutation_report::run(&root, std::env::args().skip(2)),
"qualify" => qualification::qualify(&root, run_checks),
other => Err(format!("unknown xtask command: {other}").into()),
Expand Down
Loading
Loading