From 34b38fd8b823d6b43baa82f7f16e0c6fbf700a4e Mon Sep 17 00:00:00 2001 From: Yaraslau Tamashevich Date: Sat, 19 Sep 2026 13:43:08 +0200 Subject: [PATCH] test: give equation()'s 70000-node stack-safety test a wider TIMEOUT under sanitizers (fixes #589) It exceeds ctest's blanket 120s cap under TSan (observed >120.07s on master's own CI), whose per-access instrumentation pushes #582's already-measured O(n^2) string-building cost over the line. The test has no performance budget of its own -- it exists only to prove the morph#574 iterative rewrite doesn't overflow the stack -- so this widens its timeout alone via a `[slow]` tag rather than raising the cap for every test, which would defeat the "hang fails fast" property the 120s default exists for. DISCOVERY_MODE PRE_TEST defers test discovery to ctest invocation time, so set_tests_properties() can't target one Catch2 test case directly at configure time; splitting the discovery call by tag is the mechanism that's actually available. Verified: `ctest --show-only=json-v1` shows the tagged test alone at TIMEOUT 600 while its neighbor keeps TIMEOUT 120; the full 1513-test count is unchanged; the test itself still passes. Co-Authored-By: Claude Sonnet 5 --- tests/CMakeLists.txt | 21 +++++++++++++++++++-- tests/test_quantity.cpp | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 45018c8f..6b52681d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -210,8 +210,25 @@ endif() include(Catch) # Every test is sub-second in practice; cap each at 120s so a hung/deadlocked # test fails fast instead of stalling the whole CI job (observed: a single test -# hanging blocked a Windows runner for over half an hour). -catch_discover_tests(morph_tests DISCOVERY_MODE PRE_TEST PROPERTIES TIMEOUT 120) +# hanging blocked a Windows runner for over half an hour). One named exception +# below, registered separately: DISCOVERY_MODE PRE_TEST defers test discovery +# to ctest invocation time, so a per-test TIMEOUT can't be set with +# set_tests_properties() here (nothing named that test yet at configure time) +# -- excluding it by tag and giving the excluded tag its own +# catch_discover_tests() call is the mechanism that's actually available. +catch_discover_tests(morph_tests DISCOVERY_MODE PRE_TEST TEST_SPEC "~[slow]" PROPERTIES TIMEOUT 120) + +# `[slow]` (issue #589): `equation()` walks a 70,000-node provenance chain +# purely to prove the morph#574 iterative rewrite doesn't overflow the stack +# -- it has no performance budget of its own, but the O(n^2) string-building +# cost #582 measured (32.3s at this size under ASan+UBSan) is close enough to +# 120s that TSan's heavier per-access instrumentation pushes it over: observed +# at ">120.07s" on master's own CI (the sanitizer killed it at the deadline, +# so the true completion time is unknown, just past it). Widened generously +# rather than precisely measured, to leave headroom for sanitizer variance, +# without raising the cap for every other test and weakening the "hang fails +# fast" property the 120s default exists for. +catch_discover_tests(morph_tests DISCOVERY_MODE PRE_TEST TEST_SPEC "[slow]" PROPERTIES TIMEOUT 600) # ── Two-binary journal-path skew test (issue #246) ─────────────────────────── # The executable form of the journal's data-at-rest contract, which diff --git a/tests/test_quantity.cpp b/tests/test_quantity.cpp index 93295481..73a937ba 100644 --- a/tests/test_quantity.cpp +++ b/tests/test_quantity.cpp @@ -734,7 +734,7 @@ TEST_CASE("A 100000-node provenance chain is destroyed without overflowing the s } TEST_CASE("equation() walks a 70000-node provenance chain without overflowing the stack", - "[quantity][provenance][equation][morph574]") { + "[quantity][provenance][equation][morph574][slow]") { Euro const total = runningTotal(kDeepEquationNodes); auto const lines = total.equation();