Skip to content

Commit a65dd13

Browse files
authored
Merge pull request #24 from freebatteryfactory/phase5-b5-interruption-evidence
Make mutation evidence interruption-honest
2 parents b838b45 + 8234bc9 commit a65dd13

6 files changed

Lines changed: 722 additions & 71 deletions

File tree

‎.cargo/mutants.toml‎

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

74+
# CHOSEN: the changed code is compiled in cargo-mutants' VCS-free scratch copy.
75+
# Copying `.git` would make the mutant a tracked difference, so committed-
76+
# snapshot tests would catch every mutation for the same unrelated reason. The
77+
# launcher supplies a separate exact clean subject root, commit, and tree to
78+
# tests instead; no mutation is made in that subject checkout.
79+
copy_vcs = false
80+
7081
# CHOSEN: cap the lints, and this is the setting that decides what the run
7182
# MEASURES. `.cargo/config.toml` makes a surviving warning fatal, so without
7283
# this a mutant that merely trips `unused_variables` or `clippy::let_and_return`
@@ -78,15 +89,6 @@ examine_globs = ["xtask/**/*.rs"]
7889
# a scratch directory and no build of this tree.
7990
cap_lints = true
8091

81-
# CHOSEN: write the report inside `target`, which is already ignored. The
82-
# default puts `mutants.out` beside `Cargo.toml`, and the qualification road
83-
# ends by refusing a checkout that does not match what is committed — so the
84-
# default would make a working machine's mutation run fail the next `cargo xtask
85-
# qualify` for a reason that has nothing to do with the tree. A tool that writes
86-
# into the checkout is a tool that has to be remembered; one that writes into
87-
# `target` does not.
88-
output = "target"
89-
9092
# NOT SET, ON THE RECORD: `test_tool`. nextest would run these suites faster,
9193
# and `.config/nextest.toml` configures it two directories away. It is left at
9294
# cargo deliberately: a mutation result must not depend on whether the second

‎.github/workflows/mutation.yml‎

Lines changed: 83 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
# run reads. This file is the caller.
88
#
99
# WHAT A RED HERE MEANS. Nothing about a change in flight, because no change can
10-
# trigger this workflow. A red means either that the alarm did not operate or
11-
# that a timed-out mutant left its outcome uncertain. SURVIVING MUTANTS DO NOT
12-
# TURN IT RED. They are the finding, they are printed in full, and they are
13-
# evidence debt owed at the home of the source that carries them; a job that
14-
# went red on the first survivor would be red from the day it landed, and an
15-
# alarm that is always red is an alarm nobody reads.
10+
# trigger this workflow. A red means that the alarm did not operate, that a
11+
# timed-out mutant left its outcome uncertain, or that external interruption
12+
# prevented a final observation. SURVIVING MUTANTS DO NOT TURN IT RED. They are
13+
# the finding, they are printed in full, and they are evidence debt owed at the
14+
# home of the source that carries them; a job that went red on the first
15+
# survivor would be red from the day it landed, and an alarm that is always red
16+
# is an alarm nobody reads.
1617
name: mutation
1718

1819
on:
@@ -34,7 +35,68 @@ concurrency:
3435
permissions:
3536
contents: read
3637

38+
env:
39+
CARGO_MUTANTS_VERSION: "27.0.0"
40+
3741
jobs:
42+
# THE PLANTED REVERSAL. This is a sibling of the campaign, with no `needs`
43+
# edge in either direction. A campaign runner that disappears therefore
44+
# cannot erase the cheap observation that the empty scope is still empty.
45+
# Whole-workflow cancellation may stop both jobs; no workflow topology can
46+
# make a running job survive cancellation of its own run.
47+
empty-scope-reversal:
48+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
49+
runs-on: ubuntu-latest
50+
# The reversal builds no baseline after its deliberately empty selection.
51+
# This is an operational hang ceiling, not a population claim.
52+
timeout-minutes: 30
53+
steps:
54+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
55+
with:
56+
persist-credentials: false
57+
58+
- name: Install the pinned toolchain
59+
run: |
60+
rustup toolchain install
61+
rustup show
62+
63+
- name: Install cargo-mutants
64+
run: cargo install cargo-mutants --version "$CARGO_MUTANTS_VERSION" --locked
65+
66+
- name: Record the exact reversal basis
67+
run: |
68+
echo "repository SHA: $(git rev-parse HEAD)"
69+
echo "repository tree: $(git rev-parse HEAD^{tree})"
70+
cargo mutants --version
71+
72+
# The real campaign's positive control cannot establish that its empty-
73+
# scope guard bites: a run that silently selects nothing also exits zero.
74+
# Excluding every source selected by `.cargo/mutants.toml` plants that
75+
# exact defect. cargo-mutants v27 writes an empty inventory and empty
76+
# rosters but no outcomes document, and the typed adapter admits exactly
77+
# that producer posture.
78+
- name: The planted reversal must examine nothing
79+
run: |
80+
mkdir -p target
81+
set +e
82+
cargo mutants --workspace -e 'xtask/**' --output target/mutation-reversal
83+
code=$?
84+
set -e
85+
cargo xtask mutation-report reversal "$code" target/mutation-reversal/mutants.out
86+
87+
# This says only that the reversal's checkout remained clean. It is not
88+
# evidence about the independent campaign runner or its checkout.
89+
- name: The reversal checkout remains clean
90+
if: always()
91+
run: |
92+
dirty=$(git status --porcelain=v2 --untracked-files=all)
93+
if [ -n "$dirty" ]; then
94+
echo "::error::the empty-scope reversal changed Git-visible checkout bytes" >&2
95+
printf '%s\n' "$dirty" >&2
96+
exit 1
97+
fi
98+
echo "the empty-scope reversal left its Git-visible checkout clean"
99+
38100
xtask:
39101
# NAMED BY WHAT IT RUNS ON, never by what it skips, and the triggers above
40102
# already allow nothing else — so this condition is redundant TODAY and is
@@ -64,7 +126,7 @@ jobs:
64126
# decision deferred to whoever resolves next. This is the version MEASURED
65127
# on a working machine when `.cargo/mutants.toml` was written.
66128
- name: Install cargo-mutants
67-
run: cargo install cargo-mutants --version 27.0.0 --locked
129+
run: cargo install cargo-mutants --version "$CARGO_MUTANTS_VERSION" --locked
68130

69131
- name: Record the exact evidence basis
70132
run: |
@@ -82,56 +144,27 @@ jobs:
82144
# this step counts what was examined instead of trusting that something
83145
# was.
84146
#
85-
# The typed report adapter owns the producer's exit-code and report
86-
# contract. In particular, survivors are evidence debt and keep the alarm
87-
# green after validation, while a timeout prints its full receipt and then
88-
# leaves this step red because the outcome is uncertain.
147+
# The fixed xtask launcher first validates this clean checkout as the
148+
# campaign's exact subject, then passes its absolute root, commit, and tree
149+
# to tests compiled in cargo-mutants' VCS-free scratch copy. It owns the
150+
# producer exit and invokes the typed report adapter afterward. Survivors
151+
# are evidence debt and keep the alarm green after validation, while a
152+
# timeout prints its full receipt and then leaves this step red because
153+
# the outcome is uncertain.
89154
#
90-
# `-j 2` builds in two scratch directories at once. More would trade the
91-
# runner's disk for a speed this job does not need, since nothing waits on
92-
# it.
93155
- name: The mutation run
94-
run: |
95-
mkdir -p target
96-
set +e
97-
cargo mutants --workspace -j 2 --output target/mutation-run
98-
code=$?
99-
set -e
100-
cargo xtask mutation-report run "$code" target/mutation-run/mutants.out
101-
102-
# THE PLANTED REVERSAL. The step above establishes that the alarm worked
103-
# today. It cannot establish that its own guard bites, because a guard that
104-
# quietly stopped counting prints the same numbers as one that counted
105-
# everything — and what it guards against costs nothing by MEASUREMENT: a
106-
# scope matching no source finds no mutant, warns, writes empty rosters,
107-
# and exits 0.
108-
#
109-
# So the same run is made deliberately wrong, by excluding every source the
110-
# scope selects, and this step fails the job when that run comes back with
111-
# a mutant. It writes to its own report directory so the finding above is
112-
# not overwritten by a run that found nothing. MEASURED: it finishes in
113-
# seconds, because a run with nothing to test builds no baseline.
114-
#
115-
# WHAT THIS COVERS: that an empty scope really is silent, and that a run
116-
# over one is distinguishable from a run over the real one by the typed
117-
# report contract both roads consume.
118-
- name: The planted reversal must examine nothing
119-
if: always()
120-
run: |
121-
mkdir -p target
122-
set +e
123-
cargo mutants --workspace -e 'xtask/**' --output target/mutation-reversal
124-
code=$?
125-
set -e
126-
cargo xtask mutation-report reversal "$code" target/mutation-reversal/mutants.out
156+
run: cargo xtask mutation-campaign
127157

128-
- name: The checkout remains clean
158+
# This can run after an ordinary command refusal. It cannot run after the
159+
# hosted runner itself disappears, so cleanliness is claimed only when
160+
# this step has an executed record.
161+
- name: The campaign checkout remains clean
129162
if: always()
130163
run: |
131164
dirty=$(git status --porcelain=v2 --untracked-files=all)
132165
if [ -n "$dirty" ]; then
133-
echo "::error::the mutation alarm changed Git-visible checkout bytes" >&2
166+
echo "::error::the mutation campaign changed Git-visible checkout bytes" >&2
134167
printf '%s\n' "$dirty" >&2
135168
exit 1
136169
fi
137-
echo "the mutation alarm left the Git-visible checkout clean"
170+
echo "the mutation campaign left its Git-visible checkout clean"

‎xtask/src/main.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
//! checks themselves.
2828
2929
mod checks;
30+
mod mutation_campaign;
3031
mod mutation_report;
3132
mod repository;
3233
mod qualification;
@@ -62,6 +63,7 @@ fn main() -> Result<(), Box<dyn Error>> {
6263
};
6364
match command.as_str() {
6465
"check" => run_checks(&root),
66+
"mutation-campaign" => mutation_campaign::run(&root, std::env::args().skip(2)),
6567
"mutation-report" => mutation_report::run(&root, std::env::args().skip(2)),
6668
"qualify" => qualification::qualify(&root, run_checks),
6769
other => Err(format!("unknown xtask command: {other}").into()),

0 commit comments

Comments
 (0)