cmake: let a red leg name every failing test, not just the first (fixes #616) - #617
Merged
Merged
Conversation
#616) All twelve test presets set `stopOnFailure: true`, so `ctest` abandoned the run at the first failure on every CI leg. One broken test and thirty broken tests therefore produced the same log, and every diagnosis made from that log was made on partial information. That cost is not hypothetical: CI on #585's head was read as "all six failing jobs fail on one test", and the singularity was treated as a property of the defect. Six tests were failing; `ctest` stopped at the first, and it took a full local build by someone else to establish that. Decision, per preset: **removed on all twelve.** Each ctest case is its own process here (`catch_discover_tests`), so no leg's later tests are invalidated by an earlier failure -- not the sanitizer legs, where a diagnostic aborts only its own process, and not `clang-coverage`, where stopping early loses profile data rather than protecting it. The "it saves time" argument does not survive the matrix either: the leg that already reports in full is Valgrind, the slowest one there is. A leg that ever does earn the setting can set it on its own preset, with its reason in its own `description`. The twelve presets now inherit a hidden `base-test` preset that carries `output`/`execution` once, so the decision lives in exactly one place with the reasoning next to it, instead of twelve copies of a setting nobody recorded a reason for. `ctest` has no `--no-stop-on-failure`, so keeping it in the preset and overriding it per invocation in CI is not available -- the preset is the only place the choice can be made. No workflow change is needed: every leg runs its suite through `ctest --preset ...`. Measured, gcc-debug on this revision, with two Catch2 cases in tests/test_rational.cpp deliberately broken (reverted before this commit) and the run filtered to `-R "^Rational::"`: # before, stopOnFailure: true 2/46 Test #597: Rational::Comparison ......***Failed 0.00 sec 50% tests passed, 1 tests failed out of 2 The following tests FAILED: 597 - Rational::Comparison (Failed) # after 46/46 Test #641: Rational::toDouble::PrecisionRoundtrip ... Passed 0.00 sec 96% tests passed, 2 tests failed out of 46 The following tests FAILED: 597 - Rational::Comparison (Failed) 599 - Rational::Constants (Failed) Both runs exit 8. Restoring `stopOnFailure` on the inheriting `gcc-debug` preset alone reproduces the one-line report, which is what shows the setting and not something else is responsible. Inheritance was checked rather than assumed. `noTestsAction: error` still reaches each leg through `base-test`: `ctest --preset gcc-debug -R zzz_no_such_test` exits 8 where the same filter without the preset exits 0. `outputOnFailure` likewise -- both failing cases printed their Catch2 bodies in the "after" run above. And CMake merges `execution` key by key across `inherits`: the child that set only `stopOnFailure` still errored on no tests. Full suite green on the new presets: 100% tests passed out of 1562 (gcc-debug, GNU 16.2.1). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk
This was referenced Sep 20, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
All twelve test presets set
stopOnFailure: true, soctestabandoned the run at the first failure on every CI leg. One broken test and thirty broken tests produce the same log, so every diagnosis read off a red leg is made on partial information — as happened on #585, where CI was read as "all six failing jobs fail on one test" and the singularity was taken as a property of the defect. Six tests were failing.fixes #616
What changed
CMakePresets.jsononly. The twelve test presets now inherit a hiddenbase-testpreset that carriesoutput/executiononce, andstopOnFailureis gone from it.The per-preset decision
Removed on all twelve —
cl-debug,cl-qt-debug,cl-qt-release,clangcl-debug,windows-everything,gcc-debug,clang-debug,linux-everything,clang-asan,clang-tsan,clang-ubsan,clang-coverage.The issue is explicit that this is not "delete it everywhere", and a leg whose later tests are meaningless after an early failure would be a legitimate place to keep it. Looking for one:
catch_discover_tests), so an earlier failure does not poison a later case's state on any leg.clang-asan,clang-tsan,clang-ubsan) are the obvious candidates, and they are not it: a sanitizer diagnostic aborts the process it fired in and the next case starts a fresh one. Sanitizer findings also tend to arrive in clusters, which is precisely the case where seeing one of them is worst.clang-coverageis the other candidate — and stopping early loses profile data rather than protecting it. The crashing test has already written its truncated.profraw; abandoning the remaining cases only makes the merged coverage wrong as well.So none of the twelve earns it today. Rather than leaving twelve copies of a setting with no recorded reason, the decision now lives in one
descriptiononbase-test, which also says what a future leg has to do to set it back (set it on its own preset, with its reason in its owndescription).Shape: preset, not per-invocation
The issue offered "keep the preset honest for local one-shot use, pass the opposite in CI" as an alternative. That option does not exist.
ctesthas--stop-on-failureand no negation:(ctest 4.4.3.) The preset is the only place the choice can be made, so the choice is made there. A local one-shot run that wants to stop early still can:
ctest --preset gcc-debug --stop-on-failure, which is the direction that is available per invocation.Verification — a green run proves nothing here
Two Catch2 cases in
tests/test_rational.cppwere deliberately broken, the leg run filtered to-R "^Rational::"(46 cases) ongcc-debug, GNU 16.2.1. The breakage was reverted before the commit; the tree is clean andtests/is untouched by this PR.Before (
stopOnFailure: true, master's presets) — run abandoned at 2 of 46, one test named:After (this PR) — all 46 run, both named:
Both exit 8, so a red leg is still red.
--output-on-failurestill reaches both: the "after" run printed both Catch2 bodies (test_rational.cpp:451: FAILEDandtest_rational.cpp:303: FAILED).Restoring the setting restores the old behaviour, which is what shows this change and nothing else is responsible. Adding
"execution": { "stopOnFailure": true }to the inheritinggcc-debugpreset only, same two broken tests:Inheritance was checked, not assumed
Moving
executioninto a base preset is only safe if the base's settings actually reach the legs, so:noTestsAction: errorstill arrives.ctest --preset gcc-debug -R zzz_no_such_test→No tests were found!!!, exit 8. The same filter without the preset (ctest --test-dir build/gcc-debug -R zzz_no_such_test) exits 0. The check is live.outputOnFailurestill arrives — see the failure bodies in the "after" run above.executionkey by key acrossinherits, not as a whole map: in the restore experiment the child set onlystopOnFailure, and the no-such-test probe still exited 8, sonoTestsActioncame through alongside it. (Measured on CMake 4.4.3; thebase-testdescription records this, because the opposite assumption would be a quiet way to losenoTestsActionlater.)$commentis not an option in this file at all: CMake 4.4.3 rejects it both in a preset (Invalid extra field "$comment" in Preset) and at the root (... in root object).descriptionis the only field that can carry the reasoning, which is why it reads like a comment.Full
gcc-debugsuite on the new presets: 100% tests passed out of 1562, 11.2 s.Stop-on-failure equivalents outside
CMakePresets.jsonChecked, as the issue asked:
--stop-on-failureanywhere in.github/workflows/,scripts/, orcmake/.--abort/-x(abort after N failures) in any test registration or workflow step.ci.ymljob matrices already setfail-fast: false, so a red leg does not cancel its siblings.One equivalent was found and is not fixed here — a job's later test steps are skipped once an earlier one fails, which hides the same kind of blast radius one level up. It is filed separately (see below) rather than folded in, because it needs
ci.ymledits and #605 is queued to touch that file.Not done, deliberately
ci.ymlchange. Every leg runs its suite throughctest --preset ..., so the preset change is sufficient andci.ymlstays free for ci: MORPH_BUILD_BANK_GUI is enabled by no job, and nothing checks that a declared option is built at all #605.CHANGELOG.mdentry. This is CI/tooling reporting behaviour, the same shape as Batch issues per PR by default, and make the cache-stats step measure the job it ran in #611 which took none; and CHANGELOG is the file every in-flight PR conflicts in. Say the word and I will add one.docs/spec/states a stop-on-failure policy (grepped).testing_charter.mdwould be the place to record it, and it is held by ci: audit mutation_survivors.json's line citations instead of hoping (fixes #608) #614 — noted in the follow-up issue.Not verified
gcc-debugwas run locally. The other eleven presets are changed by inheritance, checked byctest --list-presetsandcmake --list-presets=workflowresolving all of them, and by thegcc-debuginheritance probes above — not by running each leg. The Windows and sanitizer legs are unverified locally and rest on CI.🤖 Generated with Claude Code
https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk