Skip to content

Commit dea5ce2

Browse files
jll63claude
andcommitted
test: stop compile-fail tests racing on the shared build tree
Each compile-fail test runs `cmake --build` on the shared build tree as its test command. When that tree is out of date - someone edited a CMakeLists.txt - all 24 of them re-run CMake at once under `ctest -j N` and corrupt each other's regeneration. On Ninja the losers die with ninja: error: failed recompaction: No such file or directory FAILED: build.ninja before compiling anything, so the expected diagnostic never reaches the output and PASS_REGULAR_EXPRESSION fails. It reproduces in roughly two runs out of three with touch test/CMakeLists.txt; ctest -R compile_fail -j32 and not at all on an up-to-date tree, which is why it looked random. 644c3e2 predicted this ("other generators race analogously on the cmake-regeneration step") but 0b0e7db narrowed the RESOURCE_LOCK to the Visual Studio generator, leaving Ninja exposed. Do the regeneration once, in a CTest fixture, so no test regenerates while another runs. That keeps the 24 compiles parallel, unlike a lock. 10/10 stale-tree runs at -j32 are now clean. The Visual Studio lock stays, and its comment was wrong: the race there is not the regeneration step, so the fixture does not help. Every MSBuild invocation walks the whole project dependency graph and rewrites the .tlog/.lastbuildstate of each project it passes through, the dependencies' as well as ZERO_CHECK's, so concurrent invocations collide whether or not anything needs regenerating. Measured with VS 18 2026: 7-13 of 24 fail on every run without the lock, 3/3 clean with it. Windows users who want these parallel should configure with -G Ninja, which is clean at -j32 and ~14x faster. b2 is unaffected: it compiles these sources as ordinary targets in its own dependency graph rather than shelling out to a nested build. ~40 runs at -j32/-j64/-j128 found nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e5ce8f5 commit dea5ce2

2 files changed

Lines changed: 59 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,28 @@ comment above the marker - `no matching` (clang/gcc "no matching function for ca
9999
matching overloaded function found"), `deleted function` (gcc "use of a", clang "call to", MSVC
100100
"attempting to reference a").
101101

102+
**Do not let the compile-fail tests regenerate the build tree concurrently.** Each one runs
103+
`cmake --build` on the shared tree as its test command, so when the tree is stale they all re-run
104+
CMake at once and corrupt each other - on Ninja the losers die with `failed recompaction` /
105+
`FAILED: build.ninja` before compiling anything, the expected diagnostic never appears, and the
106+
test fails. It reproduces about two runs in three with `touch test/CMakeLists.txt; ctest -R
107+
compile_fail -j32`, and not at all on an up-to-date tree, which is why it reads as random. The
108+
empty `boost_openmethod-compile_fail_fixture` target plus `FIXTURES_SETUP`/`FIXTURES_REQUIRED`
109+
does the regeneration once, before any of them.
110+
111+
**The Visual Studio `RESOURCE_LOCK` is still needed on top - do not remove it.** MSBuild is not
112+
fixable by the fixture: every one of the 24 concurrent invocations walks the same project
113+
dependency graph and stomps the same `.tlog`/`.lastbuildstate` files, not just `ZERO_CHECK`'s.
114+
Measured with VS 18 2026 + `ctest -R compile_fail -j32`: without the lock, 7-13 of 24 fail on
115+
*every* run; with it, 3/3 clean at 24s (serialized). The lock is scoped to the generator, so
116+
**Windows already runs these fully parallel under `-G Ninja`** - 4/4 clean, 1.65s, same
117+
`cl.exe`. That is the fast path on Windows; the generator cannot be defaulted from CMakeLists.txt
118+
anyway (it is fixed before the file is read - only a preset or `CMAKE_GENERATOR` in the
119+
environment can set it), and CI picks its own.
120+
121+
b2 is not affected - it compiles these sources as ordinary targets in its own dependency graph
122+
instead of shelling out to a nested build (~40 runs at `-j32`/`-j64`/`-j128` are clean).
123+
102124
**b2 cannot check the message - do not try to make it.** `test/Jamfile` already loops
103125
(`compile-fail $(src)`), but Boost.Build's `compile-fail` only inverts the exit status: the
104126
`expect-failure-generator` sets `T_FLAG_FAIL_EXPECTED` and the engine flips OK/FAIL

test/CMakeLists.txt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ target_link_libraries(boost_openmethod-test_mix_release_debug PRIVATE Boost::ope
108108
boost_openmethod_add_test(boost_openmethod-test_mix_release_debug)
109109
add_dependencies(tests boost_openmethod-test_mix_release_debug)
110110

111+
# Every compile-fail test runs `cmake --build` on the shared build tree. When
112+
# that tree is out of date - someone edited a CMakeLists.txt - each of them
113+
# re-runs CMake, and under `ctest -j N` those regenerations run concurrently and
114+
# corrupt each other. On Ninja the losers die with
115+
#
116+
# ninja: error: failed recompaction: No such file or directory
117+
# FAILED: build.ninja
118+
#
119+
# before compiling anything, so the expected diagnostic never appears and the
120+
# test fails - reproducibly, in roughly two runs out of three. (Unix Makefiles
121+
# survived the same stress, but the window is not specific to Ninja, so close it
122+
# for every generator.) Bring the tree up to date once, in a CTest fixture, so
123+
# no test regenerates while another is running. The target is empty: building it
124+
# does nothing except make the generator refresh its build files first. CTest
125+
# pulls a required fixture's setup test in automatically, so a narrower
126+
# `ctest -R ...` selection still gets it.
127+
add_custom_target(boost_openmethod-compile_fail_fixture)
128+
add_test(
129+
NAME boost_openmethod-compile_fail_fixture
130+
COMMAND "${CMAKE_COMMAND}" --build ${CMAKE_BINARY_DIR}
131+
--target boost_openmethod-compile_fail_fixture --config $<CONFIG>)
132+
set_property(
133+
TEST boost_openmethod-compile_fail_fixture
134+
PROPERTY FIXTURES_SETUP openmethod_build_tree)
135+
111136
function(openmethod_compile_fail_test testname fail_regex)
112137
set(test_target "boost_openmethod-${testname}")
113138
add_library(${test_target} STATIC EXCLUDE_FROM_ALL "${testname}.cpp")
@@ -116,15 +141,19 @@ function(openmethod_compile_fail_test testname fail_regex)
116141
NAME "${test_target}"
117142
COMMAND "${CMAKE_COMMAND}" --build ${CMAKE_BINARY_DIR} --target "${test_target}" --config $<CONFIG>)
118143
set_property(TEST "${test_target}" PROPERTY PASS_REGULAR_EXPRESSION "${fail_regex}")
119-
# Each of these tests runs `cmake --build` on the shared build tree. With
120-
# the Visual Studio generator, concurrent MSBuild invocations race on shared
121-
# files (every MSBuild invocation rewrites ZERO_CHECK.tlog/
122-
# ZERO_CHECK.lastbuildstate, and the losers abort with MSB3491 before
123-
# compiling anything, so the expected diagnostic never appears in the
124-
# output). Serialize the compile-fail tests among themselves in that case;
144+
set_property(TEST "${test_target}" PROPERTY FIXTURES_REQUIRED openmethod_build_tree)
145+
# The fixture above is enough for Ninja and Makefiles, but not for the
146+
# Visual Studio generator, where the regeneration step is not the problem:
147+
# every MSBuild invocation walks the whole project dependency graph and
148+
# rewrites the .tlog/.lastbuildstate of each project it passes through -
149+
# ZERO_CHECK's and the dependencies' alike - so concurrent invocations
150+
# collide whether or not anything needs regenerating, and the losers abort
151+
# (MSB3374/3375/3491/3501) before compiling anything. Measured with VS 18
152+
# 2026 at `ctest -R compile_fail -j32`: 7-13 of 24 fail on every run without
153+
# the lock, 3/3 runs clean with it. Serialize them among themselves there;
125154
# they still run in parallel with the ordinary tests, which do not touch the
126-
# build tree. Other generators (Ninja, Makefiles) tolerate concurrent
127-
# `cmake --build` invocations, so leave those free to run in parallel.
155+
# build tree. Windows users who want these parallel should configure with
156+
# -G Ninja, which is clean and ~14x faster here.
128157
if (CMAKE_GENERATOR MATCHES "Visual Studio")
129158
set_property(TEST "${test_target}" PROPERTY RESOURCE_LOCK openmethod_compile_fail)
130159
endif()

0 commit comments

Comments
 (0)