summaryrefslogtreecommitdiffstats
path: root/Source/CTest
diff options
context:
space:
mode:
authorBrad King <brad.king@kitware.com>2023-05-11 17:32:15 (GMT)
committerBrad King <brad.king@kitware.com>2023-06-02 10:51:13 (GMT)
commit54c5654f7d530ea363b3fcc02f2176d79342c07b (patch)
tree23a2bd337eb22e827914dbbb9d631899bd78dd8e /Source/CTest
parente38c05688ed637bdda8e6af5f2d76fc12bee35e3 (diff)
downloadCMake-54c5654f7d530ea363b3fcc02f2176d79342c07b.zip
CMake-54c5654f7d530ea363b3fcc02f2176d79342c07b.tar.gz
CMake-54c5654f7d530ea363b3fcc02f2176d79342c07b.tar.bz2
ctest: Optionally terminate tests with a custom signal on timeout
CTest normally terminates test processes on timeout using `SIGKILL`. Offer tests a chance to exit gracefully, on platforms supporting POSIX signals, by setting `TIMEOUT_SIGNAL_{NAME,GRACE_PERIOD}` properties. Fixes: #17288
Diffstat (limited to 'Source/CTest')
-rw-r--r--Source/CTest/cmCTestRunTest.cxx18
-rw-r--r--Source/CTest/cmCTestTestHandler.cxx61
-rw-r--r--Source/CTest/cmCTestTestHandler.h11
-rw-r--r--Source/CTest/cmProcess.cxx23
-rw-r--r--Source/CTest/cmProcess.h9
5 files changed, 122 insertions, 0 deletions
diff --git a/Source/CTest/cmCTestRunTest.cxx b/Source/CTest/cmCTestRunTest.cxx
index 2859b82..9b62183 100644
--- a/Source/CTest/cmCTestRunTest.cxx
+++ b/Source/CTest/cmCTestRunTest.cxx
@@ -181,6 +181,11 @@ cmCTestRunTest::EndTestResult cmCTestRunTest::EndTest(size_t completed,
}
} else if (res == cmProcess::State::Expired) {
outputStream << "***Timeout ";
+ if (this->TestProperties->TimeoutSignal &&
+ this->TestProcess->GetTerminationStyle() ==
+ cmProcess::Termination::Custom) {
+ outputStream << "(" << this->TestProperties->TimeoutSignal->Name << ") ";
+ }
this->TestResult.Status = cmCTestTestHandler::TIMEOUT;
outputTestErrorsToConsole =
this->CTest->GetOutputTestOutputOnTestFailure();
@@ -540,6 +545,19 @@ bool cmCTestRunTest::StartTest(size_t completed, size_t total)
this->TestResult.Name = this->TestProperties->Name;
this->TestResult.Path = this->TestProperties->Directory;
+ // Reject invalid test properties.
+ if (this->TestProperties->Error) {
+ std::string const& msg = *this->TestProperties->Error;
+ *this->TestHandler->LogFile << msg << std::endl;
+ cmCTestLog(this->CTest, HANDLER_OUTPUT, msg << std::endl);
+ this->TestResult.CompletionStatus = "Invalid Test Properties";
+ this->TestResult.Status = cmCTestTestHandler::NOT_RUN;
+ this->TestResult.Output = msg;
+ this->TestResult.FullCommandLine.clear();
+ this->TestResult.Environment.clear();
+ return false;
+ }
+
// Return immediately if test is disabled
if (this->TestProperties->Disabled) {
this->TestResult.CompletionStatus = "Disabled";
diff --git a/Source/CTest/cmCTestTestHandler.cxx b/Source/CTest/cmCTestTestHandler.cxx
index 7764f2b..6b02a5e 100644
--- a/Source/CTest/cmCTestTestHandler.cxx
+++ b/Source/CTest/cmCTestTestHandler.cxx
@@ -17,6 +17,10 @@
#include <sstream>
#include <utility>
+#ifndef _WIN32
+# include <csignal>
+#endif
+
#include <cm/memory>
#include <cm/string_view>
#include <cmext/algorithm>
@@ -2171,6 +2175,16 @@ void cmCTestTestHandler::CleanTestOutput(std::string& output, size_t length,
}
}
+void cmCTestTestHandler::cmCTestTestProperties::AppendError(
+ cm::string_view err)
+{
+ if (this->Error) {
+ *this->Error = cmStrCat(*this->Error, '\n', err);
+ } else {
+ this->Error = err;
+ }
+}
+
bool cmCTestTestHandler::SetTestsProperties(
const std::vector<std::string>& args)
{
@@ -2247,6 +2261,53 @@ bool cmCTestTestHandler::SetTestsProperties(
rt.FixturesRequired.insert(lval.begin(), lval.end());
} else if (key == "TIMEOUT"_s) {
rt.Timeout = cmDuration(atof(val.c_str()));
+ } else if (key == "TIMEOUT_SIGNAL_NAME"_s) {
+#ifdef _WIN32
+ rt.AppendError("TIMEOUT_SIGNAL_NAME is not supported on Windows.");
+#else
+ std::string const& signalName = val;
+ Signal s;
+ if (signalName == "SIGINT"_s) {
+ s.Number = SIGINT;
+ } else if (signalName == "SIGQUIT"_s) {
+ s.Number = SIGQUIT;
+ } else if (signalName == "SIGTERM"_s) {
+ s.Number = SIGTERM;
+ } else if (signalName == "SIGUSR1"_s) {
+ s.Number = SIGUSR1;
+ } else if (signalName == "SIGUSR2"_s) {
+ s.Number = SIGUSR2;
+ }
+ if (s.Number) {
+ s.Name = signalName;
+ rt.TimeoutSignal = std::move(s);
+ } else {
+ rt.AppendError(cmStrCat("TIMEOUT_SIGNAL_NAME \"", signalName,
+ "\" not supported on this platform."));
+ }
+#endif
+ } else if (key == "TIMEOUT_SIGNAL_GRACE_PERIOD"_s) {
+#ifdef _WIN32
+ rt.AppendError(
+ "TIMEOUT_SIGNAL_GRACE_PERIOD is not supported on Windows.");
+#else
+ std::string const& gracePeriod = val;
+ static cmDuration minGracePeriod{ 0 };
+ static cmDuration maxGracePeriod{ 60 };
+ cmDuration gp = cmDuration(atof(gracePeriod.c_str()));
+ if (gp <= minGracePeriod) {
+ rt.AppendError(cmStrCat("TIMEOUT_SIGNAL_GRACE_PERIOD \"",
+ gracePeriod, "\" is not greater than \"",
+ minGracePeriod.count(), "\" seconds."));
+ } else if (gp > maxGracePeriod) {
+ rt.AppendError(cmStrCat("TIMEOUT_SIGNAL_GRACE_PERIOD \"",
+ gracePeriod,
+ "\" is not less than the maximum of \"",
+ maxGracePeriod.count(), "\" seconds."));
+ } else {
+ rt.TimeoutGracePeriod = gp;
+ }
+#endif
} else if (key == "COST"_s) {
rt.Cost = static_cast<float>(atof(val.c_str()));
} else if (key == "REQUIRED_FILES"_s) {
diff --git a/Source/CTest/cmCTestTestHandler.h b/Source/CTest/cmCTestTestHandler.h
index b6bfde1..315a5b7 100644
--- a/Source/CTest/cmCTestTestHandler.h
+++ b/Source/CTest/cmCTestTestHandler.h
@@ -15,6 +15,7 @@
#include <vector>
#include <cm/optional>
+#include <cm/string_view>
#include "cmsys/RegularExpression.hxx"
@@ -119,8 +120,16 @@ public:
bool operator!=(const cmCTestTestResourceRequirement& other) const;
};
+ struct Signal
+ {
+ int Number = 0;
+ std::string Name;
+ };
+
struct cmCTestTestProperties
{
+ void AppendError(cm::string_view err);
+ cm::optional<std::string> Error;
std::string Name;
std::string Directory;
std::vector<std::string> Args;
@@ -144,6 +153,8 @@ public:
int PreviousRuns = 0;
bool RunSerial = false;
cm::optional<cmDuration> Timeout;
+ cm::optional<Signal> TimeoutSignal;
+ cm::optional<cmDuration> TimeoutGracePeriod;
cmDuration AlternateTimeout;
int Index = 0;
// Requested number of process slots
diff --git a/Source/CTest/cmProcess.cxx b/Source/CTest/cmProcess.cxx
index 780d626..269b92c 100644
--- a/Source/CTest/cmProcess.cxx
+++ b/Source/CTest/cmProcess.cxx
@@ -13,6 +13,7 @@
#include "cmCTest.h"
#include "cmCTestRunTest.h"
+#include "cmCTestTestHandler.h"
#include "cmGetPipes.h"
#include "cmStringAlgorithms.h"
#if defined(_WIN32)
@@ -274,7 +275,29 @@ void cmProcess::OnTimeoutCB(uv_timer_t* timer)
void cmProcess::OnTimeout()
{
+ bool const wasExecuting = this->ProcessState == cmProcess::State::Executing;
this->ProcessState = cmProcess::State::Expired;
+
+ // If the test process is still executing normally, and we timed out because
+ // the test timeout was reached, send the custom timeout signal, if any.
+ if (wasExecuting && this->TimeoutReason_ == TimeoutReason::Normal) {
+ cmCTestTestHandler::cmCTestTestProperties* p =
+ this->Runner->GetTestProperties();
+ if (p->TimeoutSignal) {
+ this->TerminationStyle = Termination::Custom;
+ uv_process_kill(this->Process, p->TimeoutSignal->Number);
+ if (p->TimeoutGracePeriod) {
+ this->Timeout = *p->TimeoutGracePeriod;
+ } else {
+ static const cmDuration defaultGracePeriod{ 1.0 };
+ this->Timeout = defaultGracePeriod;
+ }
+ this->StartTimer();
+ return;
+ }
+ }
+
+ this->TerminationStyle = Termination::Forced;
bool const was_still_reading = !this->ReadHandleClosed;
if (!this->ReadHandleClosed) {
this->ReadHandleClosed = true;
diff --git a/Source/CTest/cmProcess.h b/Source/CTest/cmProcess.h
index c80922d..dc755eb 100644
--- a/Source/CTest/cmProcess.h
+++ b/Source/CTest/cmProcess.h
@@ -85,6 +85,14 @@ public:
return std::move(this->Runner);
}
+ enum class Termination
+ {
+ Normal,
+ Custom,
+ Forced,
+ };
+ Termination GetTerminationStyle() const { return this->TerminationStyle; }
+
private:
cm::optional<cmDuration> Timeout;
TimeoutReason TimeoutReason_ = TimeoutReason::Normal;
@@ -137,4 +145,5 @@ private:
std::vector<const char*> ProcessArgs;
int Id;
int64_t ExitValue;
+ Termination TerminationStyle = Termination::Normal;
};