Browse Source

Merge topic 'ctest_truncate'

140704d443 ctest: add option for output truncation
359e5b17d8 presets: bump version to v5
4634de335b cmCTestTestHandler: refactor CleanTestOutput method

Acked-by: Kitware Robot <[email protected]>
Merge-request: !6993
Brad King 3 years ago
parent
commit
cbd36eac23

+ 1 - 0
Auxiliary/vim/syntax/cmake.vim

@@ -1574,6 +1574,7 @@ syn keyword cmakeVariable contained
             \ CTEST_CUSTOM_MAXIMUM_NUMBER_OF_ERRORS
             \ CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS
             \ CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE
+            \ CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION
             \ CTEST_CUSTOM_MEMCHECK_IGNORE
             \ CTEST_CUSTOM_POST_MEMCHECK
             \ CTEST_CUSTOM_POST_TEST

+ 3 - 2
Help/command/ctest_test.rst

@@ -172,8 +172,9 @@ The options are:
   affected.  Summary info detailing the percentage of passing tests is also
   unaffected by the ``QUIET`` option.
 
-See also the :variable:`CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE`
-and :variable:`CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE` variables.
+See also the :variable:`CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE`,
+:variable:`CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE` and
+:variable:`CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION` variables.
 
 .. _`Additional Test Measurements`:
 

+ 7 - 1
Help/manual/cmake-presets.7.rst

@@ -42,7 +42,7 @@ The root object recognizes the following fields:
 ``version``
 
   A required integer representing the version of the JSON schema.
-  The supported versions are ``1``, ``2``, ``3``, and ``4``.
+  The supported versions are ``1``, ``2``, ``3``, ``4``, and ``5``.
 
 ``cmakeMinimumRequired``
 
@@ -715,6 +715,12 @@ that may contain the following fields:
     bytes. Equivalent to passing ``--test-output-size-failed`` on the
     command line.
 
+  ``testOutputTruncation``
+
+    An optional string specifying the test output truncation mode. Equivalent
+    to passing ``--test-output-truncation`` on the command line."
+    This is allowed in preset files specifying version ``5`` or above.
+
   ``maxTestNameWidth``
 
     An optional integer specifying the maximum width of a test name to

+ 1 - 0
Help/manual/cmake-variables.7.rst

@@ -642,6 +642,7 @@ Variables for CTest
    /variable/CTEST_CUSTOM_MAXIMUM_NUMBER_OF_ERRORS
    /variable/CTEST_CUSTOM_MAXIMUM_NUMBER_OF_WARNINGS
    /variable/CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE
+   /variable/CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION
    /variable/CTEST_CUSTOM_MEMCHECK_IGNORE
    /variable/CTEST_CUSTOM_POST_MEMCHECK
    /variable/CTEST_CUSTOM_POST_TEST

+ 4 - 0
Help/manual/ctest.1.rst

@@ -362,6 +362,10 @@ Specify the directory in which to look for tests.
 ``--test-output-size-failed <size>``
  Limit the output for failed tests to ``<size>`` bytes.
 
+``--test-output-truncation <mode>``
+ Truncate 'tail' (default), 'middle' or 'head' of test output once maximum
+ output size is reached.
+
 ``--overwrite``
  Overwrite CTest configuration option.
 

+ 1 - 1
Help/manual/presets/example.json

@@ -1,5 +1,5 @@
 {
-  "version": 4,
+  "version": 5,
   "cmakeMinimumRequired": {
     "major": 3,
     "minor": 23,

+ 71 - 2
Help/manual/presets/schema.json

@@ -57,6 +57,21 @@
         "include": { "$ref": "#/definitions/include"}
       },
       "additionalProperties": false
+    },
+    {
+      "properties": {
+        "version": {
+          "const": 5,
+          "description": "A required integer representing the version of the JSON schema."
+        },
+        "cmakeMinimumRequired": { "$ref": "#/definitions/cmakeMinimumRequired"},
+        "vendor": { "$ref": "#/definitions/vendor" },
+        "configurePresets": { "$ref": "#/definitions/configurePresetsV3"},
+        "buildPresets": { "$ref": "#/definitions/buildPresetsV4"},
+        "testPresets": { "$ref": "#/definitions/testPresetsV5"},
+        "include": { "$ref": "#/definitions/include"}
+      },
+      "additionalProperties": false
     }
   ],
   "required": [
@@ -673,6 +688,28 @@
         "additionalProperties": false
       }
     },
+    "testPresetsItemsV5": {
+      "type": "array",
+      "description": "An optional array of test preset objects. Used to specify arguments to ctest. Available in version 5 and higher.",
+      "items": {
+        "type": "object",
+        "properties": {
+          "output": {
+            "type": "object",
+            "description": "An optional object specifying output options.",
+            "properties": {
+              "testOutputTruncation": {
+                "type": "string",
+                "description": "An optional string specifying the test output truncation mode. Equivalent to passing --test-output-truncation on the command line. Must be one of the following values: \"tail\", \"middle\", or \"head\".",
+                "enum": [
+                  "tail", "middle", "head"
+                ]
+              }
+            }
+          }
+        }
+      }
+    },
     "testPresetsItemsV3": {
       "type": "array",
       "description": "An optional array of test preset objects. Used to specify arguments to ctest. Available in version 3 and higher.",
@@ -821,8 +858,7 @@
                 "type": "integer",
                 "description": "An optional integer specifying the maximum width of a test name to output. Equivalent to passing --max-width on the command line."
               }
-            },
-            "additionalProperties": false
+            }
           },
           "filter": {
             "type": "object",
@@ -998,6 +1034,39 @@
         ]
       }
     },
+    "testPresetsV5": {
+      "type": "array",
+      "description": "An optional array of test preset objects. Used to specify arguments to ctest. Available in version 5 and higher.",
+      "allOf": [
+        { "$ref": "#/definitions/testPresetsItemsV2" },
+        { "$ref": "#/definitions/testPresetsItemsV3" },
+        { "$ref": "#/definitions/testPresetsItemsV5" }
+      ],
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {},
+          "hidden": {},
+          "inherits": {},
+          "configurePreset": {},
+          "vendor": {},
+          "displayName": {},
+          "description": {},
+          "inheritConfigureEnvironment": {},
+          "environment": {},
+          "configuration": {},
+          "overwriteConfigurationFile": {},
+          "output": {},
+          "filter": {},
+          "execution": {},
+          "condition": {}
+        },
+        "required": [
+          "name"
+        ],
+        "additionalProperties": false
+      }
+    },
     "testPresetsV3": {
       "type": "array",
       "description": "An optional array of test preset objects. Used to specify arguments to ctest. Available in version 3 and higher.",

+ 2 - 1
Help/variable/CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE.rst

@@ -3,7 +3,8 @@ CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE
 
 When saving a failing test's output, this is the maximum size, in bytes, that
 will be collected by the :command:`ctest_test` command. Defaults to 307200
-(300 KiB).
+(300 KiB). See :variable:`CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION` for possible
+truncation modes.
 
 If a test's output contains the literal string "CTEST_FULL_OUTPUT",
 the output will not be truncated and may exceed the maximum size.

+ 2 - 1
Help/variable/CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE.rst

@@ -3,7 +3,8 @@ CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE
 
 When saving a passing test's output, this is the maximum size, in bytes, that
 will be collected by the :command:`ctest_test` command. Defaults to 1024
-(1 KiB).
+(1 KiB). See :variable:`CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION` for possible
+truncation modes.
 
 If a test's output contains the literal string "CTEST_FULL_OUTPUT",
 the output will not be truncated and may exceed the maximum size.

+ 12 - 0
Help/variable/CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION.rst

@@ -0,0 +1,12 @@
+CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION
+-----------------------------------
+
+.. versionadded:: 3.24
+
+Set the test output truncation mode in case a maximum size is configured
+via the :variable:`CTEST_CUSTOM_MAXIMUM_PASSED_TEST_OUTPUT_SIZE` or
+:variable:`CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE` variables.
+By default the ``tail`` of the output will be truncated. Other possible
+values are ``middle`` and ``head``.
+
+.. include:: CTEST_CUSTOM_XXX.txt

+ 2 - 1
Source/CTest/cmCTestMemCheckHandler.cxx

@@ -371,7 +371,8 @@ void cmCTestMemCheckHandler::GenerateCTestXML(cmXMLWriter& xml)
     }
     this->CleanTestOutput(
       memcheckstr,
-      static_cast<size_t>(this->CustomMaximumFailedTestOutputSize));
+      static_cast<size_t>(this->CustomMaximumFailedTestOutputSize),
+      this->TestOutputTruncation);
     this->WriteTestResultHeader(xml, result);
     xml.StartElement("Results");
     int memoryErrors = 0;

+ 2 - 1
Source/CTest/cmCTestRunTest.cxx

@@ -277,7 +277,8 @@ bool cmCTestRunTest::EndTest(size_t completed, size_t total, bool started)
       static_cast<size_t>(
         this->TestResult.Status == cmCTestTestHandler::COMPLETED
           ? this->TestHandler->CustomMaximumPassedTestOutputSize
-          : this->TestHandler->CustomMaximumFailedTestOutputSize));
+          : this->TestHandler->CustomMaximumFailedTestOutputSize),
+      this->TestHandler->TestOutputTruncation);
   }
   this->TestResult.Reason = reason;
   if (this->TestHandler->LogFile) {

+ 65 - 27
Source/CTest/cmCTestTestHandler.cxx

@@ -281,6 +281,7 @@ cmCTestTestHandler::cmCTestTestHandler()
 
   this->CustomMaximumPassedTestOutputSize = 1 * 1024;
   this->CustomMaximumFailedTestOutputSize = 300 * 1024;
+  this->TestOutputTruncation = cmCTestTypes::TruncationMode::Tail;
 
   this->MemCheck = false;
 
@@ -325,6 +326,7 @@ void cmCTestTestHandler::Initialize()
   this->CustomPostTest.clear();
   this->CustomMaximumPassedTestOutputSize = 1 * 1024;
   this->CustomMaximumFailedTestOutputSize = 300 * 1024;
+  this->TestOutputTruncation = cmCTestTypes::TruncationMode::Tail;
 
   this->TestsToRun.clear();
 
@@ -358,6 +360,11 @@ void cmCTestTestHandler::PopulateCustomVectors(cmMakefile* mf)
   this->CTest->PopulateCustomInteger(
     mf, "CTEST_CUSTOM_MAXIMUM_FAILED_TEST_OUTPUT_SIZE",
     this->CustomMaximumFailedTestOutputSize);
+
+  cmValue dval = mf->GetDefinition("CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION");
+  if (dval) {
+    this->SetTestOutputTruncation(dval);
+  }
 }
 
 int cmCTestTestHandler::PreProcessHandler()
@@ -2076,6 +2083,20 @@ void cmCTestTestHandler::SetExcludeRegExp(const std::string& arg)
   this->ExcludeRegExp = arg;
 }
 
+bool cmCTestTestHandler::SetTestOutputTruncation(const std::string& mode)
+{
+  if (mode == "tail") {
+    this->TestOutputTruncation = cmCTestTypes::TruncationMode::Tail;
+  } else if (mode == "middle") {
+    this->TestOutputTruncation = cmCTestTypes::TruncationMode::Middle;
+  } else if (mode == "head") {
+    this->TestOutputTruncation = cmCTestTypes::TruncationMode::Head;
+  } else {
+    return false;
+  }
+  return true;
+}
+
 void cmCTestTestHandler::SetTestsToRunInformation(cmValue in)
 {
   if (!in) {
@@ -2094,41 +2115,58 @@ void cmCTestTestHandler::SetTestsToRunInformation(cmValue in)
   }
 }
 
-void cmCTestTestHandler::CleanTestOutput(std::string& output, size_t length)
+void cmCTestTestHandler::CleanTestOutput(std::string& output, size_t length,
+                                         cmCTestTypes::TruncationMode truncate)
 {
   if (!length || length >= output.size() ||
       output.find("CTEST_FULL_OUTPUT") != std::string::npos) {
     return;
   }
 
-  // Truncate at given length but do not break in the middle of a multi-byte
-  // UTF-8 encoding.
+  // Advance n bytes in string delimited by begin/end but do not break in the
+  // middle of a multi-byte UTF-8 encoding.
+  auto utf8_advance = [](char const* const begin, char const* const end,
+                         size_t n) -> const char* {
+    char const* const stop = begin + n;
+    char const* current = begin;
+    while (current < stop) {
+      unsigned int ch;
+      if (const char* next = cm_utf8_decode_character(current, end, &ch)) {
+        if (next > stop) {
+          break;
+        }
+        current = next;
+      } else // Bad byte will be handled by cmXMLWriter.
+      {
+        ++current;
+      }
+    }
+    return current;
+  };
+
+  // Truncation message.
+  const std::string msg =
+    "\n[This part of the test output was removed since it "
+    "exceeds the threshold of " +
+    std::to_string(length) + " bytes.]\n";
+
   char const* const begin = output.c_str();
   char const* const end = begin + output.size();
-  char const* const truncate = begin + length;
-  char const* current = begin;
-  while (current < truncate) {
-    unsigned int ch;
-    if (const char* next = cm_utf8_decode_character(current, end, &ch)) {
-      if (next > truncate) {
-        break;
-      }
-      current = next;
-    } else // Bad byte will be handled by cmXMLWriter.
-    {
-      ++current;
-    }
-  }
-  output.erase(current - begin);
-
-  // Append truncation message.
-  std::ostringstream msg;
-  msg << "...\n"
-         "The rest of the test output was removed since it exceeds the "
-         "threshold "
-         "of "
-      << length << " bytes.\n";
-  output += msg.str();
+
+  // Erase head, middle or tail of output.
+  if (truncate == cmCTestTypes::TruncationMode::Head) {
+    char const* current = utf8_advance(begin, end, output.size() - length);
+    output.erase(0, current - begin);
+    output.insert(0, msg + "...");
+  } else if (truncate == cmCTestTypes::TruncationMode::Middle) {
+    char const* current = utf8_advance(begin, end, length / 2);
+    output.erase(current - begin, output.size() - length);
+    output.insert(current - begin, "..." + msg + "...");
+  } else { // default or "tail"
+    char const* current = utf8_advance(begin, end, length);
+    output.erase(current - begin);
+    output += ("..." + msg);
+  }
 }
 
 bool cmCTestTestHandler::SetTestsProperties(

+ 9 - 2
Source/CTest/cmCTestTestHandler.h

@@ -19,6 +19,7 @@
 #include "cmCTest.h"
 #include "cmCTestGenericHandler.h"
 #include "cmCTestResourceSpec.h"
+#include "cmCTestTypes.h"
 #include "cmDuration.h"
 #include "cmListFileCache.h"
 #include "cmValue.h"
@@ -32,6 +33,7 @@ class cmXMLWriter;
  */
 class cmCTestTestHandler : public cmCTestGenericHandler
 {
+  friend class cmCTest;
   friend class cmCTestRunTest;
   friend class cmCTestMultiProcessHandler;
 
@@ -80,6 +82,9 @@ public:
     this->CustomMaximumFailedTestOutputSize = n;
   }
 
+  //! Set test output truncation mode. Return false if unknown mode.
+  bool SetTestOutputTruncation(const std::string& mode);
+
   //! pass the -I argument down
   void SetTestsToRunInformation(cmValue);
 
@@ -242,8 +247,9 @@ protected:
   void AttachFile(cmXMLWriter& xml, std::string const& file,
                   std::string const& name);
 
-  //! Clean test output to specified length
-  void CleanTestOutput(std::string& output, size_t length);
+  //! Clean test output to specified length and truncation mode
+  void CleanTestOutput(std::string& output, size_t length,
+                       cmCTestTypes::TruncationMode truncate);
 
   cmDuration ElapsedTestingTime;
 
@@ -258,6 +264,7 @@ protected:
   bool MemCheck;
   int CustomMaximumPassedTestOutputSize;
   int CustomMaximumFailedTestOutputSize;
+  cmCTestTypes::TruncationMode TestOutputTruncation;
   int MaxIndex;
 
 public:

+ 16 - 0
Source/CTest/cmCTestTypes.h

@@ -0,0 +1,16 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+namespace cmCTestTypes {
+
+enum class TruncationMode
+{ // Test output truncation mode
+  Tail,
+  Middle,
+  Head
+};
+}

+ 5 - 0
Source/cmCMakePresetsGraph.cxx

@@ -781,6 +781,8 @@ cmCMakePresetsGraph::TestPreset::VisitPresetInherit(
                            parentOutput.MaxPassedTestOutputSize);
       InheritOptionalValue(output.MaxFailedTestOutputSize,
                            parentOutput.MaxFailedTestOutputSize);
+      InheritOptionalValue(output.TestOutputTruncation,
+                           parentOutput.TestOutputTruncation);
       InheritOptionalValue(output.MaxTestNameWidth,
                            parentOutput.MaxTestNameWidth);
     } else {
@@ -1035,6 +1037,9 @@ const char* cmCMakePresetsGraph::ResultToString(ReadFileResult result)
              "support.";
     case ReadFileResult::CYCLIC_INCLUDE:
       return "Cyclic include among preset files";
+    case ReadFileResult::TEST_OUTPUT_TRUNCATION_UNSUPPORTED:
+      return "File version must be 5 or higher for testOutputTruncation "
+             "preset support.";
   }
 
   return "Unknown error";

+ 4 - 0
Source/cmCMakePresetsGraph.h

@@ -14,6 +14,8 @@
 
 #include <cm/optional>
 
+#include "CTest/cmCTestTypes.h"
+
 enum class PackageResolveMode;
 
 class cmCMakePresetsGraph
@@ -47,6 +49,7 @@ public:
     CONDITION_UNSUPPORTED,
     TOOLCHAIN_FILE_UNSUPPORTED,
     CYCLIC_INCLUDE,
+    TEST_OUTPUT_TRUNCATION_UNSUPPORTED,
   };
 
   enum class ArchToolsetStrategy
@@ -226,6 +229,7 @@ public:
       cm::optional<bool> SubprojectSummary;
       cm::optional<int> MaxPassedTestOutputSize;
       cm::optional<int> MaxFailedTestOutputSize;
+      cm::optional<cmCTestTypes::TruncationMode> TestOutputTruncation;
       cm::optional<int> MaxTestNameWidth;
     };
 

+ 6 - 1
Source/cmCMakePresetsGraphReadJSON.cxx

@@ -33,7 +33,7 @@ using TestPreset = cmCMakePresetsGraph::TestPreset;
 using ArchToolsetStrategy = cmCMakePresetsGraph::ArchToolsetStrategy;
 
 constexpr int MIN_VERSION = 1;
-constexpr int MAX_VERSION = 4;
+constexpr int MAX_VERSION = 5;
 
 struct CMakeVersion
 {
@@ -568,6 +568,11 @@ cmCMakePresetsGraph::ReadFileResult cmCMakePresetsGraph::ReadJSONFile(
       return ReadFileResult::CONDITION_UNSUPPORTED;
     }
 
+    // Support for TestOutputTruncation added in version 5.
+    if (v < 5 && preset.Output) {
+      return ReadFileResult::TEST_OUTPUT_TRUNCATION_UNSUPPORTED;
+    }
+
     this->TestPresetOrder.push_back(preset.Name);
   }
 

+ 39 - 0
Source/cmCMakePresetsGraphReadJSONTestPresets.cxx

@@ -16,6 +16,8 @@
 #include "cmCMakePresetsGraphInternal.h"
 #include "cmJSONHelpers.h"
 
+#include "CTest/cmCTestTypes.h"
+
 namespace {
 using ReadFileResult = cmCMakePresetsGraph::ReadFileResult;
 using TestPreset = cmCMakePresetsGraph::TestPreset;
@@ -55,6 +57,40 @@ auto const TestPresetOptionalOutputVerbosityHelper =
                        ReadFileResult>(ReadFileResult::READ_OK,
                                        TestPresetOutputVerbosityHelper);
 
+ReadFileResult TestPresetOutputTruncationHelper(
+  cmCTestTypes::TruncationMode& out, const Json::Value* value)
+{
+  if (!value) {
+    out = cmCTestTypes::TruncationMode::Tail;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (!value->isString()) {
+    return ReadFileResult::INVALID_PRESET;
+  }
+
+  if (value->asString() == "tail") {
+    out = cmCTestTypes::TruncationMode::Tail;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (value->asString() == "middle") {
+    out = cmCTestTypes::TruncationMode::Middle;
+    return ReadFileResult::READ_OK;
+  }
+
+  if (value->asString() == "head") {
+    out = cmCTestTypes::TruncationMode::Head;
+    return ReadFileResult::READ_OK;
+  }
+
+  return ReadFileResult::INVALID_PRESET;
+}
+
+auto const TestPresetOptionalTruncationHelper =
+  cmJSONOptionalHelper<cmCTestTypes::TruncationMode, ReadFileResult>(
+    ReadFileResult::READ_OK, TestPresetOutputTruncationHelper);
+
 auto const TestPresetOptionalOutputHelper =
   cmJSONOptionalHelper<TestPreset::OutputOptions, ReadFileResult>(
     ReadFileResult::READ_OK,
@@ -83,6 +119,9 @@ auto const TestPresetOptionalOutputHelper =
       .Bind("maxFailedTestOutputSize"_s,
             &TestPreset::OutputOptions::MaxFailedTestOutputSize,
             cmCMakePresetsGraphInternal::PresetOptionalIntHelper, false)
+      .Bind("testOutputTruncation"_s,
+            &TestPreset::OutputOptions::TestOutputTruncation,
+            TestPresetOptionalTruncationHelper, false)
       .Bind("maxTestNameWidth"_s, &TestPreset::OutputOptions::MaxTestNameWidth,
             cmCMakePresetsGraphInternal::PresetOptionalIntHelper, false));
 

+ 12 - 0
Source/cmCTest.cxx

@@ -2036,6 +2036,13 @@ bool cmCTest::HandleCommandLineArguments(size_t& i,
                  "Invalid value for '--test-output-size-failed': " << args[i]
                                                                    << "\n");
     }
+  } else if (this->CheckArgument(arg, "--test-output-truncation"_s) &&
+             i < args.size() - 1) {
+    i++;
+    if (!this->Impl->TestHandler.SetTestOutputTruncation(args[i])) {
+      errormsg = "Invalid value for '--test-output-truncation': " + args[i];
+      return false;
+    }
   } else if (this->CheckArgument(arg, "-N"_s, "--show-only")) {
     this->Impl->ShowOnly = true;
   } else if (cmHasLiteralPrefix(arg, "--show-only=")) {
@@ -2464,6 +2471,11 @@ bool cmCTest::SetArgsFromPreset(const std::string& presetName,
         *expandedPreset->Output->MaxFailedTestOutputSize);
     }
 
+    if (expandedPreset->Output->TestOutputTruncation) {
+      this->Impl->TestHandler.TestOutputTruncation =
+        *expandedPreset->Output->TestOutputTruncation;
+    }
+
     if (expandedPreset->Output->MaxTestNameWidth) {
       this->Impl->MaxTestNameWidth = *expandedPreset->Output->MaxTestNameWidth;
     }

+ 3 - 0
Source/ctest.cxx

@@ -44,6 +44,9 @@ static const char* cmDocumentationOptions[][2] = {
   { "--test-output-size-failed <size>",
     "Limit the output for failed tests "
     "to <size> bytes" },
+  { "--test-output-truncation <mode>",
+    "Truncate 'tail' (default), 'middle' or 'head' of test output once "
+    "maximum output size is reached" },
   { "-F", "Enable failover." },
   { "-j <jobs>, --parallel <jobs>",
     "Run the tests in parallel using the "

+ 1 - 1
Tests/RunCMake/CMakePresets/IncludeOutsideProjectInclude.json

@@ -1,3 +1,3 @@
 {
-  "version": 4
+  "version": 5
 }

+ 1 - 1
Tests/RunCMake/CMakePresetsBuild/Good.json.in

@@ -1,5 +1,5 @@
 {
-    "version": 4,
+    "version": 5,
     "configurePresets": [
         {
             "name": "default",

+ 3 - 2
Tests/RunCMake/CMakePresetsTest/Good.json.in

@@ -1,5 +1,5 @@
 {
-    "version": 2,
+    "version": 5,
     "configurePresets": [
         {
             "name": "default",
@@ -48,7 +48,8 @@
                 "quiet": false,
                 "outputLogFile": "",
                 "labelSummary": true,
-                "subprojectSummary": true
+                "subprojectSummary": true,
+                "testOutputTruncation": "tail"
             },
             "filter": {
                 "include": {

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

@@ -274,6 +274,27 @@ function(run_TestOutputSize)
 endfunction()
 run_TestOutputSize()
 
+# Test --test-output-truncation
+function(run_TestOutputTruncation mode expected)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TestOutputTruncation_${mode})
+  set(RunCMake_TEST_NO_CLEAN 1)
+  set(TRUNCATED_OUTPUT ${expected})  # used in TestOutputTruncation-check.cmake
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+  file(WRITE "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" "
+  add_test(Truncation_${mode} \"${CMAKE_COMMAND}\" -E echo 123456789)
+")
+  run_cmake_command(TestOutputTruncation
+    ${CMAKE_CTEST_COMMAND} -M Experimental -T Test
+                           --no-compress-output
+                           --test-output-size-passed 5
+                           --test-output-truncation ${mode}
+    )
+endfunction()
+run_TestOutputTruncation("head" "\\.\\.\\.6789")
+run_TestOutputTruncation("middle" "12\\.\\.\\..*\\.\\.\\.89")
+run_TestOutputTruncation("tail" "12345\.\.\.")
+
 # Test --stop-on-failure
 function(run_stop_on_failure)
   set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/stop-on-failure)

+ 12 - 0
Tests/RunCMake/CTestCommandLine/TestOutputTruncation-check.cmake

@@ -0,0 +1,12 @@
+file(GLOB test_xml_file "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(test_xml_file)
+  file(READ "${test_xml_file}" test_xml LIMIT 4096)
+  if("${test_xml}" MATCHES [[(<Test Status="passed">.*</Test>)]])
+    set(test_result "${CMAKE_MATCH_1}")
+  endif()
+  if(NOT "${test_result}" MATCHES "<Value>.*${TRUNCATED_OUTPUT}.*</Value>")
+    set(RunCMake_TEST_FAILED "Test output truncation failed:\n ${test_result}\nExpected: ${TRUNCATED_OUTPUT}")
+  endif()
+else()
+  set(RunCMake_TEST_FAILED "Test.xml not found")
+endif()

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

@@ -0,0 +1 @@
+^Cannot find file: .*/Tests/RunCMake/CTestCommandLine/TestOutputTruncation.*/DartConfiguration.tcl

+ 17 - 0
Tests/RunCMake/ctest_test/RunCMakeTest.cmake

@@ -80,6 +80,23 @@ add_test(NAME FailingTest COMMAND ${CMAKE_COMMAND} -E no_such_command)
 endfunction()
 run_TestOutputSize()
 
+# Test --test-output-truncation
+function(run_TestOutputTruncation mode expected)
+  set(CASE_CTEST_TEST_ARGS EXCLUDE RunCMakeVersion)
+  set(TRUNCATED_OUTPUT ${expected})  # used in TestOutputTruncation-check.cmake
+  set(CASE_TEST_PREFIX_CODE [[
+set( CTEST_CUSTOM_TEST_OUTPUT_TRUNCATION${mode})
+  ]])
+  set(CASE_CMAKELISTS_SUFFIX_CODE [[
+add_test(NAME Truncation_${mode} COMMAND ${CMAKE_COMMAND} -E echo 123456789)
+  ]])
+
+  run_ctest(TestOutputTruncation)
+endfunction()
+run_TestOutputTruncation("head" "...6789")
+run_TestOutputTruncation("middle" "12....*...89")
+run_TestOutputTruncation("tail" "12345...")
+
 run_ctest_test(TestRepeatBad1 REPEAT UNKNOWN:3)
 run_ctest_test(TestRepeatBad2 REPEAT UNTIL_FAIL:-1)
 

+ 12 - 0
Tests/RunCMake/ctest_test/TestOutputTruncation-check.cmake

@@ -0,0 +1,12 @@
+file(GLOB test_xml_file "${RunCMake_TEST_BINARY_DIR}/Testing/*/Test.xml")
+if(test_xml_file)
+  file(READ "${test_xml_file}" test_xml LIMIT 4096)
+  if("${test_xml}" MATCHES [[(<Test Status="passed">.*</Test>)]])
+    set(test_result "${CMAKE_MATCH_1}")
+  endif()
+  if(NOT "${test_result}" MATCHES "<Value>.*${TRUNCATED_OUTPUT}.*</Value>")
+    set(RunCMake_TEST_FAILED "Test output truncation failed:\n ${test_result}\nExpected: ${TRUNCATED_OUTPUT}")
+  endif()
+else()
+  set(RunCMake_TEST_FAILED "Test.xml not found")
+endif()