Instrumentation: Write ctest snippet on interrupted test run

Extend the interrupt-flush mechanism already used for cmakeBuild and
cmakeInstall to ctest, so that a user interrupt (e.g. Ctrl+C) still writes
the overall ctest instrumentation snippet, marked with interruptSignal,
before exiting.

On interrupt CTest stops scheduling further tests at every launch site,
skips the postCTest hook, forces a non-zero exit status, and preserves the
checkpoint file so that a later 'ctest -F' can resume the interrupted test
set.

Issue: #27859
This commit is contained in:
Daksh Mamodiya
2026-07-13 20:20:45 +02:00
parent 7738491600
commit d5d2741414
15 changed files with 313 additions and 17 deletions

View File

@@ -456,9 +456,9 @@ Snippet files have a filename with the syntax
The integer signal number that interrupted the command before it completed The integer signal number that interrupted the command before it completed
(for example ``2`` for ``SIGINT`` from Ctrl+C). Only included when ``role`` (for example ``2`` for ``SIGINT`` from Ctrl+C). Only included when ``role``
is ``cmakeBuild`` or ``cmakeInstall`` and the command was interrupted. is ``cmakeBuild``, ``cmakeInstall``, or ``ctest`` and the command was
Consumers can use the presence of this field to distinguish an interrupted interrupted. Consumers can use the presence of this field to distinguish an
command from one that ran to completion. interrupted command from one that ran to completion.
Only available as of data version ``1.2``. Only available as of data version ``1.2``.

View File

@@ -237,7 +237,7 @@
}, },
"interruptSignal": { "interruptSignal": {
"type": "integer", "type": "integer",
"description": "The signal number that interrupted the command before it completed. Only included when role is cmakeBuild or cmakeInstall and the command was interrupted.", "description": "The signal number that interrupted the command before it completed. Only included when role is cmakeBuild, cmakeInstall, or ctest and the command was interrupted.",
"minimum": 1 "minimum": 1
}, },
"role": { "role": {

View File

@@ -11,3 +11,8 @@ instrumentation-interrupt
* Similarly, :manual:`cmake-instrumentation(7)` API now records an overall * Similarly, :manual:`cmake-instrumentation(7)` API now records an overall
``cmakeInstall`` snippet, marked with the same ``interruptSignal`` field, ``cmakeInstall`` snippet, marked with the same ``interruptSignal`` field,
when a :option:`cmake --install` invocation is interrupted by the user. when a :option:`cmake --install` invocation is interrupted by the user.
* Similarly, :manual:`cmake-instrumentation(7)` API now records an overall
``ctest`` snippet, marked with the same ``interruptSignal`` field, when a
:manual:`ctest(1)` invocation is interrupted by the user. On interrupt CTest
stops scheduling further tests and preserves its checkpoint file so that a
later :option:`ctest -F` can resume the interrupted test set.

View File

@@ -35,6 +35,7 @@
#include "cmCTestBinPacker.h" #include "cmCTestBinPacker.h"
#include "cmCTestRunTest.h" #include "cmCTestRunTest.h"
#include "cmCTestTestHandler.h" #include "cmCTestTestHandler.h"
#include "cmInstrumentationInterrupt.h"
#include "cmJSONState.h" #include "cmJSONState.h"
#include "cmListFileCache.h" #include "cmListFileCache.h"
#include "cmRange.h" #include "cmRange.h"
@@ -246,13 +247,21 @@ void cmCTestMultiProcessHandler::RunTests()
uv_run(this->Loop, UV_RUN_DEFAULT); uv_run(this->Loop, UV_RUN_DEFAULT);
this->FinalizeLoop(); this->FinalizeLoop();
if (!this->StopTimePassed && !this->CheckStopOnFailure()) { // A user interrupt (e.g. Ctrl+C) deliberately stops scheduling with tests
// still pending, so skip the completion asserts in that case. Canceled
// launches release their resources, so AllResourcesAvailable() still holds.
if (!this->StopTimePassed && !this->CheckStopOnFailure() &&
cmInstrumentationInterrupt::PendingInterruptSignal() == 0) {
assert(this->Complete()); assert(this->Complete());
assert(this->PendingTests.empty()); assert(this->PendingTests.empty());
} }
assert(this->AllResourcesAvailable()); assert(this->AllResourcesAvailable());
this->MarkFinished(); // On interrupt, leave the checkpoint file intact so a later `ctest -F` can
// resume from where this run left off; MarkFinished() would delete it.
if (cmInstrumentationInterrupt::PendingInterruptSignal() == 0) {
this->MarkFinished();
}
this->UpdateCostData(); this->UpdateCostData();
} }
@@ -583,6 +592,17 @@ void cmCTestMultiProcessHandler::JobServerReceivedToken()
assert(!this->JobServerQueuedTests.empty()); assert(!this->JobServerQueuedTests.empty());
int test = this->JobServerQueuedTests.front(); int test = this->JobServerQueuedTests.front();
this->JobServerQueuedTests.pop_front(); this->JobServerQueuedTests.pop_front();
if (cmInstrumentationInterrupt::PendingInterruptSignal() != 0) {
// Interrupted (e.g. Ctrl+C): do not launch this queued test. Its
// resources were locked when it was queued in StartNextTests, and this
// callback runs holding a job server token, so release both to keep the
// scheduler's bookkeeping balanced (see the AllResourcesAvailable() and
// token invariants checked after the loop).
this->DeallocateResources(test);
this->UnlockResources(test);
this->JobServerClient->ReleaseToken();
return;
}
this->StartTestProcess(test); this->StartTestProcess(test);
} }
@@ -594,7 +614,11 @@ void cmCTestMultiProcessHandler::StartNextTests()
this->StartNextTestsOnTimer_.stop(); this->StartNextTestsOnTimer_.stop();
if (this->PendingTests.empty() || this->CheckStopTimePassed() || if (this->PendingTests.empty() || this->CheckStopTimePassed() ||
(this->CheckStopOnFailure() && !this->Failed->empty())) { (this->CheckStopOnFailure() && !this->Failed->empty()) ||
cmInstrumentationInterrupt::PendingInterruptSignal() != 0) {
// A user interrupt (e.g. Ctrl+C) stops scheduling: launch no further
// tests. Tests already running receive the interrupt too and unwind on
// their own.
return; return;
} }
@@ -650,6 +674,7 @@ void cmCTestMultiProcessHandler::StartNextTests()
// Start tests in the preferred order, each subject to readiness checks. // Start tests in the preferred order, each subject to readiness checks.
auto ti = this->OrderedTests.begin(); auto ti = this->OrderedTests.begin();
while (numToStart > 0 && !this->SerialTestRunning && while (numToStart > 0 && !this->SerialTestRunning &&
cmInstrumentationInterrupt::PendingInterruptSignal() == 0 &&
ti != this->OrderedTests.end()) { ti != this->OrderedTests.end()) {
// Increment the test iterator now because the current list // Increment the test iterator now because the current list
// entry may be deleted below. // entry may be deleted below.
@@ -791,6 +816,7 @@ void cmCTestMultiProcessHandler::FinishTestProcess(
} }
if (started) { if (started) {
if (!this->StopTimePassed && if (!this->StopTimePassed &&
cmInstrumentationInterrupt::PendingInterruptSignal() == 0 &&
cmCTestRunTest::StartAgain(std::move(runner), this->Completed)) { cmCTestRunTest::StartAgain(std::move(runner), this->Completed)) {
this->Completed--; // remove the completed test because run again this->Completed--; // remove the completed test because run again
return; return;
@@ -807,7 +833,12 @@ void cmCTestMultiProcessHandler::FinishTestProcess(
t.second.Depends.erase(test); t.second.Depends.erase(test);
} }
this->WriteCheckpoint(test); // A test killed by the interrupt (e.g. Ctrl+C) never truly finished, so do
// not record it in the checkpoint; otherwise `ctest -F` would skip it when
// resuming this interrupted run.
if (cmInstrumentationInterrupt::PendingInterruptSignal() == 0) {
this->WriteCheckpoint(test);
}
this->DeallocateResources(test); this->DeallocateResources(test);
this->UnlockResources(test); this->UnlockResources(test);

View File

@@ -52,6 +52,7 @@
#include "cmGeneratedFileStream.h" #include "cmGeneratedFileStream.h"
#include "cmGlobalGenerator.h" #include "cmGlobalGenerator.h"
#include "cmInstrumentation.h" #include "cmInstrumentation.h"
#include "cmInstrumentationInterrupt.h"
#include "cmInstrumentationQuery.h" #include "cmInstrumentationQuery.h"
#include "cmJSONState.h" #include "cmJSONState.h"
#include "cmList.h" #include "cmList.h"
@@ -2758,12 +2759,40 @@ int cmCTest::ExecuteTests(std::vector<std::string> const& args)
}; };
std::map<std::string, std::string> data; std::map<std::string, std::string> data;
data["showOnly"] = this->GetShowOnly() ? "1" : "0"; data["showOnly"] = this->GetShowOnly() ? "1" : "0";
int ret = instrumentation.InstrumentCommand( // Run the tests under an interrupt handler so that a user interrupt (e.g.
"ctest", args, // Ctrl+C) still writes the overall `ctest` snippet before we exit.
[processHandler]() -> cmInstrumentation::CommandResult { cmInstrumentationInterrupt::InterruptOutcome testsOutcome =
return { processHandler(), cm::nullopt, cm::nullopt }; cmInstrumentationInterrupt::HandleInterrupt(
}, instrumentation.HasQuery(),
data); [&instrumentation, &args, &processHandler, &data]() -> int {
return instrumentation.InstrumentCommand(
"ctest", args,
[&processHandler]() -> cmInstrumentation::CommandResult {
return { processHandler(), cm::nullopt, cm::nullopt };
},
data);
});
int ret = testsOutcome.ExitCode;
if (testsOutcome.Interrupted) {
// The tests were interrupted and the `ctest` snippet has been written.
// Skip the post-ctest indexing hook and make the exit status reflect the
// interrupt the same way it does without instrumentation, so enabling
// instrumentation does not change the exit code of an interrupted run.
// For a real signal, re-raise it on POSIX (the shell then reports
// 128+signo); on Windows our console handler suppressed the OS default, so
// report the status Windows itself uses for a Ctrl+C-terminated process.
// A test-injected interrupt has no real signal, so it instead returns a
// normal error status to keep the seam case deterministic.
if (testsOutcome.ShouldRaise) {
cmInstrumentationInterrupt::RaiseInterrupt(testsOutcome.Signal);
#ifdef _WIN32
// STATUS_CONTROL_C_EXIT: the exit status Windows reports for a process
// terminated by Ctrl+C, which our console handler otherwise suppressed.
return static_cast<int>(0xC000013A);
#endif
}
return cmCTest::TEST_ERRORS;
}
instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostCTest); instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostCTest);
if (ret == cmCTest::TEST_ERRORS) { if (ret == cmCTest::TEST_ERRORS) {
cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest\n"); cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest\n");

View File

@@ -840,13 +840,13 @@ int cmInstrumentation::InstrumentCommand(
} }
this->configureSnippetData.clear(); this->configureSnippetData.clear();
} }
// Write the cmakeBuild/cmakeInstall envelope atomically (temp file + // Write the cmakeBuild/cmakeInstall/ctest envelope atomically (temp file +
// rename). This is the snippet flushed while unwinding from a user // rename). This is the snippet flushed while unwinding from a user
// interrupt, where a second Ctrl+C could otherwise truncate it mid-write; // interrupt, where a second Ctrl+C could otherwise truncate it mid-write;
// the atomic write guarantees it is either absent or complete. Per-step // the atomic write guarantees it is either absent or complete. Per-step
// snippets are never flushed under interrupt and are left non-atomic. // snippets are never flushed under interrupt and are left non-atomic.
bool const atomicEnvelope = bool const atomicEnvelope = command_type == "cmakeBuild" ||
command_type == "cmakeBuild" || command_type == "cmakeInstall"; command_type == "cmakeInstall" || command_type == "ctest";
this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name, this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name,
atomicEnvelope ? Atomic::Yes : Atomic::No); atomicEnvelope ? Atomic::Yes : Atomic::No);
} }

View File

@@ -16,6 +16,9 @@ function(instrument test)
"INSTALL_SEAM" "INSTALL_SEAM"
"INSTALL_INTERRUPT" "INSTALL_INTERRUPT"
"TEST" "TEST"
"CTEST_SEAM"
"CTEST_INTERRUPT"
"CTEST_FAILOVER"
"WORKFLOW" "WORKFLOW"
"COPY_QUERIES" "COPY_QUERIES"
"COPY_QUERIES_GENERATED" "COPY_QUERIES_GENERATED"
@@ -135,6 +138,11 @@ function(instrument test)
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c" "-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c"
"-DINSTALL_INTERRUPT=ON") "-DINSTALL_INTERRUPT=ON")
endif() endif()
if (ARGS_CTEST_INTERRUPT OR ARGS_CTEST_FAILOVER)
list(APPEND ARGS_CONFIGURE_ARGS
"-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c"
"-DCTEST_INTERRUPT=ON")
endif()
set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project) set(RunCMake_TEST_SOURCE_DIR ${RunCMake_SOURCE_DIR}/project)
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG) if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(maybe_CMAKE_BUILD_TYPE -DCMAKE_BUILD_TYPE=Debug) set(maybe_CMAKE_BUILD_TYPE -DCMAKE_BUILD_TYPE=Debug)
@@ -313,6 +321,106 @@ function(instrument test)
if (ARGS_TEST) if (ARGS_TEST)
run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug) run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug)
endif() endif()
if (ARGS_CTEST_SEAM)
# Drive the ctest interrupt path deterministically via the test-only
# injection seam, with no OS signal, so it runs on every generator. First
# run ctest normally so the postCTest hook runs and creates its marker file;
# remove it so its absence after the injected run proves that run's hook was
# skipped.
set(RunCMake_QUIET_ERROR 1)
run_cmake_command(${test}-warmup
${CMAKE_CTEST_COMMAND} . -C Debug)
file(REMOVE ${v1}/postCTest.hook)
# Inject an interrupt (SIGINT == 2) via the undocumented test seam and run
# ctest again; ctest exits with an error status (cmCTest::TEST_ERRORS == 8)
# but writes the interrupted ctest snippet and skips the hook.
set(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT} 2)
set(RunCMake_TEST_EXPECT_RESULT 8)
run_cmake_command(${test}-seam
${CMAKE_CTEST_COMMAND} . -C Debug)
unset(RunCMake_TEST_EXPECT_RESULT)
unset(ENV{__CMAKE_INSTRUMENTATION_TEST_INTERRUPT})
unset(RunCMake_QUIET_ERROR)
endif()
if (ARGS_CTEST_INTERRUPT)
# Build just the interrupt helper so it exists for the interrupted run.
run_cmake_command(${test}-helper
${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild)
file(REMOVE ${v1}/postCTest.hook)
file(REMOVE_RECURSE ${RunCMake_TEST_BINARY_DIR}/ran)
set(helper_dir ${RunCMake_TEST_BINARY_DIR})
if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(helper_dir ${helper_dir}/Debug)
endif()
set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX})
# Interrupt a serial `ctest -j 1` a few seconds in, while the fast test has
# finished and a slow test is in-flight with others pending. The
# instrumented ctest re-raises the signal, so the helper reports exit 42.
# Restrict to the ctest* fixture tests (the project's `test` needs `main`).
set(RunCMake_TEST_OUTPUT_MERGE 1)
set(RunCMake_QUIET_ERROR 1)
run_cmake_command(${test}-signal
${helper} 4
${CMAKE_CTEST_COMMAND} . -C Debug -j 1 -R ctest)
unset(RunCMake_QUIET_ERROR)
unset(RunCMake_TEST_OUTPUT_MERGE)
# Record which tests ran during the interrupted run so the check script can
# assert the pending tests were canceled.
if (EXISTS ${RunCMake_TEST_BINARY_DIR}/ran)
file(RENAME ${RunCMake_TEST_BINARY_DIR}/ran
${RunCMake_TEST_BINARY_DIR}/ran-interrupt)
endif()
endif()
if (ARGS_CTEST_FAILOVER)
# Build just the interrupt helper so it exists for the interrupted run.
run_cmake_command(${test}-helper
${CMAKE_COMMAND} --build . --config Debug --target InterruptBuild)
file(REMOVE ${v1}/postCTest.hook)
file(REMOVE_RECURSE ${RunCMake_TEST_BINARY_DIR}/ran)
set(helper_dir ${RunCMake_TEST_BINARY_DIR})
if (RunCMake_GENERATOR_IS_MULTI_CONFIG)
set(helper_dir ${helper_dir}/Debug)
endif()
set(helper ${helper_dir}/InterruptBuild${CMAKE_EXECUTABLE_SUFFIX})
# Phase 1: interrupt a serial `ctest -j 1` while a slow test is in-flight.
# The fast test finishes (checkpointed); the in-flight slow test is killed
# (deliberately not checkpointed) and the rest stay pending. Restrict to
# the ctest* fixture tests (the project's `test` needs `main`).
set(RunCMake_TEST_OUTPUT_MERGE 1)
set(RunCMake_QUIET_ERROR 1)
run_cmake_command(${test}-signal
${helper} 4
${CMAKE_CTEST_COMMAND} . -C Debug -j 1 -R ctest)
if (EXISTS ${RunCMake_TEST_BINARY_DIR}/ran)
file(RENAME ${RunCMake_TEST_BINARY_DIR}/ran
${RunCMake_TEST_BINARY_DIR}/ran-interrupt)
endif()
# The interrupted run left an un-indexed ctest snippet marked with
# interruptSignal. The resuming run below re-indexes instrumentation, and
# the generic snippet validator rejects that field, so clear the snippet
# data first. The `ctest -F` checkpoint lives under Testing/Temporary and
# is untouched.
file(REMOVE_RECURSE ${v1}/data)
# Phase 2: resume the interrupted test set with `ctest -F`. It must skip
# the already-finished fast test and run the interrupted and pending slow
# tests (run in parallel so resume finishes promptly).
run_cmake_command(${test}-resume
${CMAKE_CTEST_COMMAND} . -C Debug -F -j 4 -R ctest)
unset(RunCMake_QUIET_ERROR)
unset(RunCMake_TEST_OUTPUT_MERGE)
if (EXISTS ${RunCMake_TEST_BINARY_DIR}/ran)
file(RENAME ${RunCMake_TEST_BINARY_DIR}/ran
${RunCMake_TEST_BINARY_DIR}/ran-resume)
endif()
endif()
if (ARGS_MANUAL_HOOK) if (ARGS_MANUAL_HOOK)
run_cmake_command(${test}-index ${CMAKE_CTEST_COMMAND} --collect-instrumentation .) run_cmake_command(${test}-index ${CMAKE_CTEST_COMMAND} --collect-instrumentation .)
endif() endif()
@@ -345,6 +453,18 @@ if (INSTRUMENTATION_INTERRUPT_REAL)
instrument(interrupt-install INSTALL_INTERRUPT instrument(interrupt-install INSTALL_INTERRUPT
CHECK_SCRIPT check-installation-interrupted.cmake CHECK_SCRIPT check-installation-interrupted.cmake
) )
# Interrupt a `ctest` run with a real OS signal, proving the scheduler stops
# launching pending tests, skips the hook, and preserves the `ctest -F`
# checkpoint so the interrupted test set can be resumed.
instrument(interrupt-test CTEST_INTERRUPT
CHECK_SCRIPT check-test-interrupted.cmake
)
# Interrupt a `ctest` run and then resume it with `ctest -F`, proving the
# checkpoint keeps the finished test (skipped on resume) but not the
# in-flight test killed by the interrupt (re-run on resume).
instrument(interrupt-test-failover CTEST_FAILOVER
CHECK_SCRIPT check-test-failover.cmake
)
endif() endif()
return() return()
endif() endif()
@@ -563,6 +683,9 @@ instrument(interrupt-build INTERRUPT_SEAM
instrument(interrupt-install BUILD INSTALL_SEAM instrument(interrupt-install BUILD INSTALL_SEAM
CHECK_SCRIPT check-installation-interrupted.cmake CHECK_SCRIPT check-installation-interrupted.cmake
) )
instrument(interrupt-test BUILD CTEST_SEAM
CHECK_SCRIPT check-test-interrupted.cmake
)
# Test make/ninja hooks # Test make/ninja hooks
if(RunCMake_GENERATOR STREQUAL "FASTBuild") if(RunCMake_GENERATOR STREQUAL "FASTBuild")

View File

@@ -0,0 +1,33 @@
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
# This checks a `ctest -F` resume of an interrupted test set. The overall
# ctest snippet and the postCTest hook are exercised by the seam and
# interrupt-only cases; here the resuming run re-runs (and re-indexes) the
# instrumentation, so only the resume behavior is asserted.
set(ran_interrupt ${RunCMake_TEST_BINARY_DIR}/ran-interrupt)
set(ran_resume ${RunCMake_TEST_BINARY_DIR}/ran-resume)
if (NOT EXISTS ${ran_interrupt})
add_error("Expected the interrupted run to record which tests ran")
elseif (NOT EXISTS ${ran_interrupt}/ctestFast)
add_error("ctestFast should have finished before the interrupt, but did not")
endif()
if (NOT EXISTS ${ran_resume})
add_error("Expected the `ctest -F` resume to record which tests ran")
else ()
# Failover: `ctest -F` must skip the already-finished fast test (it was
# checkpointed) and re-run the interrupted and pending slow tests (the
# in-flight test killed by the interrupt was deliberately not checkpointed).
if (EXISTS ${ran_resume}/ctestFast)
add_error("ctestFast finished before the interrupt and should be skipped "
"by `ctest -F`, but it ran again")
endif()
foreach (n 1 2 3)
if (NOT EXISTS ${ran_resume}/ctestSlow${n})
add_error("ctestSlow${n} was not finished before the interrupt and must "
"run on `ctest -F` resume, but it did not")
endif()
endforeach()
endif()

View File

@@ -0,0 +1,25 @@
include(${CMAKE_CURRENT_LIST_DIR}/json.cmake)
# After an interrupted `ctest`, exactly one ctest snippet should be present,
# marked with the interrupting signal, and the postCTest hook must have been
# skipped.
check_interrupted_snippet(ctest postCTest)
# The real-signal case (CTEST_INTERRUPT) records which tests ran in a marker
# directory. The deterministic seam case runs no tests, so guard on the
# directory's existence.
set(ran_interrupt ${RunCMake_TEST_BINARY_DIR}/ran-interrupt)
if (EXISTS ${ran_interrupt})
# Cancellation: the fast test finished but the pending slow tests must not all
# have started, proving the scheduler stopped launching tests on interrupt.
if (NOT EXISTS ${ran_interrupt}/ctestFast)
add_error("ctestFast should have run before the interrupt, but did not")
endif()
file(GLOB slow_ran LIST_DIRECTORIES false ${ran_interrupt}/ctestSlow*)
list(LENGTH slow_ran num_slow_ran)
if (num_slow_ran GREATER_EQUAL 3)
add_error("Expected some slow tests to remain pending on interrupt, "
"but all ${num_slow_ran} ran: ${slow_ran}")
endif()
endif()

View File

@@ -0,0 +1 @@
42

View File

@@ -89,3 +89,25 @@ if (INSTALL_INTERRUPT)
add_subdirectory(installSlow2) add_subdirectory(installSlow2)
add_subdirectory(installSlow3) add_subdirectory(installSlow3)
endif() endif()
if (CTEST_INTERRUPT)
# A fast test plus several slow tests. A high COST on the fast test makes it
# sort first (CTest orders by descending cost, and on a first run with no
# historical cost data the COST property is used), so with `ctest -j 1` the
# fast test finishes first -- recording a completed test in the checkpoint --
# and then one slow test is in-flight, with the remaining slow tests pending,
# when the interrupt arrives. Each test touches a marker so the check script
# can observe which tests ran. Dependencies are deliberately avoided: on a
# `ctest -F` resume the finished fast test is removed, which would otherwise
# leave its dependents blocked. (InterruptBuild is provided by the
# INTERRUPT_BUILD_SRC block above, which is always enabled together with this.)
set(runtest "${CMAKE_CURRENT_SOURCE_DIR}/runtest.cmake")
add_test(NAME ctestFast COMMAND ${CMAKE_COMMAND}
-DMARKER=${CMAKE_BINARY_DIR}/ran/ctestFast -P ${runtest})
set_tests_properties(ctestFast PROPERTIES COST 100)
foreach (n 1 2 3)
add_test(NAME ctestSlow${n} COMMAND ${CMAKE_COMMAND}
-DMARKER=${CMAKE_BINARY_DIR}/ran/ctestSlow${n} -DSECONDS=10 -P ${runtest})
set_tests_properties(ctestSlow${n} PROPERTIES COST 1)
endforeach()
endif()

View File

@@ -0,0 +1,16 @@
# Test helper run as a CTest test command:
# cmake -DMARKER=<path> [-DSECONDS=<n>] -P runtest.cmake
#
# It records that the test started by touching MARKER, then optionally sleeps
# for SECONDS. The marker lets the interrupt tests observe exactly which tests
# ran (and thus which pending tests were canceled, or which finished tests a
# `ctest -F` resume skipped).
cmake_minimum_required(VERSION 3.30)
cmake_path(GET MARKER PARENT_PATH marker_dir)
file(MAKE_DIRECTORY "${marker_dir}")
file(TOUCH "${MARKER}")
if (SECONDS)
execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep "${SECONDS}")
endif()

View File

@@ -0,0 +1,5 @@
{
"version": 1,
"hooks": ["postCTest"],
"callbacks": ["@GET_HOOK@"]
}

View File

@@ -0,0 +1,5 @@
{
"version": 1,
"hooks": ["postCTest"],
"callbacks": ["@GET_HOOK@"]
}