diff options
author | Brad King <brad.king@kitware.com> | 2023-05-11 17:32:15 (GMT) |
---|---|---|
committer | Brad King <brad.king@kitware.com> | 2023-06-02 10:51:13 (GMT) |
commit | 54c5654f7d530ea363b3fcc02f2176d79342c07b (patch) | |
tree | 23a2bd337eb22e827914dbbb9d631899bd78dd8e /Source/CTest | |
parent | e38c05688ed637bdda8e6af5f2d76fc12bee35e3 (diff) | |
download | CMake-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.cxx | 18 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestHandler.cxx | 61 | ||||
-rw-r--r-- | Source/CTest/cmCTestTestHandler.h | 11 | ||||
-rw-r--r-- | Source/CTest/cmProcess.cxx | 23 | ||||
-rw-r--r-- | Source/CTest/cmProcess.h | 9 |
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; }; |