Problem Statement
The messrust test suite passes, and the strict self-analysis gate passes. This
tells nobody if the tests detect a change of behaviour. A test can run a code
path and assert almost nothing. Today the repository has no measurement of test
strength, and no gate on it.
The sibling mutarust repository shows the failure mode to avoid. Its own
mutation workflow mutates four files, runs only the --lib suite, and uses
--git-diff-lines with --ignore-msi-with-no-mutations on pull requests. The
printed score is high, but it does not speak for the crate.
The maintainer wants a hard quality gate for messrust production code:
- 75 percent total MSI (mutation score indicator).
- 80 percent covered-code MSI.
- Full production scope, on every push to
main and every pull request.
- No escape hatch. No baseline, no blacklist, no diff-only scope, no disabled
mutators, no continue-on-error.
Solution
Measure the current mutation score of src with the released mutarust
command. Add tests through the existing library seam until the total score is 75
percent or more and the covered-code score is 80 percent or more. Then add a CI
job that runs the same command with --min-msi 75 --min-covered-msi 80. A score
below a threshold returns exit value 4 and fails the build.
Add one fast policy test to cargo test. The test reads the workflow file, the
mutarust.yml policy file, and the production sources. It fails if the
thresholds change, if a forbidden escape-hatch option appears, or if a
mutation-disable annotation appears in production source. The gate then protects
itself.
User Stories
- As a crate maintainer, I want a mutation score gate on production code, so
that a weak test cannot pass review.
- As a crate maintainer, I want the gate set at 75 percent total MSI, so that
the crate meets the "stable library" level in the mutarust CI guide.
- As a crate maintainer, I want the gate set at 80 percent covered-code MSI, so
that covered code is tested strongly, not only executed.
- As a crate maintainer, I want both scores checked in one run, so that a
contributor cannot satisfy one score and ignore the other.
- As a crate maintainer, I want the gate to run on every pull request, so that
a defect cannot enter main.
- As a crate maintainer, I want the gate to run on every push to
main, so
that a merge does not lower the score without a signal.
- As a crate maintainer, I want the gate to mutate all production source in
src, so that no module is outside the measurement.
- As a crate maintainer, I want no baseline file in the repository, so that a
known escaped mutant cannot stay accepted.
- As a crate maintainer, I want no blacklist file in the repository, so that an
accepted checksum cannot hide a mutation.
- As a crate maintainer, I want no
--git-diff-lines scope in the gate, so
that the score always speaks for the full crate.
- As a crate maintainer, I want no
--ignore-msi-with-no-mutations option, so
that an empty mutant set cannot pass the gate.
- As a crate maintainer, I want no disabled mutators, so that a difficult
mutator cannot be removed to raise the score.
- As a crate maintainer, I want no
exclude_dirs and no ignore_source_lines
policy values, so that source cannot leave the scope quietly.
- As a crate maintainer, I want no
mutator-disable annotations in src, so
that a source comment cannot remove a mutation.
- As a crate maintainer, I want no
continue-on-error and no || true in the
workflow, so that a failed gate cannot report success.
- As a crate maintainer, I want the mutation job to be a required status
check, so that a pull request cannot merge with a failed gate.
- As a contributor, I want one documented local command, so that I can measure
the score before I open a pull request.
- As a contributor, I want a committed
mutarust.yml policy file, so that my
local run uses the same thresholds as CI.
- As a contributor, I want escaped mutants shown as GitHub annotations, so
that I can see the exact line to test.
- As a contributor, I want the failure output to name the mutator and the
source line, so that I can write a test without a second run.
- As a contributor, I want the new tests to use the existing
run seam, so
that I do not learn a new test style.
- As a contributor, I want a fast
cargo test suite, so that the mutation run
stays in CI and does not slow local work.
- As an AFK agent, I want a binary success checkpoint, so that I know when the
task is complete.
- As an AFK agent, I want the forbidden option list written down, so that I do
not add an escape hatch to make the gate pass.
- As an AFK agent, I want the instruction to delete unreachable production
code instead of adding a unit test for it, so that I do not add a private
seam.
- As a reviewer, I want a policy test in
cargo test, so that a weakened gate
fails fast and visibly in the diff.
- As a reviewer, I want the thresholds written in two places that must agree,
so that a silent change is not possible.
- As a reviewer, I want a README section about mutation testing, so that a new
contributor finds the rules.
- As a release manager, I want the mutation score measured before a release,
so that a published version has strong tests.
- As a CI operator, I want the mutation job in its own workflow file, so that
the fast test job stays fast.
- As a CI operator, I want a job timeout, so that a stuck run does not hold a
runner for six hours.
- As a CI operator, I want the
mutarust version pinned, so that a new
release of the tool does not change the gate result without a commit.
- As a CI operator, I want actions pinned by commit identifier, so that the
workflow matches the security level of the sibling repository.
- As a maintainer of the test suite, I want the packaging smoke test excluded
from the per-mutant command, so that the run does not do a cargo install
for each mutant.
- As a maintainer of the test suite, I want a written statement that this
exclusion lowers the score and never raises it, so that it is not an escape
hatch.
- As a maintainer, I want the mutation job to fail if the tool cannot collect
coverage, so that the covered-code gate cannot pass without coverage data.
- As a maintainer, I want the score printed in the job log, so that I can see
the margin above the threshold.
Implementation Decisions
Tool and installation
- Use the released
mutarust command from crates.io. Do not add it as a
dependency of messrust.
- Install it in CI with
cargo install mutarust --locked --version <pinned>.
The current release is 0.1.1. A version change is a commit.
- Install
cargo-llvm-cov with --locked, and add the llvm-tools-preview
toolchain component. --coverage needs both.
Scope
- The gate mutates all production source of the crate. Before the workflow is
written, run mutarust --list-files and confirm the output holds every file
in src: analyze.rs, discover.rs, lib.rs, main.rs, metrics.rs,
report.rs, ruleset.rs, suppressions.rs.
- Use the workspace target
. if that output is complete. If the workspace
target selects only the declared lib.rs and main.rs paths, use the
recursive target src... instead. Record the chosen target in the workflow.
- Do not name individual files in the command. A file list becomes stale when a
module is added.
Gate command
- One run does all the work:
--coverage, --min-msi 75,
--min-covered-msi 80, --logger-github.
--coverage is mandatory. A positive covered-score gate without --coverage
returns exit value 4, so the option cannot be dropped quietly.
- Exit value 4 fails the job. The workflow does not trap it.
- Set
--exec-timeout to a value that fits the full suite, and --workers to
the runner CPU count. These options change speed, not the score.
Per-mutant test command
- Each mutant runs the full
cargo test suite, less the packaging smoke test,
with --test-flags "-- --skip pack_install_smoke".
- Reason:
tests/pack_install_smoke.rs runs cargo package and
cargo install. A per-mutant packaging run is recursive and very slow.
- This exclusion removes killing power. It can only lower the measured score.
It is therefore not an escape hatch, and the two thresholds still apply.
- Do not use
--test-flags "--lib". The crate has almost no library unit tests,
so that option would make the gate meaningless.
- Do not add a target list such as
--test cli --test naming. A new test file
would then leave the mutation run without a signal.
Forbidden in the gate
The workflow, the policy file, and the repository must not hold any of these:
--baseline, --fail-on-escaped, mutarust-baseline.json
--blacklist
--ignore-msi-with-no-mutations
--git-diff-lines, --git-diff-base
--match, --run-mutant-id, --dry-run, --no-exec, --exec
disable_mutators, enable_mutators with values
exclude_dirs, ignore_source_lines with values
skip_without_test: true, skip_with_cfg: true
continue-on-error, || true, if: conditions on the mutation step
- a
paths: filter or a branch filter that lets a pull request skip the job
workflow_dispatch as the only trigger
// mutator-disable-func, // mutator-disable-next-line, or
// mutator-disable-regexp in any file under src
Policy file
- Commit
mutarust.yml at the repository root with min_msi: 75,
min_covered_msi: 80, all Boolean fields false, and all lists empty.
- The CI command repeats the same two numbers as command options. A command
option takes priority over the file, so the two values must agree. The policy
test asserts that they agree.
Workflow
- Add
.github/workflows/mutation.yml. Do not put the job in ci.yml, because
the fast test job must stay fast.
- Triggers:
push to main and pull_request. No paths filter.
- Set
timeout-minutes to a value near 120.
- Pin
actions/checkout and dtolnay/rust-toolchain by full commit
identifier, as the sibling repository does.
- The maintainer makes the job a required status check in branch protection.
This is a human step outside the code.
Score work
- Raise the score with new tests through
messrust::run. Do not add a public
function only to make a test possible.
- If a mutant cannot be killed through the command seam, the mutated code is not
reachable from the command. Delete that code. Do not add a #[cfg(test)] unit
test to protect it.
- Do not change rule behaviour to make mutants easier to kill.
Documentation
- Add a "Mutation testing" section to
README.md with the local command, the
two thresholds, and the rule that the thresholds do not move down.
- Add the mutation workflow badge to the README title line.
Testing Decisions
What makes a good test here
A good test drives the command from the outside and asserts what a user sees.
The test builds an argument vector, calls messrust::run, and asserts the exit
code, the standard output text, and the standard error text. It does not call a
private function, and it does not assert an internal data shape. A mutation that
changes the report a user reads must fail such a test.
Seams
- Primary seam, already present:
messrust::run(&[String], &mut dyn Write, &mut dyn Write) -> i32. All new tests use it. Prior art: tests/cli.rs,
tests/codesize.rs, tests/naming.rs, tests/design.rs,
tests/cleancode.rs, tests/controversial.rs, tests/unusedcode.rs.
- Process seam, already present:
tests/pack_install_smoke.rs spawns the
installed binary. Do not add tests here. It is slow.
- New policy seam: the repository files themselves. One new test file, for
example tests/mutation_gate.rs, reads the workflow file, mutarust.yml, and
the files in src as text. Prior art: tests/self_analysis.rs, which runs
the crate policy against the crate itself in cargo test.
No other seam is added.
Modules under test
- Every module in
src is under mutation. analyze.rs is 3269 lines and holds
most of the rule logic, so it needs most of the new tests.
- The report formats in
report.rs (text, json, sarif, github) need assertions
on the exact output text. A format mutation escapes when a test asserts only
the exit code.
ruleset.rs, suppressions.rs, discover.rs, and metrics.rs need tests
for their error paths, because an error message mutation escapes when no test
reads standard error.
Policy test assertions (binary)
mutarust.yml holds min_msi: 75 and min_covered_msi: 80.
- The workflow command holds
--min-msi 75 and --min-covered-msi 80.
- The workflow command holds
--coverage.
- The workflow holds a
pull_request trigger and no paths filter.
- No forbidden token from the list above is in the workflow or the policy file.
- No
mutator-disable comment is in any file under src.
Success checkpoints (binary)
mutarust --list-files <target> lists all eight files in src.
- The full local gate command exits 0, prints a total score of 75 percent or
more, and prints a covered-code score of 80 percent or more.
cargo test passes and includes the new policy test.
cargo clippy --all-targets adds no new warning.
- The mutation job passes on a pull request in GitHub Actions.
- A temporary threshold raise makes the job fail with exit value 4. Revert this
change after the check.
Out of Scope
- A higher threshold than 75 and 80. A later issue can raise them.
- A line-coverage percentage gate. Coverage is collected for the covered-code
score only.
- Mutation of test code.
mutarust does not mutate #[cfg(test)] items, and
the tests directory is not production source.
- Any change to
mutarust itself, or to its own mutation workflow.
- Any change to rule behaviour, to the ruleset catalogue, or to the report
formats, except a change that a mutant proves to be dead code.
- Any change to the existing strict self-analysis gate.
--per-test mode as a requirement. Use it only if the job time needs it.
- Publication of the crate to crates.io.
Further Notes
- Expect the test work to be large.
analyze.rs holds most of the rule logic,
and the current tests assert findings more than they assert exact output.
- Sequence the work as: measure, then write tests, then add the gate. Do not
commit a baseline file as an interim step. A baseline is an escape hatch.
- If the job time is too long, the correct actions are more workers, a longer
timeout, or --per-test. A smaller scope and a lower threshold are not
correct actions.
- The toolchain must be Rust stable 1.85 or newer.
AGENTS.md says Cargo.lock
is not in git; the file is tracked, and the CI jobs use --locked.
mutarust exit values: 0 pass, 1 command error, 3 invalid annotation, 4 gate
failure.
Problem Statement
The
messrusttest suite passes, and the strict self-analysis gate passes. Thistells nobody if the tests detect a change of behaviour. A test can run a code
path and assert almost nothing. Today the repository has no measurement of test
strength, and no gate on it.
The sibling
mutarustrepository shows the failure mode to avoid. Its ownmutation workflow mutates four files, runs only the
--libsuite, and uses--git-diff-lineswith--ignore-msi-with-no-mutationson pull requests. Theprinted score is high, but it does not speak for the crate.
The maintainer wants a hard quality gate for
messrustproduction code:mainand every pull request.mutators, no
continue-on-error.Solution
Measure the current mutation score of
srcwith the releasedmutarustcommand. Add tests through the existing library seam until the total score is 75
percent or more and the covered-code score is 80 percent or more. Then add a CI
job that runs the same command with
--min-msi 75 --min-covered-msi 80. A scorebelow a threshold returns exit value 4 and fails the build.
Add one fast policy test to
cargo test. The test reads the workflow file, themutarust.ymlpolicy file, and the production sources. It fails if thethresholds change, if a forbidden escape-hatch option appears, or if a
mutation-disable annotation appears in production source. The gate then protects
itself.
User Stories
that a weak test cannot pass review.
the crate meets the "stable library" level in the
mutarustCI guide.that covered code is tested strongly, not only executed.
contributor cannot satisfy one score and ignore the other.
a defect cannot enter
main.main, sothat a merge does not lower the score without a signal.
src, so that no module is outside the measurement.known escaped mutant cannot stay accepted.
accepted checksum cannot hide a mutation.
--git-diff-linesscope in the gate, sothat the score always speaks for the full crate.
--ignore-msi-with-no-mutationsoption, sothat an empty mutant set cannot pass the gate.
mutator cannot be removed to raise the score.
exclude_dirsand noignore_source_linespolicy values, so that source cannot leave the scope quietly.
mutator-disableannotations insrc, sothat a source comment cannot remove a mutation.
continue-on-errorand no|| truein theworkflow, so that a failed gate cannot report success.
check, so that a pull request cannot merge with a failed gate.
the score before I open a pull request.
mutarust.ymlpolicy file, so that mylocal run uses the same thresholds as CI.
that I can see the exact line to test.
source line, so that I can write a test without a second run.
runseam, sothat I do not learn a new test style.
cargo testsuite, so that the mutation runstays in CI and does not slow local work.
task is complete.
not add an escape hatch to make the gate pass.
code instead of adding a unit test for it, so that I do not add a private
seam.
cargo test, so that a weakened gatefails fast and visibly in the diff.
so that a silent change is not possible.
contributor finds the rules.
so that a published version has strong tests.
the fast test job stays fast.
runner for six hours.
mutarustversion pinned, so that a newrelease of the tool does not change the gate result without a commit.
workflow matches the security level of the sibling repository.
from the per-mutant command, so that the run does not do a
cargo installfor each mutant.
exclusion lowers the score and never raises it, so that it is not an escape
hatch.
coverage, so that the covered-code gate cannot pass without coverage data.
the margin above the threshold.
Implementation Decisions
Tool and installation
mutarustcommand from crates.io. Do not add it as adependency of
messrust.cargo install mutarust --locked --version <pinned>.The current release is
0.1.1. A version change is a commit.cargo-llvm-covwith--locked, and add thellvm-tools-previewtoolchain component.
--coverageneeds both.Scope
written, run
mutarust --list-filesand confirm the output holds every filein
src:analyze.rs,discover.rs,lib.rs,main.rs,metrics.rs,report.rs,ruleset.rs,suppressions.rs..if that output is complete. If the workspacetarget selects only the declared
lib.rsandmain.rspaths, use therecursive target
src...instead. Record the chosen target in the workflow.module is added.
Gate command
--coverage,--min-msi 75,--min-covered-msi 80,--logger-github.--coverageis mandatory. A positive covered-score gate without--coveragereturns exit value 4, so the option cannot be dropped quietly.
--exec-timeoutto a value that fits the full suite, and--workerstothe runner CPU count. These options change speed, not the score.
Per-mutant test command
cargo testsuite, less the packaging smoke test,with
--test-flags "-- --skip pack_install_smoke".tests/pack_install_smoke.rsrunscargo packageandcargo install. A per-mutant packaging run is recursive and very slow.It is therefore not an escape hatch, and the two thresholds still apply.
--test-flags "--lib". The crate has almost no library unit tests,so that option would make the gate meaningless.
--test cli --test naming. A new test filewould then leave the mutation run without a signal.
Forbidden in the gate
The workflow, the policy file, and the repository must not hold any of these:
--baseline,--fail-on-escaped,mutarust-baseline.json--blacklist--ignore-msi-with-no-mutations--git-diff-lines,--git-diff-base--match,--run-mutant-id,--dry-run,--no-exec,--execdisable_mutators,enable_mutatorswith valuesexclude_dirs,ignore_source_lineswith valuesskip_without_test: true,skip_with_cfg: truecontinue-on-error,|| true,if:conditions on the mutation steppaths:filter or a branch filter that lets a pull request skip the jobworkflow_dispatchas the only trigger// mutator-disable-func,// mutator-disable-next-line, or// mutator-disable-regexpin any file undersrcPolicy file
mutarust.ymlat the repository root withmin_msi: 75,min_covered_msi: 80, all Boolean fieldsfalse, and all lists empty.option takes priority over the file, so the two values must agree. The policy
test asserts that they agree.
Workflow
.github/workflows/mutation.yml. Do not put the job inci.yml, becausethe fast test job must stay fast.
pushtomainandpull_request. Nopathsfilter.timeout-minutesto a value near 120.actions/checkoutanddtolnay/rust-toolchainby full commitidentifier, as the sibling repository does.
This is a human step outside the code.
Score work
messrust::run. Do not add a publicfunction only to make a test possible.
reachable from the command. Delete that code. Do not add a
#[cfg(test)]unittest to protect it.
Documentation
README.mdwith the local command, thetwo thresholds, and the rule that the thresholds do not move down.
Testing Decisions
What makes a good test here
A good test drives the command from the outside and asserts what a user sees.
The test builds an argument vector, calls
messrust::run, and asserts the exitcode, the standard output text, and the standard error text. It does not call a
private function, and it does not assert an internal data shape. A mutation that
changes the report a user reads must fail such a test.
Seams
messrust::run(&[String], &mut dyn Write, &mut dyn Write) -> i32. All new tests use it. Prior art:tests/cli.rs,tests/codesize.rs,tests/naming.rs,tests/design.rs,tests/cleancode.rs,tests/controversial.rs,tests/unusedcode.rs.tests/pack_install_smoke.rsspawns theinstalled binary. Do not add tests here. It is slow.
example
tests/mutation_gate.rs, reads the workflow file,mutarust.yml, andthe files in
srcas text. Prior art:tests/self_analysis.rs, which runsthe crate policy against the crate itself in
cargo test.No other seam is added.
Modules under test
srcis under mutation.analyze.rsis 3269 lines and holdsmost of the rule logic, so it needs most of the new tests.
report.rs(text, json, sarif, github) need assertionson the exact output text. A format mutation escapes when a test asserts only
the exit code.
ruleset.rs,suppressions.rs,discover.rs, andmetrics.rsneed testsfor their error paths, because an error message mutation escapes when no test
reads standard error.
Policy test assertions (binary)
mutarust.ymlholdsmin_msi: 75andmin_covered_msi: 80.--min-msi 75and--min-covered-msi 80.--coverage.pull_requesttrigger and nopathsfilter.mutator-disablecomment is in any file undersrc.Success checkpoints (binary)
mutarust --list-files <target>lists all eight files insrc.more, and prints a covered-code score of 80 percent or more.
cargo testpasses and includes the new policy test.cargo clippy --all-targetsadds no new warning.change after the check.
Out of Scope
score only.
mutarustdoes not mutate#[cfg(test)]items, andthe
testsdirectory is not production source.mutarustitself, or to its own mutation workflow.formats, except a change that a mutant proves to be dead code.
--per-testmode as a requirement. Use it only if the job time needs it.Further Notes
analyze.rsholds most of the rule logic,and the current tests assert findings more than they assert exact output.
commit a baseline file as an interim step. A baseline is an escape hatch.
timeout, or
--per-test. A smaller scope and a lower threshold are notcorrect actions.
AGENTS.mdsaysCargo.lockis not in git; the file is tracked, and the CI jobs use
--locked.mutarustexit values: 0 pass, 1 command error, 3 invalid annotation, 4 gatefailure.