1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
name: mutants
# Mutation testing in two modes, because the single nightly this replaces
# produced nothing at all.
#
# Its one run in history was CANCELLED at the GitHub Actions six-hour
# ceiling: no `mutants.toml`, no `[package.metadata.mutants]`, so it
# enumerated mutants across the whole tree with a rebuild each and could
# never finish. `continue-on-error: true` meant it never went red either,
# and the artifact step uploaded `mutants.out/` from a killed run. So the
# signal this project explicitly chose OVER coverage-as-a-gate — on the
# reasoning that coverage answers "what is untested" and this codebase is
# past that, into "are the tests load-bearing" — was emitting nothing.
#
# Worth automating at all because the hand process fails in ways that look
# like success: a `sed` written against the pre-`fmt` shape silently
# no-ops and the test "passes" a mutation never applied; a mutation that
# doesn't compile prints no `test result:` line and reads as a pass; and a
# test filter matching zero tests reports `ok. 0 passed`.
# `scripts/mutate.sh` guards a single experiment against all three.
on:
pull_request:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
# The one that matters. `--in-diff` mutates only the lines this PR
# touched, which is minutes rather than hours, and asks exactly the
# question worth asking at review time: is the test that came with this
# change load-bearing? Blocking, because on a diff-sized surface a
# surviving mutant is nearly always a real gap rather than an
# equivalent-mutant argument.
in-diff:
name: cargo-mutants (changed lines)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-mutants --locked
- name: Diff against the merge base
run: |
set -euo pipefail
base="${{ github.event.pull_request.base.sha }}"
git diff "$base" -- 'src/**/*.rs' > pr.diff
wc -l < pr.diff | xargs echo "diff lines:"
- name: Mutate the changed lines
run: |
set -euo pipefail
# No Rust changes in this PR — nothing to mutate, and an empty
# diff must not read as a pass.
if [ ! -s pr.diff ]; then
echo "no src/**/*.rs changes; nothing to mutate"
exit 0
fi
cargo mutants --in-diff pr.diff --no-shuffle --timeout 120 -- --lib
- uses: actions/upload-artifact@v7
if: always()
with:
name: mutants-in-diff
path: mutants.out/
retention-days: 14
# Whole-tree sweep, sharded so each shard finishes. Non-blocking:
# across the full surface, surviving mutants are a reading list, and
# turning them into a build failure means either chasing every
# equivalent mutant or switching the job off — and the second is what
# actually happens.
full:
name: cargo-mutants (shard ${{ matrix.shard }}/8)
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 320
continue-on-error: true
strategy:
fail-fast: false
matrix:
shard:
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo install cargo-mutants --locked
- name: Mutate this shard
run: cargo mutants --shard ${{ matrix.shard }}/8 --no-shuffle --timeout 120 -- --lib
- uses: actions/upload-artifact@v7
if: always()
with:
name: mutants-shard-${{ matrix.shard }}
path: mutants.out/
retention-days: 14