Skip to content

mutate: refuse a file the binary under test is not built from - #205

Merged
martinus merged 1 commit into
mainfrom
claude/oans-testing-improvements-wslk93
Aug 22, 2026
Merged

mutate: refuse a file the binary under test is not built from#205
martinus merged 1 commit into
mainfrom
claude/oans-testing-improvements-wslk93

Conversation

@martinus

Copy link
Copy Markdown
Owner

The worst way this tool can be wrong is in the flattering direction, and this was the last way left open.

A file that no test binary includes scores every mutant survived, because the mutation never reaches the binary the tests run against. The report then says:

nothing noticed these - whatever covers them is decoration:
  src/oans.c:2172  == -> !=

…which reads as an indictment of the tests when nothing was ever measured.

Not hypothetical — it is how this was found

oans's src/tests.c #includes most of src/*.c into a single translation unit, but deliberately not oans.c or run_dedupe.c, which belong to the shipped binary alone — and make test-build compiles only tests.c. Sweeping src/oans.c planned 1,394 mutants and would have scored all of them survived: a 0% kill rate over 2,177 lines, presented as a fact about the test suite.

The probe

Append an #error to the target and build. If the build still succeeds, nothing the test binary is made of ever included that file.

That is deterministic and needs to know nothing about the build system, which is what makes it the right shape for a core shared by make, cmake and meson. A compilation database can say which files are compiled, but not which are #included into something that is — and oans's case is exactly the latter, so the cheaper backend-specific check would have missed it.

Two details that are deliberate:

  • It runs before the baseline build, not after, so a refusal leaves the tree with a good build rather than a failed one.
  • A timeout is inconclusive, not a refusal. A slow machine must not become a wrong answer about the tests.

It sits directly beside the check that already refuses a filter matching no test cases, which exists for the same reason and is worded almost the same way:

A filter that matches nothing is green, and every mutant under it survives. Refusing here is the difference between "your filter names nothing" and a report claiming the suite covers nothing.

Verification

scripts/test_mutate.py gains six cases, checked the way this repository asks — disabling the refusal makes test_a_file_nothing_includes_is_refused fail and everything else pass:

FAILED (failures=1)
  test_a_file_nothing_includes_is_refused
  AssertionError: unexpectedly None

They cover the refusal, the acceptance, that the probe really puts an #error in the file (without which the check would pass for the wrong reason on any tree whose build happens to fail), that the file is restored on all three outcomes, and that a timeout does not refuse.

257 tests, green.

End to end on two of the three backends, since the change was developed against make:

backend repo result
make oans refuses src/oans.c and src/run_dedupe.c; still sweeps src/util.c normally and returns real verdicts; make mutation-replay unchanged at 65/65
cmake nanobench accepts src/include/nanobench.h, baseline builds, mutants score caught as before
meson this repo not verified end to end — meson is not installed on this machine. Covered by the hermetic suite only

I would rather state that gap than imply otherwise. The mechanism touches no backend-specific code beyond Backend.build_argv, which the hermetic suite does cover for meson, but that is an argument rather than a measurement.

Sync

Copied into nanobench and oans with mutate_core.sha256 updated in all three; lint-mutate-core.py passes in each.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GeJoYWWvf2ewg87ih8DpGJ


Generated by Claude Code

The worst way this tool can be wrong is in the flattering direction, and
this was the last way left. A file no test binary includes scores every
mutant `survived`, because the mutation never reaches the binary the tests
run - and the report then says "nothing noticed these - whatever covers
them is decoration", which reads as an indictment of the tests when nothing
was ever measured.

Not hypothetical, and it is why this was found: oans's src/tests.c
#includes most of src/*.c into one translation unit but deliberately not
oans.c or run_dedupe.c, which belong to the shipped binary alone, and
`make test-build` compiles only tests.c. Sweeping src/oans.c planned 1,394
mutants and would have scored all of them survived - a 0% kill rate over
2,177 lines, presented as a fact about the tests.

The probe appends an #error to the target and builds. If the build still
succeeds, nothing the test binary is made of ever included the file. That
is deterministic and needs to know nothing about the build system, which is
what makes it the right shape for a core shared by make, cmake and meson: a
compilation database can say which files are *compiled*, but not which are
#included into something that is - and oans's case is exactly the latter.

It runs before the baseline build rather than after, so a refusal leaves
the tree with a good build rather than a failed one, and a timeout is
treated as inconclusive rather than as a refusal: a slow machine must not
become a wrong answer about the tests. It sits beside the check that
already refuses a filter matching no test cases, which exists for the same
reason worded the same way.

Verified end to end on two of the three backends: make (oans refuses
src/oans.c and src/run_dedupe.c, and still sweeps src/util.c normally) and
cmake (nanobench accepts its header and scores mutants `caught` as before).
The meson leg is covered by the hermetic suite only - meson is not
installed on this machine - but the mechanism touches no backend-specific
code beyond Backend.build_argv, which that suite does cover.

scripts/test_mutate.py gains six cases, and they were checked the way this
repository asks: disabling the refusal makes
test_a_file_nothing_includes_is_refused fail and everything else pass. They
cover the refusal, the acceptance, that the probe really puts an #error in
the file rather than passing for some other reason, that the file is
restored on all three outcomes, and that a timeout does not refuse.

257 tests, green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeJoYWWvf2ewg87ih8DpGJ
@martinus
martinus merged commit 63ac634 into main Aug 22, 2026
32 checks passed
@martinus
martinus deleted the claude/oans-testing-improvements-wslk93 branch August 22, 2026 21:55
martinus added a commit to martinus/oans that referenced this pull request Aug 22, 2026
…#240)

Vendored sync of martinus/unordered_dense#205. This is the repository the
bug was found in and the only one of the three that can hit it: src/tests.c
#includes most of src/*.c into one translation unit but deliberately not
oans.c or run_dedupe.c, and make test-build compiles only tests.c.

So a mutant in either file never reached ./test and was scored `survived`.
A dry run over src/oans.c planned 1,394 of them - a 0% kill rate over 2,177
lines, printed as "whatever covers them is decoration", which reads as an
indictment of the tests when nothing was ever measured.

Both are now refused, with a message that names the consequence rather than
the condition. src/util.c still sweeps and still returns real verdicts, and
make mutation-replay is unchanged at 65 across eight files with nothing
surviving.
@martinus
martinus restored the claude/oans-testing-improvements-wslk93 branch August 23, 2026 07:23
@martinus
martinus deleted the claude/oans-testing-improvements-wslk93 branch August 23, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants