diff --git a/Help/manual/cmake-instrumentation.7.rst b/Help/manual/cmake-instrumentation.7.rst index fb6f244b67..09cb706c6a 100644 --- a/Help/manual/cmake-instrumentation.7.rst +++ b/Help/manual/cmake-instrumentation.7.rst @@ -456,9 +456,9 @@ Snippet files have a filename with the syntax The integer signal number that interrupted the command before it completed (for example ``2`` for ``SIGINT`` from Ctrl+C). Only included when ``role`` - is ``cmakeBuild`` or ``cmakeInstall`` and the command was interrupted. - Consumers can use the presence of this field to distinguish an interrupted - command from one that ran to completion. + is ``cmakeBuild``, ``cmakeInstall``, or ``ctest`` and the command was + interrupted. Consumers can use the presence of this field to distinguish an + interrupted command from one that ran to completion. Only available as of data version ``1.2``. diff --git a/Help/manual/instrumentation/snippet-v1-schema.json b/Help/manual/instrumentation/snippet-v1-schema.json index 37731ff1a4..1b7e74049b 100644 --- a/Help/manual/instrumentation/snippet-v1-schema.json +++ b/Help/manual/instrumentation/snippet-v1-schema.json @@ -237,7 +237,7 @@ }, "interruptSignal": { "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 }, "role": { diff --git a/Help/release/dev/instrumentation-interrupt.rst b/Help/release/dev/instrumentation-interrupt.rst index c930f2ced6..37681e723b 100644 --- a/Help/release/dev/instrumentation-interrupt.rst +++ b/Help/release/dev/instrumentation-interrupt.rst @@ -11,3 +11,8 @@ instrumentation-interrupt * Similarly, :manual:`cmake-instrumentation(7)` API now records an overall ``cmakeInstall`` snippet, marked with the same ``interruptSignal`` field, 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. diff --git a/Source/CTest/cmCTestMultiProcessHandler.cxx b/Source/CTest/cmCTestMultiProcessHandler.cxx index 71a3ab4a4a..4954e0d55b 100644 --- a/Source/CTest/cmCTestMultiProcessHandler.cxx +++ b/Source/CTest/cmCTestMultiProcessHandler.cxx @@ -35,6 +35,7 @@ #include "cmCTestBinPacker.h" #include "cmCTestRunTest.h" #include "cmCTestTestHandler.h" +#include "cmInstrumentationInterrupt.h" #include "cmJSONState.h" #include "cmListFileCache.h" #include "cmRange.h" @@ -246,13 +247,21 @@ void cmCTestMultiProcessHandler::RunTests() uv_run(this->Loop, UV_RUN_DEFAULT); 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->PendingTests.empty()); } 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(); } @@ -583,6 +592,17 @@ void cmCTestMultiProcessHandler::JobServerReceivedToken() assert(!this->JobServerQueuedTests.empty()); int test = this->JobServerQueuedTests.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); } @@ -594,7 +614,11 @@ void cmCTestMultiProcessHandler::StartNextTests() this->StartNextTestsOnTimer_.stop(); 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; } @@ -650,6 +674,7 @@ void cmCTestMultiProcessHandler::StartNextTests() // Start tests in the preferred order, each subject to readiness checks. auto ti = this->OrderedTests.begin(); while (numToStart > 0 && !this->SerialTestRunning && + cmInstrumentationInterrupt::PendingInterruptSignal() == 0 && ti != this->OrderedTests.end()) { // Increment the test iterator now because the current list // entry may be deleted below. @@ -791,6 +816,7 @@ void cmCTestMultiProcessHandler::FinishTestProcess( } if (started) { if (!this->StopTimePassed && + cmInstrumentationInterrupt::PendingInterruptSignal() == 0 && cmCTestRunTest::StartAgain(std::move(runner), this->Completed)) { this->Completed--; // remove the completed test because run again return; @@ -807,7 +833,12 @@ void cmCTestMultiProcessHandler::FinishTestProcess( 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->UnlockResources(test); diff --git a/Source/cmCTest.cxx b/Source/cmCTest.cxx index a0f24dbd93..758c61f7a1 100644 --- a/Source/cmCTest.cxx +++ b/Source/cmCTest.cxx @@ -52,6 +52,7 @@ #include "cmGeneratedFileStream.h" #include "cmGlobalGenerator.h" #include "cmInstrumentation.h" +#include "cmInstrumentationInterrupt.h" #include "cmInstrumentationQuery.h" #include "cmJSONState.h" #include "cmList.h" @@ -2758,12 +2759,40 @@ int cmCTest::ExecuteTests(std::vector const& args) }; std::map data; data["showOnly"] = this->GetShowOnly() ? "1" : "0"; - int ret = instrumentation.InstrumentCommand( - "ctest", args, - [processHandler]() -> cmInstrumentation::CommandResult { - return { processHandler(), cm::nullopt, cm::nullopt }; - }, - data); + // Run the tests under an interrupt handler so that a user interrupt (e.g. + // Ctrl+C) still writes the overall `ctest` snippet before we exit. + cmInstrumentationInterrupt::InterruptOutcome testsOutcome = + cmInstrumentationInterrupt::HandleInterrupt( + instrumentation.HasQuery(), + [&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(0xC000013A); +#endif + } + return cmCTest::TEST_ERRORS; + } instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostCTest); if (ret == cmCTest::TEST_ERRORS) { cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest\n"); diff --git a/Source/cmInstrumentation.cxx b/Source/cmInstrumentation.cxx index c50bb90f57..b0a2b6d37e 100644 --- a/Source/cmInstrumentation.cxx +++ b/Source/cmInstrumentation.cxx @@ -840,13 +840,13 @@ int cmInstrumentation::InstrumentCommand( } 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 // interrupt, where a second Ctrl+C could otherwise truncate it mid-write; // the atomic write guarantees it is either absent or complete. Per-step // snippets are never flushed under interrupt and are left non-atomic. - bool const atomicEnvelope = - command_type == "cmakeBuild" || command_type == "cmakeInstall"; + bool const atomicEnvelope = command_type == "cmakeBuild" || + command_type == "cmakeInstall" || command_type == "ctest"; this->WriteInstrumentationJson(latestDataVersion, root, "data", file_name, atomicEnvelope ? Atomic::Yes : Atomic::No); } diff --git a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake index 9a4cd0cc7b..b9a52e2fd2 100644 --- a/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake +++ b/Tests/RunCMake/Instrumentation/RunCMakeTest.cmake @@ -16,6 +16,9 @@ function(instrument test) "INSTALL_SEAM" "INSTALL_INTERRUPT" "TEST" + "CTEST_SEAM" + "CTEST_INTERRUPT" + "CTEST_FAILOVER" "WORKFLOW" "COPY_QUERIES" "COPY_QUERIES_GENERATED" @@ -135,6 +138,11 @@ function(instrument test) "-DINTERRUPT_BUILD_SRC=${RunCMake_SOURCE_DIR}/InterruptBuild.c" "-DINSTALL_INTERRUPT=ON") 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) if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG) set(maybe_CMAKE_BUILD_TYPE -DCMAKE_BUILD_TYPE=Debug) @@ -313,6 +321,106 @@ function(instrument test) if (ARGS_TEST) run_cmake_command(${test}-test ${CMAKE_CTEST_COMMAND} . -C Debug) 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) run_cmake_command(${test}-index ${CMAKE_CTEST_COMMAND} --collect-instrumentation .) endif() @@ -345,6 +453,18 @@ if (INSTRUMENTATION_INTERRUPT_REAL) instrument(interrupt-install INSTALL_INTERRUPT 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() return() endif() @@ -567,6 +687,9 @@ instrument(interrupt-build INTERRUPT_SEAM instrument(interrupt-install BUILD INSTALL_SEAM CHECK_SCRIPT check-installation-interrupted.cmake ) +instrument(interrupt-test BUILD CTEST_SEAM + CHECK_SCRIPT check-test-interrupted.cmake +) # Test make/ninja hooks if(RunCMake_GENERATOR STREQUAL "FASTBuild") diff --git a/Tests/RunCMake/Instrumentation/check-test-failover.cmake b/Tests/RunCMake/Instrumentation/check-test-failover.cmake new file mode 100644 index 0000000000..a723c2198d --- /dev/null +++ b/Tests/RunCMake/Instrumentation/check-test-failover.cmake @@ -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() diff --git a/Tests/RunCMake/Instrumentation/check-test-interrupted.cmake b/Tests/RunCMake/Instrumentation/check-test-interrupted.cmake new file mode 100644 index 0000000000..b0a5cbef74 --- /dev/null +++ b/Tests/RunCMake/Instrumentation/check-test-interrupted.cmake @@ -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() diff --git a/Tests/RunCMake/Instrumentation/interrupt-test-failover-signal-result.txt b/Tests/RunCMake/Instrumentation/interrupt-test-failover-signal-result.txt new file mode 100644 index 0000000000..d81cc0710e --- /dev/null +++ b/Tests/RunCMake/Instrumentation/interrupt-test-failover-signal-result.txt @@ -0,0 +1 @@ +42 diff --git a/Tests/RunCMake/Instrumentation/interrupt-test-signal-result.txt b/Tests/RunCMake/Instrumentation/interrupt-test-signal-result.txt new file mode 100644 index 0000000000..d81cc0710e --- /dev/null +++ b/Tests/RunCMake/Instrumentation/interrupt-test-signal-result.txt @@ -0,0 +1 @@ +42 diff --git a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt index 17d3c76703..e2dda8573f 100644 --- a/Tests/RunCMake/Instrumentation/project/CMakeLists.txt +++ b/Tests/RunCMake/Instrumentation/project/CMakeLists.txt @@ -89,3 +89,25 @@ if (INSTALL_INTERRUPT) add_subdirectory(installSlow2) add_subdirectory(installSlow3) 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() diff --git a/Tests/RunCMake/Instrumentation/project/runtest.cmake b/Tests/RunCMake/Instrumentation/project/runtest.cmake new file mode 100644 index 0000000000..711c82ea23 --- /dev/null +++ b/Tests/RunCMake/Instrumentation/project/runtest.cmake @@ -0,0 +1,16 @@ +# Test helper run as a CTest test command: +# cmake -DMARKER= [-DSECONDS=] -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() diff --git a/Tests/RunCMake/Instrumentation/query/interrupt-test-failover.json.in b/Tests/RunCMake/Instrumentation/query/interrupt-test-failover.json.in new file mode 100644 index 0000000000..81477e6704 --- /dev/null +++ b/Tests/RunCMake/Instrumentation/query/interrupt-test-failover.json.in @@ -0,0 +1,5 @@ +{ + "version": 1, + "hooks": ["postCTest"], + "callbacks": ["@GET_HOOK@"] +} diff --git a/Tests/RunCMake/Instrumentation/query/interrupt-test.json.in b/Tests/RunCMake/Instrumentation/query/interrupt-test.json.in new file mode 100644 index 0000000000..81477e6704 --- /dev/null +++ b/Tests/RunCMake/Instrumentation/query/interrupt-test.json.in @@ -0,0 +1,5 @@ +{ + "version": 1, + "hooks": ["postCTest"], + "callbacks": ["@GET_HOOK@"] +}