mirror of
https://github.com/Kitware/CMake.git
synced 2026-08-07 00:00:57 +00:00
Merge topic 'instrumentation-interrupt-ctest'
d5d2741414 Instrumentation: Write ctest snippet on interrupted test run
Acked-by: Kitware Robot <kwrobot@kitware.com>
Tested-by: buildbot <buildbot@kitware.com>
Merge-request: !12272
This commit is contained in:
@@ -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``.
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<std::string> const& args)
|
||||
};
|
||||
std::map<std::string, std::string> 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<int>(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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
33
Tests/RunCMake/Instrumentation/check-test-failover.cmake
Normal file
33
Tests/RunCMake/Instrumentation/check-test-failover.cmake
Normal 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()
|
||||
25
Tests/RunCMake/Instrumentation/check-test-interrupted.cmake
Normal file
25
Tests/RunCMake/Instrumentation/check-test-interrupted.cmake
Normal 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()
|
||||
@@ -0,0 +1 @@
|
||||
42
|
||||
@@ -0,0 +1 @@
|
||||
42
|
||||
@@ -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()
|
||||
|
||||
16
Tests/RunCMake/Instrumentation/project/runtest.cmake
Normal file
16
Tests/RunCMake/Instrumentation/project/runtest.cmake
Normal 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()
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": ["postCTest"],
|
||||
"callbacks": ["@GET_HOOK@"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": ["postCTest"],
|
||||
"callbacks": ["@GET_HOOK@"]
|
||||
}
|
||||
Reference in New Issue
Block a user