mutate: refuse a file the binary under test is not built from - #205
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:…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 ofsrc/*.cinto a single translation unit, but deliberately notoans.corrun_dedupe.c, which belong to the shipped binary alone — andmake test-buildcompiles onlytests.c. Sweepingsrc/oans.cplanned 1,394 mutants and would have scored all of themsurvived: a 0% kill rate over 2,177 lines, presented as a fact about the test suite.The probe
Append an
#errorto 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 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:
Verification
scripts/test_mutate.pygains six cases, checked the way this repository asks — disabling the refusal makestest_a_file_nothing_includes_is_refusedfail and everything else pass:They cover the refusal, the acceptance, that the probe really puts an
#errorin 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:
src/oans.candsrc/run_dedupe.c; still sweepssrc/util.cnormally and returns real verdicts;make mutation-replayunchanged at 65/65src/include/nanobench.h, baseline builds, mutants scorecaughtas beforeI 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.sha256updated in all three;lint-mutate-core.pypasses in each.🤖 Generated with Claude Code
https://claude.ai/code/session_01GeJoYWWvf2ewg87ih8DpGJ
Generated by Claude Code