Skip to content

ci: every ctest preset sets stopOnFailure, so a leg with six failures reports one — and which one is a race #616

Description

@Yaraslaut

Found while fixing #593, whose own body reports six failing tests while CI on the same commit reported one. The discrepancy is not a difference in behaviour between the two environments. It is this setting, and resolving it took a full local build to do.

The finding

Every test preset in CMakePresets.json sets stopOnFailure:

$ python3 -c "
import json;d=json.load(open('CMakePresets.json'))
for p in d['testPresets']: print(p['name'], p['execution'])
"
cl-debug {'noTestsAction': 'error', 'stopOnFailure': True}
cl-qt-debug {'noTestsAction': 'error', 'stopOnFailure': True}
cl-qt-release {'noTestsAction': 'error', 'stopOnFailure': True}
clangcl-debug {'noTestsAction': 'error', 'stopOnFailure': True}
windows-everything {'noTestsAction': 'error', 'stopOnFailure': True}
gcc-debug {'noTestsAction': 'error', 'stopOnFailure': True}
clang-debug {'noTestsAction': 'error', 'stopOnFailure': True}
linux-everything {'noTestsAction': 'error', 'stopOnFailure': True}
clang-asan {'noTestsAction': 'error', 'stopOnFailure': True}
clang-tsan {'noTestsAction': 'error', 'stopOnFailure': True}
clang-ubsan {'noTestsAction': 'error', 'stopOnFailure': True}
clang-coverage {'noTestsAction': 'error', 'stopOnFailure': True}

All twelve, and .github/workflows/ci.yml drives every leg through ctest --preset ${{ matrix.preset }}. So a leg with six failures reports one, and the failure it reports is whichever one ctest reached first — which depends on the -j scheduling, so it is not even stably the same one across legs.

Measured, on one commit

PR #585 head a08db4f9, CI run 35467846733. Six legs failed. Each reported exactly one failing test:

Linux / clang-asan      1663 - SocketBackend: action result delivered via then (Failed)
Linux / clang-tsan      1663 - SocketBackend: action result delivered via then (Failed)
Linux / clang-ubsan     1669 - SocketBackend: action result delivered via then (Failed)
Linux / clang-coverage  2786 - SocketBackend: action result delivered via then (Failed)

The same branch, rebased onto master a020e69c and run locally without a preset (GCC 16.2.1, Debug, -DMORPH_BUILD_QT=ON -DMORPH_BUILD_NET=ON, ctest -j4):

99% tests passed, 6 tests failed out of 1833

The following tests FAILED:
	1660 - SocketBackend: action result delivered via then (Failed)
	1676 - SocketBackend: executeTimeout surfaces as backend::TimeoutError, not a generic runtime_error (Failed)
	1681 - SocketBackend: many concurrent in-flight executes all resolve, matched by callId (Failed)
	1689 - SocketBackend: server dropping mid-call resolves the pending completion with DisconnectedError (Failed)
	1692 - SocketBackend: two backends share one server with isolated model state (Failed)
	1833 - SocketBackend interop: connects to a QtWebSocketServer and completes an action (Failed)

One cause, six symptoms, and CI showed one of them.

Note that the Valgrind memcheck leg on that same run, which does not go through a test preset, reported all of its five:

tests/net/test_socket_backend.cpp:242: FAILED:
tests/net/test_socket_backend.cpp:299: FAILED:
tests/net/test_socket_backend.cpp:327: FAILED:
tests/net/test_socket_backend.cpp:1241: FAILED:
tests/net/test_socket_backend.cpp:1440: FAILED:
test cases:  189 |  184 passed | 5 failed
==10010== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

That is the shape of report the preset legs suppress.

Why it matters

  1. Blast radius is invisible. "One test fails" and "every test that registers a handler over this transport fails" are different verdicts about whether a PR is one line from green. qt: move QtWebSocketBackend onto the structural registration surface #585's own PR body read the CI logs correctly and still described the failure as singular for three revisions.
  2. Each extra symptom costs a CI cycle. Fix the first failure, push, wait 26–74 minutes, discover the second. With six symptoms that is potentially six cycles to learn what one uninterrupted run would have printed.
  3. It interacts badly with -j. The reported test is a race, so two runs of the same red commit can name different tests and look like two different regressions.

What it is not

stopOnFailure is a defensible setting for a leg whose later tests are meaningless once an early one fails, and for one that is expensive to run to completion. This ticket is not "delete it everywhere" — it is that the twelve presets adopt it uniformly and nothing records why, while the one leg that does not use a preset produces a strictly more useful report at apparently no cost (that leg is Valgrind, the slowest in the matrix, which is evidence against "it is there to save time").

Verification status

Reproduced. The preset listing above is real output from the tree at a020e69c. The CI counts are read from run 35467846733's job logs, not inferred. The six-failure local run is measured, on the rebased branch, with the build configuration stated.

Not measured: how much wall-clock a full run costs on each leg versus a stop-at-first — i.e. the actual price of turning it off. Somebody proposing a change should measure that, per leg, before changing all twelve.

What would change the verdict

  • Close this if a leg-by-leg measurement shows running to completion materially lengthens the matrix and the current behaviour is a deliberate trade — but then it wants a comment in CMakePresets.json saying so, because it is currently unexplained.
  • Close this if ctest's --stop-on-failure can be left on while still printing the full failing set, which would make this a reporting fix rather than a policy one. I did not check whether it can.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AbwhcguQFkhvVi2AH19sWk

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ciSubsystem: cibugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions