Просмотр исходного кода

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
Brad King 2 лет назад
Родитель
Сommit
54c5654f7d
34 измененных файлов с 433 добавлено и 1 удалено
  1. 2 0
      Help/manual/cmake-properties.7.rst
  2. 3 0
      Help/prop_test/TIMEOUT.rst
  3. 2 0
      Help/prop_test/TIMEOUT_AFTER_MATCH.rst
  4. 14 0
      Help/prop_test/TIMEOUT_SIGNAL_GRACE_PERIOD.rst
  5. 41 0
      Help/prop_test/TIMEOUT_SIGNAL_NAME.rst
  6. 7 0
      Help/release/dev/ctest-timeout-signal.rst
  7. 18 0
      Source/CTest/cmCTestRunTest.cxx
  8. 61 0
      Source/CTest/cmCTestTestHandler.cxx
  9. 11 0
      Source/CTest/cmCTestTestHandler.h
  10. 23 0
      Source/CTest/cmProcess.cxx
  11. 9 0
      Source/CTest/cmProcess.h
  12. 29 0
      Tests/RunCMake/CTestCommandLine/RunCMakeTest.cmake
  13. 1 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-result.txt
  14. 1 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-stderr.txt
  15. 5 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-stdout.txt
  16. 1 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-result.txt
  17. 1 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-stderr.txt
  18. 4 0
      Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-stdout.txt
  19. 4 0
      Tests/RunCMake/CTestTimeout/CMakeLists.txt.in
  20. 57 1
      Tests/RunCMake/CTestTimeout/RunCMakeTest.cmake
  21. 11 0
      Tests/RunCMake/CTestTimeout/Signal-check.cmake
  22. 6 0
      Tests/RunCMake/CTestTimeout/Signal-stdout.txt
  23. 11 0
      Tests/RunCMake/CTestTimeout/SignalGraceHigh-check.cmake
  24. 3 0
      Tests/RunCMake/CTestTimeout/SignalGraceHigh-stdout.txt
  25. 11 0
      Tests/RunCMake/CTestTimeout/SignalGraceLow-check.cmake
  26. 3 0
      Tests/RunCMake/CTestTimeout/SignalGraceLow-stdout.txt
  27. 11 0
      Tests/RunCMake/CTestTimeout/SignalIgnore-check.cmake
  28. 6 0
      Tests/RunCMake/CTestTimeout/SignalIgnore-stdout.txt
  29. 11 0
      Tests/RunCMake/CTestTimeout/SignalUnknown-check.cmake
  30. 3 0
      Tests/RunCMake/CTestTimeout/SignalUnknown-stdout.txt
  31. 11 0
      Tests/RunCMake/CTestTimeout/SignalWindows-check.cmake
  32. 4 0
      Tests/RunCMake/CTestTimeout/SignalWindows-stdout.txt
  33. 47 0
      Tests/RunCMake/CTestTimeout/TestTimeout.c
  34. 1 0
      Utilities/IWYU/mapping.imp

+ 2 - 0
Help/manual/cmake-properties.7.rst

@@ -522,6 +522,8 @@ Properties on Tests
    /prop_test/SKIP_RETURN_CODE
    /prop_test/TIMEOUT
    /prop_test/TIMEOUT_AFTER_MATCH
+   /prop_test/TIMEOUT_SIGNAL_GRACE_PERIOD
+   /prop_test/TIMEOUT_SIGNAL_NAME
    /prop_test/WILL_FAIL
    /prop_test/WORKING_DIRECTORY
 

+ 3 - 0
Help/prop_test/TIMEOUT.rst

@@ -10,3 +10,6 @@ setting takes precedence over :variable:`CTEST_TEST_TIMEOUT`.
 
 An explicit ``0`` value means the test has no timeout, except as
 necessary to honor :option:`ctest --stop-time`.
+
+See also :prop_test:`TIMEOUT_AFTER_MATCH` and
+:prop_test:`TIMEOUT_SIGNAL_NAME`.

+ 2 - 0
Help/prop_test/TIMEOUT_AFTER_MATCH.rst

@@ -39,3 +39,5 @@ If the required resource can be controlled by CTest you should use
 :prop_test:`RESOURCE_LOCK` instead of ``TIMEOUT_AFTER_MATCH``.
 This property should be used when only the test itself can determine
 when its required resources are available.
+
+See also :prop_test:`TIMEOUT_SIGNAL_NAME`.

+ 14 - 0
Help/prop_test/TIMEOUT_SIGNAL_GRACE_PERIOD.rst

@@ -0,0 +1,14 @@
+TIMEOUT_SIGNAL_GRACE_PERIOD
+---------------------------
+
+.. versionadded:: 3.27
+
+If the :prop_test:`TIMEOUT_SIGNAL_NAME` test property is set, this property
+specifies the number of seconds to wait for a test process to terminate after
+sending the custom signal.  Otherwise, this property has no meaning.
+
+The grace period may be any real value greater than ``0.0``, but not greater
+than ``60.0``.  If this property is not set, the default is ``1.0`` second.
+
+This is available only on platforms supporting POSIX signals.
+It is not available on Windows.

+ 41 - 0
Help/prop_test/TIMEOUT_SIGNAL_NAME.rst

@@ -0,0 +1,41 @@
+TIMEOUT_SIGNAL_NAME
+-------------------
+
+.. versionadded:: 3.27
+
+Specify a custom signal to send to a test process when its timeout is reached.
+This is available only on platforms supporting POSIX signals.
+It is not available on Windows.
+
+The name must be one of the following:
+
+  ``SIGINT``
+    Interrupt.
+
+  ``SIGQUIT``
+    Quit.
+
+  ``SIGTERM``
+    Terminate.
+
+  ``SIGUSR1``
+    User defined signal 1.
+
+  ``SIGUSR2``
+    User defined signal 2.
+
+The custom signal is sent to the test process to give it a chance
+to exit gracefully during a grace period:
+
+* If the test process created any children, it is responsible for
+  terminating them too.
+
+* The grace period length is determined by the
+  :prop_test:`TIMEOUT_SIGNAL_GRACE_PERIOD` test property.
+
+* If the test process does not terminate before the grace period ends,
+  :manual:`ctest(1)` will force termination of its entire process tree
+  via ``SIGSTOP`` and ``SIGKILL``.
+
+See also :variable:`CTEST_TEST_TIMEOUT`,
+:prop_test:`TIMEOUT`, and :prop_test:`TIMEOUT_AFTER_MATCH`.

+ 7 - 0
Help/release/dev/ctest-timeout-signal.rst

@@ -0,0 +1,7 @@
+ctest-timeout-signal
+--------------------
+
+* The :prop_test:`TIMEOUT_SIGNAL_NAME` and
+  :prop_test:`TIMEOUT_SIGNAL_GRACE_PERIOD` test properties were added
+  to specify a POSIX signal to send to a test process when its timeout
+  is reached.

+ 18 - 0
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";

+ 61 - 0
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) {

+ 11 - 0
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

+ 23 - 0
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;

+ 9 - 0
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;
 };

+ 29 - 0
Tests/RunCMake/CTestCommandLine/RunCMakeTest.cmake

@@ -479,3 +479,32 @@ set_tests_properties(test5 PROPERTIES  SKIP_REGULAR_EXPRESSION \"please skip\")
   run_cmake_command(output-junit ${CMAKE_CTEST_COMMAND} --output-junit "${RunCMake_TEST_BINARY_DIR}/junit.xml")
 endfunction()
 run_output_junit()
+
+if(WIN32)
+  block()
+    set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TimeoutSignalWindows)
+    set(RunCMake_TEST_NO_CLEAN 1)
+    file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+    file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+    file(WRITE "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" "
+add_test(test1 \"${CMAKE_COMMAND}\" -E true)
+set_tests_properties(test1 PROPERTIES TIMEOUT_SIGNAL_NAME SIGUSR1)
+set_tests_properties(test1 PROPERTIES TIMEOUT_SIGNAL_GRACE_PERIOD 1)
+")
+    run_cmake_command(TimeoutSignalWindows ${CMAKE_CTEST_COMMAND})
+  endblock()
+else()
+  block()
+    set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TimeoutSignalBad)
+    set(RunCMake_TEST_NO_CLEAN 1)
+    file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+    file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+    file(WRITE "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" "
+add_test(test1 \"${CMAKE_COMMAND}\" -E true)
+set_tests_properties(test1 PROPERTIES TIMEOUT_SIGNAL_NAME NOTASIG)
+set_tests_properties(test1 PROPERTIES TIMEOUT_SIGNAL_GRACE_PERIOD 0)
+set_tests_properties(test1 PROPERTIES TIMEOUT_SIGNAL_GRACE_PERIOD 1000)
+")
+    run_cmake_command(TimeoutSignalBad ${CMAKE_CTEST_COMMAND})
+  endblock()
+endif()

+ 1 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-result.txt

@@ -0,0 +1 @@
+[^0]

+ 1 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-stderr.txt

@@ -0,0 +1 @@
+Errors while running CTest

+ 5 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalBad-stdout.txt

@@ -0,0 +1,5 @@
+    Start 1: test1
+TIMEOUT_SIGNAL_NAME "NOTASIG" not supported on this platform\.
+TIMEOUT_SIGNAL_GRACE_PERIOD "0" is not greater than "0" seconds\.
+TIMEOUT_SIGNAL_GRACE_PERIOD "1000" is not less than the maximum of "60" seconds\.
+1/1 Test #1: test1 ............................\*\*\*Not Run +[0-9.]+ sec

+ 1 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-result.txt

@@ -0,0 +1 @@
+[^0]

+ 1 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-stderr.txt

@@ -0,0 +1 @@
+Errors while running CTest

+ 4 - 0
Tests/RunCMake/CTestCommandLine/TimeoutSignalWindows-stdout.txt

@@ -0,0 +1,4 @@
+    Start 1: test1
+TIMEOUT_SIGNAL_NAME is not supported on Windows\.
+TIMEOUT_SIGNAL_GRACE_PERIOD is not supported on Windows\.
+1/1 Test #1: test1 ............................\*\*\*Not Run +[0-9.]+ sec

+ 4 - 0
Tests/RunCMake/CTestTimeout/CMakeLists.txt.in

@@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.16)
 project(CTestTest@CASE_NAME@ C)
 include(CTest)
 
+if(CMAKE_C_COMPILER_ID STREQUAL "SunPro" AND CMAKE_C_COMPILER_VERSION VERSION_LESS 5.14)
+  set(CMAKE_C_STANDARD 99)
+endif()
+
 add_executable(TestTimeout TestTimeout.c)
 
 if(NOT DEFINED TIMEOUT)

+ 57 - 1
Tests/RunCMake/CTestTimeout/RunCMakeTest.cmake

@@ -13,12 +13,68 @@ endfunction()
 
 run_ctest_timeout(Basic)
 
-if(UNIX)
+if(WIN32)
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME SIGUSR1
+      TIMEOUT_SIGNAL_GRACE_PERIOD 1.2
+      )
+]])
+  run_ctest_timeout(SignalWindows)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+else()
   string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
     target_compile_definitions(TestTimeout PRIVATE FORK)
 ]])
   run_ctest_timeout(Fork)
   unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    target_compile_definitions(TestTimeout PRIVATE SIGNAL)
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME SIGUSR1
+      TIMEOUT_SIGNAL_GRACE_PERIOD 1.2
+      )
+]])
+  run_ctest_timeout(Signal)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    target_compile_definitions(TestTimeout PRIVATE SIGNAL SIGNAL_IGNORE=1)
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME SIGUSR1
+      # Use default TIMEOUT_SIGNAL_GRACE_PERIOD of 1.
+      )
+]])
+  run_ctest_timeout(SignalIgnore)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME NOTASIG
+      )
+]])
+  run_ctest_timeout(SignalUnknown)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME SIGUSR1
+      TIMEOUT_SIGNAL_GRACE_PERIOD -1
+      )
+]])
+  run_ctest_timeout(SignalGraceLow)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
+
+  string(CONCAT CASE_CMAKELISTS_SUFFIX_CODE [[
+    set_tests_properties(TestTimeout PROPERTIES
+      TIMEOUT_SIGNAL_NAME SIGUSR1
+      TIMEOUT_SIGNAL_GRACE_PERIOD 1000
+      )
+]])
+  run_ctest_timeout(SignalGraceHigh)
+  unset(CASE_CMAKELISTS_SUFFIX_CODE)
 endif()
 
 block()

+ 11 - 0
Tests/RunCMake/CTestTimeout/Signal-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "SIGUSR1")
+  set(RunCMake_TEST_FAILED "Test output does not mention SIGUSR1.")
+  return()
+endif()

+ 6 - 0
Tests/RunCMake/CTestTimeout/Signal-stdout.txt

@@ -0,0 +1,6 @@
+Test project [^
+]*/Tests/RunCMake/CTestTimeout/Signal-build
+    Start 1: TestTimeout
+1/1 Test #1: TestTimeout ......................\*\*\*Timeout \(SIGUSR1\) +[1-9][0-9.]* sec
++
+0% tests passed, 1 tests failed out of 1

+ 11 - 0
Tests/RunCMake/CTestTimeout/SignalGraceHigh-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "TIMEOUT_SIGNAL_GRACE_PERIOD \"1000\" is not less than the maximum of \"60\" seconds\\.")
+  set(RunCMake_TEST_FAILED "Test output does not have expected error message.")
+  return()
+endif()

+ 3 - 0
Tests/RunCMake/CTestTimeout/SignalGraceHigh-stdout.txt

@@ -0,0 +1,3 @@
+    Start 1: TestTimeout
+TIMEOUT_SIGNAL_GRACE_PERIOD "1000" is not less than the maximum of "60" seconds\.
+1/1 Test #1: TestTimeout ......................\*\*\*Not Run +[0-9.]+ sec

+ 11 - 0
Tests/RunCMake/CTestTimeout/SignalGraceLow-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "TIMEOUT_SIGNAL_GRACE_PERIOD \"-1\" is not greater than \"0\" seconds\\.")
+  set(RunCMake_TEST_FAILED "Test output does not have expected error message.")
+  return()
+endif()

+ 3 - 0
Tests/RunCMake/CTestTimeout/SignalGraceLow-stdout.txt

@@ -0,0 +1,3 @@
+    Start 1: TestTimeout
+TIMEOUT_SIGNAL_GRACE_PERIOD "-1" is not greater than "0" seconds.
+1/1 Test #1: TestTimeout ......................\*\*\*Not Run +[0-9.]+ sec

+ 11 - 0
Tests/RunCMake/CTestTimeout/SignalIgnore-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "EINTR")
+  set(RunCMake_TEST_FAILED "Test output does not mention EINTR.")
+  return()
+endif()

+ 6 - 0
Tests/RunCMake/CTestTimeout/SignalIgnore-stdout.txt

@@ -0,0 +1,6 @@
+Test project [^
+]*/Tests/RunCMake/CTestTimeout/SignalIgnore-build
+    Start 1: TestTimeout
+1/1 Test #1: TestTimeout ......................\*\*\*Timeout +[1-9][0-9.]* sec
++
+0% tests passed, 1 tests failed out of 1

+ 11 - 0
Tests/RunCMake/CTestTimeout/SignalUnknown-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "TIMEOUT_SIGNAL_NAME \"NOTASIG\" not supported on this platform\\.")
+  set(RunCMake_TEST_FAILED "Test output does not have expected error message.")
+  return()
+endif()

+ 3 - 0
Tests/RunCMake/CTestTimeout/SignalUnknown-stdout.txt

@@ -0,0 +1,3 @@
+    Start 1: TestTimeout
+TIMEOUT_SIGNAL_NAME "NOTASIG" not supported on this platform\.
+1/1 Test #1: TestTimeout ......................\*\*\*Not Run +[0-9.]+ sec

+ 11 - 0
Tests/RunCMake/CTestTimeout/SignalWindows-check.cmake

@@ -0,0 +1,11 @@
+file(GLOB test_xml "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(NOT test_xml)
+  set(RunCMake_TEST_FAILED "Test.xml not found.")
+  return()
+endif()
+
+file(READ "${test_xml}" test_xml_content)
+if(NOT test_xml_content MATCHES "TIMEOUT_SIGNAL_NAME is not supported on Windows\\.")
+  set(RunCMake_TEST_FAILED "Test output does not have expected error message.")
+  return()
+endif()

+ 4 - 0
Tests/RunCMake/CTestTimeout/SignalWindows-stdout.txt

@@ -0,0 +1,4 @@
+    Start 1: TestTimeout
+TIMEOUT_SIGNAL_GRACE_PERIOD is not supported on Windows\.
+TIMEOUT_SIGNAL_NAME is not supported on Windows\.
+1/1 Test #1: TestTimeout ......................\*\*\*Not Run +[0-9.]+ sec

+ 47 - 0
Tests/RunCMake/CTestTimeout/TestTimeout.c

@@ -1,3 +1,8 @@
+#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__OpenBSD__)
+/* NOLINTNEXTLINE(bugprone-reserved-identifier) */
+#  define _XOPEN_SOURCE 600
+#endif
+
 #if defined(_WIN32)
 #  include <windows.h>
 #else
@@ -7,6 +12,19 @@
 
 #include <stdio.h>
 
+#ifdef SIGNAL
+#  include <errno.h>
+#  include <signal.h>
+#  include <string.h>
+
+static unsigned int signal_count;
+static void signal_handler(int signum)
+{
+  (void)signum;
+  ++signal_count;
+}
+#endif
+
 int main(void)
 {
 #ifdef FORK
@@ -16,10 +34,39 @@ int main(void)
   }
 #endif
 
+#ifdef SIGNAL
+  struct sigaction sa;
+  memset(&sa, 0, sizeof(sa));
+  sa.sa_handler = signal_handler;
+  while ((sigaction(SIGUSR1, &sa, NULL) < 0) && (errno == EINTR))
+    ;
+#endif
+
 #if defined(_WIN32)
   Sleep((TIMEOUT + 4) * 1000);
+#elif defined(SIGNAL_IGNORE)
+#  if defined(__CYGWIN__) || defined(__sun__)
+#    define ERRNO_IS_EINTR (errno == EINTR || errno == 0)
+#  else
+#    define ERRNO_IS_EINTR (errno == EINTR)
+#  endif
+  {
+    unsigned int timeLeft = (TIMEOUT + 4 + SIGNAL_IGNORE);
+    while ((timeLeft = sleep(timeLeft), timeLeft > 0 && ERRNO_IS_EINTR)) {
+      printf("EINTR: timeLeft=%u\n", timeLeft);
+      fflush(stdout);
+    }
+  }
 #else
   sleep((TIMEOUT + 4));
 #endif
+
+#ifdef SIGNAL
+  if (signal_count > 0) {
+    printf("SIGUSR1: count=%u\n", signal_count);
+    fflush(stdout);
+  }
+#endif
+
   return 0;
 }

+ 1 - 0
Utilities/IWYU/mapping.imp

@@ -102,6 +102,7 @@
   { symbol: [ "std::enable_if<true, std::chrono::duration<long, std::ratio<1, 1000> > >::type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "__gnu_cxx::__enable_if<true, bool>::__type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "std::remove_reference<std::basic_string<char, std::char_traits<char>, std::allocator<char> > &>::type", private, "\"cmConfigure.h\"", public ] },
+  { symbol: [ "std::remove_reference<cmCTestTestHandler::Signal &>::type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "std::remove_reference<Defer &>::type", private, "\"cmConfigure.h\"", public ] },
   { symbol: [ "std::remove_reference<dap::StoppedEvent &>::type", private, "\"cmConfigure.h\"", public ] },