瀏覽代碼

ctest: split launcher XML writer into separate class

This refactor will allow us to more easily add additional warnings and errors
to builds that use launchers.
Zack Galbreath 5 年之前
父節點
當前提交
56e4e942d2

+ 1 - 0
Source/CMakeLists.txt

@@ -946,6 +946,7 @@ set(CTEST_SRCS cmCTest.cxx
   CTest/cmCTestResourceAllocator.cxx
   CTest/cmCTestResourceSpec.cxx
   CTest/cmCTestLaunch.cxx
+  CTest/cmCTestLaunchReporter.cxx
   CTest/cmCTestMemCheckCommand.cxx
   CTest/cmCTestMemCheckHandler.cxx
   CTest/cmCTestMultiProcessHandler.cxx

+ 38 - 323
Source/CTest/cmCTestLaunch.cxx

@@ -2,7 +2,6 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmCTestLaunch.h"
 
-#include <cstdlib>
 #include <cstring>
 #include <iostream>
 
@@ -10,8 +9,7 @@
 #include "cmsys/Process.h"
 #include "cmsys/RegularExpression.hxx"
 
-#include "cmCryptoHash.h"
-#include "cmGeneratedFileStream.h"
+#include "cmCTestLaunchReporter.h"
 #include "cmGlobalGenerator.h"
 #include "cmMakefile.h"
 #include "cmProcessOutput.h"
@@ -19,7 +17,6 @@
 #include "cmStateSnapshot.h"
 #include "cmStringAlgorithms.h"
 #include "cmSystemTools.h"
-#include "cmXMLWriter.h"
 #include "cmake.h"
 
 #ifdef _WIN32
@@ -30,16 +27,14 @@
 
 cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
 {
-  this->Passthru = true;
   this->Process = nullptr;
-  this->ExitCode = 1;
-  this->CWD = cmSystemTools::GetCurrentWorkingDirectory();
 
   if (!this->ParseArguments(argc, argv)) {
     return;
   }
 
-  this->ComputeFileNames();
+  this->Reporter.RealArgs = this->RealArgs;
+  this->Reporter.ComputeFileNames();
 
   this->ScrapeRulesLoaded = false;
   this->HaveOut = false;
@@ -50,10 +45,6 @@ cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv)
 cmCTestLaunch::~cmCTestLaunch()
 {
   cmsysProcess_Delete(this->Process);
-  if (!this->Passthru) {
-    cmSystemTools::RemoveFile(this->LogOut);
-    cmSystemTools::RemoveFile(this->LogErr);
-  }
 }
 
 bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
@@ -93,28 +84,28 @@ bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv)
     } else if (strcmp(arg, "--filter-prefix") == 0) {
       doing = DoingFilterPrefix;
     } else if (doing == DoingOutput) {
-      this->OptionOutput = arg;
+      this->Reporter.OptionOutput = arg;
       doing = DoingNone;
     } else if (doing == DoingSource) {
-      this->OptionSource = arg;
+      this->Reporter.OptionSource = arg;
       doing = DoingNone;
     } else if (doing == DoingLanguage) {
-      this->OptionLanguage = arg;
-      if (this->OptionLanguage == "CXX") {
-        this->OptionLanguage = "C++";
+      this->Reporter.OptionLanguage = arg;
+      if (this->Reporter.OptionLanguage == "CXX") {
+        this->Reporter.OptionLanguage = "C++";
       }
       doing = DoingNone;
     } else if (doing == DoingTargetName) {
-      this->OptionTargetName = arg;
+      this->Reporter.OptionTargetName = arg;
       doing = DoingNone;
     } else if (doing == DoingTargetType) {
-      this->OptionTargetType = arg;
+      this->Reporter.OptionTargetType = arg;
       doing = DoingNone;
     } else if (doing == DoingBuildDir) {
-      this->OptionBuildDir = arg;
+      this->Reporter.OptionBuildDir = arg;
       doing = DoingNone;
     } else if (doing == DoingFilterPrefix) {
-      this->OptionFilterPrefix = arg;
+      this->Reporter.OptionFilterPrefix = arg;
       doing = DoingNone;
     }
   }
@@ -150,42 +141,11 @@ void cmCTestLaunch::HandleRealArg(const char* arg)
   this->RealArgs.emplace_back(arg);
 }
 
-void cmCTestLaunch::ComputeFileNames()
-{
-  // We just passthru the behavior of the real command unless the
-  // CTEST_LAUNCH_LOGS environment variable is set.
-  const char* d = getenv("CTEST_LAUNCH_LOGS");
-  if (!(d && *d)) {
-    return;
-  }
-  this->Passthru = false;
-
-  // The environment variable specifies the directory into which we
-  // generate build logs.
-  this->LogDir = d;
-  cmSystemTools::ConvertToUnixSlashes(this->LogDir);
-  this->LogDir += "/";
-
-  // We hash the input command working dir and command line to obtain
-  // a repeatable and (probably) unique name for log files.
-  cmCryptoHash md5(cmCryptoHash::AlgoMD5);
-  md5.Initialize();
-  md5.Append(this->CWD);
-  for (std::string const& realArg : this->RealArgs) {
-    md5.Append(realArg);
-  }
-  this->LogHash = md5.FinalizeHex();
-
-  // We store stdout and stderr in temporary log files.
-  this->LogOut = cmStrCat(this->LogDir, "launch-", this->LogHash, "-out.txt");
-  this->LogErr = cmStrCat(this->LogDir, "launch-", this->LogHash, "-err.txt");
-}
-
 void cmCTestLaunch::RunChild()
 {
   // Ignore noopt make rules
   if (this->RealArgs.empty() || this->RealArgs[0] == ":") {
-    this->ExitCode = 0;
+    this->Reporter.ExitCode = 0;
     return;
   }
 
@@ -195,14 +155,14 @@ void cmCTestLaunch::RunChild()
 
   cmsys::ofstream fout;
   cmsys::ofstream ferr;
-  if (this->Passthru) {
+  if (this->Reporter.Passthru) {
     // In passthru mode we just share the output pipes.
     cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDOUT, 1);
     cmsysProcess_SetPipeShared(cp, cmsysProcess_Pipe_STDERR, 1);
   } else {
     // In full mode we record the child output pipes to log files.
-    fout.open(this->LogOut.c_str(), std::ios::out | std::ios::binary);
-    ferr.open(this->LogErr.c_str(), std::ios::out | std::ios::binary);
+    fout.open(this->Reporter.LogOut.c_str(), std::ios::out | std::ios::binary);
+    ferr.open(this->Reporter.LogErr.c_str(), std::ios::out | std::ios::binary);
   }
 
 #ifdef _WIN32
@@ -216,7 +176,7 @@ void cmCTestLaunch::RunChild()
   cmsysProcess_Execute(cp);
 
   // Record child stdout and stderr if necessary.
-  if (!this->Passthru) {
+  if (!this->Reporter.Passthru) {
     char* data = nullptr;
     int length = 0;
     cmProcessOutput processOutput;
@@ -248,7 +208,7 @@ void cmCTestLaunch::RunChild()
 
   // Wait for the real command to finish.
   cmsysProcess_WaitForExit(cp, nullptr);
-  this->ExitCode = cmsysProcess_GetExitValue(cp);
+  this->Reporter.ExitCode = cmsysProcess_GetExitValue(cp);
 }
 
 int cmCTestLaunch::Run()
@@ -261,255 +221,31 @@ int cmCTestLaunch::Run()
   this->RunChild();
 
   if (this->CheckResults()) {
-    return this->ExitCode;
+    return this->Reporter.ExitCode;
   }
 
   this->LoadConfig();
-  this->WriteXML();
-
-  return this->ExitCode;
-}
-
-void cmCTestLaunch::LoadLabels()
-{
-  if (this->OptionBuildDir.empty() || this->OptionTargetName.empty()) {
-    return;
-  }
-
-  // Labels are listed in per-target files.
-  std::string fname = cmStrCat(this->OptionBuildDir, "/CMakeFiles/",
-                               this->OptionTargetName, ".dir/Labels.txt");
-
-  // We are interested in per-target labels for this source file.
-  std::string source = this->OptionSource;
-  cmSystemTools::ConvertToUnixSlashes(source);
-
-  // Load the labels file.
-  cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
-  if (!fin) {
-    return;
-  }
-  bool inTarget = true;
-  bool inSource = false;
-  std::string line;
-  while (cmSystemTools::GetLineFromStream(fin, line)) {
-    if (line.empty() || line[0] == '#') {
-      // Ignore blank and comment lines.
-      continue;
-    }
-    if (line[0] == ' ') {
-      // Label lines appear indented by one space.
-      if (inTarget || inSource) {
-        this->Labels.insert(line.substr(1));
-      }
-    } else if (!this->OptionSource.empty() && !inSource) {
-      // Non-indented lines specify a source file name.  The first one
-      // is the end of the target-wide labels.  Use labels following a
-      // matching source.
-      inTarget = false;
-      inSource = this->SourceMatches(line, source);
-    } else {
-      return;
-    }
-  }
-}
-
-bool cmCTestLaunch::SourceMatches(std::string const& lhs,
-                                  std::string const& rhs)
-{
-  // TODO: Case sensitivity, UseRelativePaths, etc.  Note that both
-  // paths in the comparison get generated by CMake.  This is done for
-  // every source in the target, so it should be efficient (cannot use
-  // cmSystemTools::IsSameFile).
-  return lhs == rhs;
-}
-
-bool cmCTestLaunch::IsError() const
-{
-  return this->ExitCode != 0;
-}
-
-void cmCTestLaunch::WriteXML()
-{
-  // Name the xml file.
-  std::string logXML =
-    cmStrCat(this->LogDir, this->IsError() ? "error-" : "warning-",
-             this->LogHash, ".xml");
-
-  // Use cmGeneratedFileStream to atomically create the report file.
-  cmGeneratedFileStream fxml(logXML);
-  cmXMLWriter xml(fxml, 2);
-  cmXMLElement e2(xml, "Failure");
-  e2.Attribute("type", this->IsError() ? "Error" : "Warning");
-  this->WriteXMLAction(e2);
-  this->WriteXMLCommand(e2);
-  this->WriteXMLResult(e2);
-  this->WriteXMLLabels(e2);
-}
-
-void cmCTestLaunch::WriteXMLAction(cmXMLElement& e2)
-{
-  e2.Comment("Meta-information about the build action");
-  cmXMLElement e3(e2, "Action");
-
-  // TargetName
-  if (!this->OptionTargetName.empty()) {
-    e3.Element("TargetName", this->OptionTargetName);
-  }
-
-  // Language
-  if (!this->OptionLanguage.empty()) {
-    e3.Element("Language", this->OptionLanguage);
-  }
-
-  // SourceFile
-  if (!this->OptionSource.empty()) {
-    std::string source = this->OptionSource;
-    cmSystemTools::ConvertToUnixSlashes(source);
-
-    // If file is in source tree use its relative location.
-    if (cmSystemTools::FileIsFullPath(this->SourceDir) &&
-        cmSystemTools::FileIsFullPath(source) &&
-        cmSystemTools::IsSubDirectory(source, this->SourceDir)) {
-      source = cmSystemTools::RelativePath(this->SourceDir, source);
-    }
-
-    e3.Element("SourceFile", source);
-  }
-
-  // OutputFile
-  if (!this->OptionOutput.empty()) {
-    e3.Element("OutputFile", this->OptionOutput);
-  }
-
-  // OutputType
-  const char* outputType = nullptr;
-  if (!this->OptionTargetType.empty()) {
-    if (this->OptionTargetType == "EXECUTABLE") {
-      outputType = "executable";
-    } else if (this->OptionTargetType == "SHARED_LIBRARY") {
-      outputType = "shared library";
-    } else if (this->OptionTargetType == "MODULE_LIBRARY") {
-      outputType = "module library";
-    } else if (this->OptionTargetType == "STATIC_LIBRARY") {
-      outputType = "static library";
-    }
-  } else if (!this->OptionSource.empty()) {
-    outputType = "object file";
-  }
-  if (outputType) {
-    e3.Element("OutputType", outputType);
-  }
-}
-
-void cmCTestLaunch::WriteXMLCommand(cmXMLElement& e2)
-{
-  e2.Comment("Details of command");
-  cmXMLElement e3(e2, "Command");
-  if (!this->CWD.empty()) {
-    e3.Element("WorkingDirectory", this->CWD);
-  }
-  for (std::string const& realArg : this->RealArgs) {
-    e3.Element("Argument", realArg);
-  }
-}
-
-void cmCTestLaunch::WriteXMLResult(cmXMLElement& e2)
-{
-  e2.Comment("Result of command");
-  cmXMLElement e3(e2, "Result");
-
-  // StdOut
-  this->DumpFileToXML(e3, "StdOut", this->LogOut);
-
-  // StdErr
-  this->DumpFileToXML(e3, "StdErr", this->LogErr);
-
-  // ExitCondition
-  cmXMLElement e4(e3, "ExitCondition");
-  cmsysProcess* cp = this->Process;
-  switch (cmsysProcess_GetState(cp)) {
-    case cmsysProcess_State_Starting:
-      e4.Content("No process has been executed");
-      break;
-    case cmsysProcess_State_Executing:
-      e4.Content("The process is still executing");
-      break;
-    case cmsysProcess_State_Disowned:
-      e4.Content("Disowned");
-      break;
-    case cmsysProcess_State_Killed:
-      e4.Content("Killed by parent");
-      break;
-
-    case cmsysProcess_State_Expired:
-      e4.Content("Killed when timeout expired");
-      break;
-    case cmsysProcess_State_Exited:
-      e4.Content(this->ExitCode);
-      break;
-    case cmsysProcess_State_Exception:
-      e4.Content("Terminated abnormally: ");
-      e4.Content(cmsysProcess_GetExceptionString(cp));
-      break;
-    case cmsysProcess_State_Error:
-      e4.Content("Error administrating child process: ");
-      e4.Content(cmsysProcess_GetErrorString(cp));
-      break;
-  }
-}
+  this->Reporter.Process = this->Process;
+  this->Reporter.WriteXML();
 
-void cmCTestLaunch::WriteXMLLabels(cmXMLElement& e2)
-{
-  this->LoadLabels();
-  if (!this->Labels.empty()) {
-    e2.Comment("Interested parties");
-    cmXMLElement e3(e2, "Labels");
-    for (std::string const& label : this->Labels) {
-      e3.Element("Label", label);
-    }
-  }
-}
-
-void cmCTestLaunch::DumpFileToXML(cmXMLElement& e3, const char* tag,
-                                  std::string const& fname)
-{
-  cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
-
-  std::string line;
-  const char* sep = "";
-
-  cmXMLElement e4(e3, tag);
-  while (cmSystemTools::GetLineFromStream(fin, line)) {
-    if (MatchesFilterPrefix(line)) {
-      continue;
-    }
-    if (this->Match(line, this->RegexWarningSuppress)) {
-      line = cmStrCat("[CTest: warning suppressed] ", line);
-    } else if (this->Match(line, this->RegexWarning)) {
-      line = cmStrCat("[CTest: warning matched] ", line);
-    }
-    e4.Content(sep);
-    e4.Content(line);
-    sep = "\n";
-  }
+  return this->Reporter.ExitCode;
 }
 
 bool cmCTestLaunch::CheckResults()
 {
   // Skip XML in passthru mode.
-  if (this->Passthru) {
+  if (this->Reporter.Passthru) {
     return true;
   }
 
   // We always report failure for error conditions.
-  if (this->IsError()) {
+  if (this->Reporter.IsError()) {
     return false;
   }
 
   // Scrape the output logs to look for warnings.
-  if ((this->HaveErr && this->ScrapeLog(this->LogErr)) ||
-      (this->HaveOut && this->ScrapeLog(this->LogOut))) {
+  if ((this->HaveErr && this->ScrapeLog(this->Reporter.LogErr)) ||
+      (this->HaveOut && this->ScrapeLog(this->Reporter.LogOut))) {
     return false;
   }
   return true;
@@ -522,22 +258,17 @@ void cmCTestLaunch::LoadScrapeRules()
   }
   this->ScrapeRulesLoaded = true;
 
-  // Common compiler warning formats.  These are much simpler than the
-  // full log-scraping expressions because we do not need to extract
-  // file and line information.
-  this->RegexWarning.emplace_back("(^|[ :])[Ww][Aa][Rr][Nn][Ii][Nn][Gg]");
-  this->RegexWarning.emplace_back("(^|[ :])[Rr][Ee][Mm][Aa][Rr][Kk]");
-  this->RegexWarning.emplace_back("(^|[ :])[Nn][Oo][Tt][Ee]");
-
   // Load custom match rules given to us by CTest.
-  this->LoadScrapeRules("Warning", this->RegexWarning);
-  this->LoadScrapeRules("WarningSuppress", this->RegexWarningSuppress);
+  this->LoadScrapeRules("Warning", this->Reporter.RegexWarning);
+  this->LoadScrapeRules("WarningSuppress",
+                        this->Reporter.RegexWarningSuppress);
 }
 
 void cmCTestLaunch::LoadScrapeRules(
   const char* purpose, std::vector<cmsys::RegularExpression>& regexps)
 {
-  std::string fname = cmStrCat(this->LogDir, "Custom", purpose, ".txt");
+  std::string fname =
+    cmStrCat(this->Reporter.LogDir, "Custom", purpose, ".txt");
   cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
   std::string line;
   cmsys::RegularExpression rex;
@@ -557,35 +288,18 @@ bool cmCTestLaunch::ScrapeLog(std::string const& fname)
   cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
   std::string line;
   while (cmSystemTools::GetLineFromStream(fin, line)) {
-    if (MatchesFilterPrefix(line)) {
+    if (this->Reporter.MatchesFilterPrefix(line)) {
       continue;
     }
 
-    if (this->Match(line, this->RegexWarning) &&
-        !this->Match(line, this->RegexWarningSuppress)) {
+    if (this->Reporter.Match(line, this->Reporter.RegexWarning) &&
+        !this->Reporter.Match(line, this->Reporter.RegexWarningSuppress)) {
       return true;
     }
   }
   return false;
 }
 
-bool cmCTestLaunch::Match(std::string const& line,
-                          std::vector<cmsys::RegularExpression>& regexps)
-{
-  for (cmsys::RegularExpression& r : regexps) {
-    if (r.find(line)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-bool cmCTestLaunch::MatchesFilterPrefix(std::string const& line) const
-{
-  return !this->OptionFilterPrefix.empty() &&
-    cmHasPrefix(line, this->OptionFilterPrefix);
-}
-
 int cmCTestLaunch::Main(int argc, const char* const argv[])
 {
   if (argc == 2) {
@@ -605,9 +319,10 @@ void cmCTestLaunch::LoadConfig()
   cm.GetCurrentSnapshot().SetDefaultDefinitions();
   cmGlobalGenerator gg(&cm);
   cmMakefile mf(&gg, cm.GetCurrentSnapshot());
-  std::string fname = cmStrCat(this->LogDir, "CTestLaunchConfig.cmake");
+  std::string fname =
+    cmStrCat(this->Reporter.LogDir, "CTestLaunchConfig.cmake");
   if (cmSystemTools::FileExists(fname) && mf.ReadListFile(fname)) {
-    this->SourceDir = mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY");
-    cmSystemTools::ConvertToUnixSlashes(this->SourceDir);
+    this->Reporter.SourceDir = mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY");
+    cmSystemTools::ConvertToUnixSlashes(this->Reporter.SourceDir);
   }
 }

+ 11 - 44
Source/CTest/cmCTestLaunch.h

@@ -4,13 +4,14 @@
 
 #include "cmConfigure.h" // IWYU pragma: keep
 
-#include <set>
 #include <string>
 #include <vector>
 
-#include "cmsys/RegularExpression.hxx"
+#include "cmCTestLaunchReporter.h"
 
-class cmXMLElement;
+namespace cmsys {
+class RegularExpression;
+}
 
 /** \class cmCTestLaunch
  * \brief Launcher for make rules to report results for ctest
@@ -35,70 +36,36 @@ private:
   int Run();
   void RunChild();
 
-  // Methods to check the result of the real command.
-  bool IsError() const;
+  // Method to check the result of the real command.
   bool CheckResults();
 
-  // Launcher options specified before the real command.
-  std::string OptionOutput;
-  std::string OptionSource;
-  std::string OptionLanguage;
-  std::string OptionTargetName;
-  std::string OptionTargetType;
-  std::string OptionBuildDir;
-  std::string OptionFilterPrefix;
+  // Parse out launcher-specific options specified before the real command.
   bool ParseArguments(int argc, const char* const* argv);
 
   // The real command line appearing after launcher arguments.
   int RealArgC;
   const char* const* RealArgV;
-  std::string CWD;
 
   // The real command line after response file expansion.
   std::vector<std::string> RealArgs;
   void HandleRealArg(const char* arg);
 
-  // A hash of the real command line is unique and unlikely to collide.
-  std::string LogHash;
-  void ComputeFileNames();
-
-  bool Passthru;
   struct cmsysProcess_s* Process;
-  int ExitCode;
 
-  // Temporary log files for stdout and stderr of real command.
-  std::string LogDir;
-  std::string LogOut;
-  std::string LogErr;
+  // Whether or not any data have been written to stdout or stderr.
   bool HaveOut;
   bool HaveErr;
 
-  // Labels associated with the build rule.
-  std::set<std::string> Labels;
-  void LoadLabels();
-  bool SourceMatches(std::string const& lhs, std::string const& rhs);
-
-  // Regular expressions to match warnings and their exceptions.
+  // Load custom rules to match warnings and their exceptions.
   bool ScrapeRulesLoaded;
-  std::vector<cmsys::RegularExpression> RegexWarning;
-  std::vector<cmsys::RegularExpression> RegexWarningSuppress;
   void LoadScrapeRules();
   void LoadScrapeRules(const char* purpose,
                        std::vector<cmsys::RegularExpression>& regexps);
   bool ScrapeLog(std::string const& fname);
-  bool Match(std::string const& line,
-             std::vector<cmsys::RegularExpression>& regexps);
-  bool MatchesFilterPrefix(std::string const& line) const;
-
-  // Methods to generate the xml fragment.
-  void WriteXML();
-  void WriteXMLAction(cmXMLElement&);
-  void WriteXMLCommand(cmXMLElement&);
-  void WriteXMLResult(cmXMLElement&);
-  void WriteXMLLabels(cmXMLElement&);
-  void DumpFileToXML(cmXMLElement&, const char* tag, std::string const& fname);
+
+  // Helper class to generate the xml fragment.
+  cmCTestLaunchReporter Reporter;
 
   // Configuration
   void LoadConfig();
-  std::string SourceDir;
 };

+ 318 - 0
Source/CTest/cmCTestLaunchReporter.cxx

@@ -0,0 +1,318 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#include "cmCTestLaunchReporter.h"
+
+#include <cstdlib>
+
+#include "cmsys/FStream.hxx"
+#include "cmsys/Process.h"
+#include "cmsys/RegularExpression.hxx"
+
+#include "cmCryptoHash.h"
+#include "cmGeneratedFileStream.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+#include "cmXMLWriter.h"
+
+#ifdef _WIN32
+#  include <fcntl.h> // for _O_BINARY
+#  include <io.h>    // for _setmode
+#  include <stdio.h> // for std{out,err} and fileno
+#endif
+
+cmCTestLaunchReporter::cmCTestLaunchReporter()
+{
+  this->Passthru = true;
+  this->ExitCode = 1;
+  this->CWD = cmSystemTools::GetCurrentWorkingDirectory();
+
+  this->ComputeFileNames();
+
+  // Common compiler warning formats.  These are much simpler than the
+  // full log-scraping expressions because we do not need to extract
+  // file and line information.
+  this->RegexWarning.emplace_back("(^|[ :])[Ww][Aa][Rr][Nn][Ii][Nn][Gg]");
+  this->RegexWarning.emplace_back("(^|[ :])[Rr][Ee][Mm][Aa][Rr][Kk]");
+  this->RegexWarning.emplace_back("(^|[ :])[Nn][Oo][Tt][Ee]");
+}
+
+cmCTestLaunchReporter::~cmCTestLaunchReporter()
+{
+  if (!this->Passthru) {
+    cmSystemTools::RemoveFile(this->LogOut);
+    cmSystemTools::RemoveFile(this->LogErr);
+  }
+}
+
+void cmCTestLaunchReporter::ComputeFileNames()
+{
+  // We just passthru the behavior of the real command unless the
+  // CTEST_LAUNCH_LOGS environment variable is set.
+  const char* d = getenv("CTEST_LAUNCH_LOGS");
+  if (!(d && *d)) {
+    return;
+  }
+  this->Passthru = false;
+
+  // The environment variable specifies the directory into which we
+  // generate build logs.
+  this->LogDir = d;
+  cmSystemTools::ConvertToUnixSlashes(this->LogDir);
+  this->LogDir += "/";
+
+  // We hash the input command working dir and command line to obtain
+  // a repeatable and (probably) unique name for log files.
+  cmCryptoHash md5(cmCryptoHash::AlgoMD5);
+  md5.Initialize();
+  md5.Append(this->CWD);
+  for (std::string const& realArg : this->RealArgs) {
+    md5.Append(realArg);
+  }
+  this->LogHash = md5.FinalizeHex();
+
+  // We store stdout and stderr in temporary log files.
+  this->LogOut = cmStrCat(this->LogDir, "launch-", this->LogHash, "-out.txt");
+  this->LogErr = cmStrCat(this->LogDir, "launch-", this->LogHash, "-err.txt");
+}
+
+void cmCTestLaunchReporter::LoadLabels()
+{
+  if (this->OptionBuildDir.empty() || this->OptionTargetName.empty()) {
+    return;
+  }
+
+  // Labels are listed in per-target files.
+  std::string fname = cmStrCat(this->OptionBuildDir, "/CMakeFiles/",
+                               this->OptionTargetName, ".dir/Labels.txt");
+
+  // We are interested in per-target labels for this source file.
+  std::string source = this->OptionSource;
+  cmSystemTools::ConvertToUnixSlashes(source);
+
+  // Load the labels file.
+  cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
+  if (!fin) {
+    return;
+  }
+  bool inTarget = true;
+  bool inSource = false;
+  std::string line;
+  while (cmSystemTools::GetLineFromStream(fin, line)) {
+    if (line.empty() || line[0] == '#') {
+      // Ignore blank and comment lines.
+      continue;
+    }
+    if (line[0] == ' ') {
+      // Label lines appear indented by one space.
+      if (inTarget || inSource) {
+        this->Labels.insert(line.substr(1));
+      }
+    } else if (!this->OptionSource.empty() && !inSource) {
+      // Non-indented lines specify a source file name.  The first one
+      // is the end of the target-wide labels.  Use labels following a
+      // matching source.
+      inTarget = false;
+      inSource = this->SourceMatches(line, source);
+    } else {
+      return;
+    }
+  }
+}
+
+bool cmCTestLaunchReporter::SourceMatches(std::string const& lhs,
+                                          std::string const& rhs)
+{
+  // TODO: Case sensitivity, UseRelativePaths, etc.  Note that both
+  // paths in the comparison get generated by CMake.  This is done for
+  // every source in the target, so it should be efficient (cannot use
+  // cmSystemTools::IsSameFile).
+  return lhs == rhs;
+}
+
+bool cmCTestLaunchReporter::IsError() const
+{
+  return this->ExitCode != 0;
+}
+
+void cmCTestLaunchReporter::WriteXML()
+{
+  // Name the xml file.
+  std::string logXML =
+    cmStrCat(this->LogDir, this->IsError() ? "error-" : "warning-",
+             this->LogHash, ".xml");
+
+  // Use cmGeneratedFileStream to atomically create the report file.
+  cmGeneratedFileStream fxml(logXML);
+  cmXMLWriter xml(fxml, 2);
+  cmXMLElement e2(xml, "Failure");
+  e2.Attribute("type", this->IsError() ? "Error" : "Warning");
+  this->WriteXMLAction(e2);
+  this->WriteXMLCommand(e2);
+  this->WriteXMLResult(e2);
+  this->WriteXMLLabels(e2);
+}
+
+void cmCTestLaunchReporter::WriteXMLAction(cmXMLElement& e2)
+{
+  e2.Comment("Meta-information about the build action");
+  cmXMLElement e3(e2, "Action");
+
+  // TargetName
+  if (!this->OptionTargetName.empty()) {
+    e3.Element("TargetName", this->OptionTargetName);
+  }
+
+  // Language
+  if (!this->OptionLanguage.empty()) {
+    e3.Element("Language", this->OptionLanguage);
+  }
+
+  // SourceFile
+  if (!this->OptionSource.empty()) {
+    std::string source = this->OptionSource;
+    cmSystemTools::ConvertToUnixSlashes(source);
+
+    // If file is in source tree use its relative location.
+    if (cmSystemTools::FileIsFullPath(this->SourceDir) &&
+        cmSystemTools::FileIsFullPath(source) &&
+        cmSystemTools::IsSubDirectory(source, this->SourceDir)) {
+      source = cmSystemTools::RelativePath(this->SourceDir, source);
+    }
+
+    e3.Element("SourceFile", source);
+  }
+
+  // OutputFile
+  if (!this->OptionOutput.empty()) {
+    e3.Element("OutputFile", this->OptionOutput);
+  }
+
+  // OutputType
+  const char* outputType = nullptr;
+  if (!this->OptionTargetType.empty()) {
+    if (this->OptionTargetType == "EXECUTABLE") {
+      outputType = "executable";
+    } else if (this->OptionTargetType == "SHARED_LIBRARY") {
+      outputType = "shared library";
+    } else if (this->OptionTargetType == "MODULE_LIBRARY") {
+      outputType = "module library";
+    } else if (this->OptionTargetType == "STATIC_LIBRARY") {
+      outputType = "static library";
+    }
+  } else if (!this->OptionSource.empty()) {
+    outputType = "object file";
+  }
+  if (outputType) {
+    e3.Element("OutputType", outputType);
+  }
+}
+
+void cmCTestLaunchReporter::WriteXMLCommand(cmXMLElement& e2)
+{
+  e2.Comment("Details of command");
+  cmXMLElement e3(e2, "Command");
+  if (!this->CWD.empty()) {
+    e3.Element("WorkingDirectory", this->CWD);
+  }
+  for (std::string const& realArg : this->RealArgs) {
+    e3.Element("Argument", realArg);
+  }
+}
+
+void cmCTestLaunchReporter::WriteXMLResult(cmXMLElement& e2)
+{
+  e2.Comment("Result of command");
+  cmXMLElement e3(e2, "Result");
+
+  // StdOut
+  this->DumpFileToXML(e3, "StdOut", this->LogOut);
+
+  // StdErr
+  this->DumpFileToXML(e3, "StdErr", this->LogErr);
+
+  // ExitCondition
+  cmXMLElement e4(e3, "ExitCondition");
+  cmsysProcess* cp = this->Process;
+  switch (cmsysProcess_GetState(cp)) {
+    case cmsysProcess_State_Starting:
+      e4.Content("No process has been executed");
+      break;
+    case cmsysProcess_State_Executing:
+      e4.Content("The process is still executing");
+      break;
+    case cmsysProcess_State_Disowned:
+      e4.Content("Disowned");
+      break;
+    case cmsysProcess_State_Killed:
+      e4.Content("Killed by parent");
+      break;
+
+    case cmsysProcess_State_Expired:
+      e4.Content("Killed when timeout expired");
+      break;
+    case cmsysProcess_State_Exited:
+      e4.Content(this->ExitCode);
+      break;
+    case cmsysProcess_State_Exception:
+      e4.Content("Terminated abnormally: ");
+      e4.Content(cmsysProcess_GetExceptionString(cp));
+      break;
+    case cmsysProcess_State_Error:
+      e4.Content("Error administrating child process: ");
+      e4.Content(cmsysProcess_GetErrorString(cp));
+      break;
+  }
+}
+
+void cmCTestLaunchReporter::WriteXMLLabels(cmXMLElement& e2)
+{
+  this->LoadLabels();
+  if (!this->Labels.empty()) {
+    e2.Comment("Interested parties");
+    cmXMLElement e3(e2, "Labels");
+    for (std::string const& label : this->Labels) {
+      e3.Element("Label", label);
+    }
+  }
+}
+
+void cmCTestLaunchReporter::DumpFileToXML(cmXMLElement& e3, const char* tag,
+                                          std::string const& fname)
+{
+  cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary);
+
+  std::string line;
+  const char* sep = "";
+
+  cmXMLElement e4(e3, tag);
+  while (cmSystemTools::GetLineFromStream(fin, line)) {
+    if (MatchesFilterPrefix(line)) {
+      continue;
+    }
+    if (this->Match(line, this->RegexWarningSuppress)) {
+      line = cmStrCat("[CTest: warning suppressed] ", line);
+    } else if (this->Match(line, this->RegexWarning)) {
+      line = cmStrCat("[CTest: warning matched] ", line);
+    }
+    e4.Content(sep);
+    e4.Content(line);
+    sep = "\n";
+  }
+}
+
+bool cmCTestLaunchReporter::Match(
+  std::string const& line, std::vector<cmsys::RegularExpression>& regexps)
+{
+  for (cmsys::RegularExpression& r : regexps) {
+    if (r.find(line)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+bool cmCTestLaunchReporter::MatchesFilterPrefix(std::string const& line) const
+{
+  return !this->OptionFilterPrefix.empty() &&
+    cmHasPrefix(line, this->OptionFilterPrefix);
+}

+ 81 - 0
Source/CTest/cmCTestLaunchReporter.h

@@ -0,0 +1,81 @@
+/* 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
+
+#include <set>
+#include <string>
+#include <vector>
+
+#include "cmsys/RegularExpression.hxx"
+
+class cmXMLElement;
+
+/** \class cmCTestLaunchReporter
+ * \brief Generate CTest XML output for the 'ctest --launch' tool.
+ */
+class cmCTestLaunchReporter
+{
+public:
+  // Initialize the launcher from its command line.
+  cmCTestLaunchReporter();
+  ~cmCTestLaunchReporter();
+
+  cmCTestLaunchReporter(const cmCTestLaunchReporter&) = delete;
+  cmCTestLaunchReporter& operator=(const cmCTestLaunchReporter&) = delete;
+
+  // Methods to check the result of the real command.
+  bool IsError() const;
+
+  // Launcher options specified before the real command.
+  std::string OptionOutput;
+  std::string OptionSource;
+  std::string OptionLanguage;
+  std::string OptionTargetName;
+  std::string OptionTargetType;
+  std::string OptionBuildDir;
+  std::string OptionFilterPrefix;
+
+  // The real command line appearing after launcher arguments.
+  std::string CWD;
+
+  // The real command line after response file expansion.
+  std::vector<std::string> RealArgs;
+
+  // A hash of the real command line is unique and unlikely to collide.
+  std::string LogHash;
+  void ComputeFileNames();
+
+  bool Passthru;
+  struct cmsysProcess_s* Process;
+  int ExitCode;
+
+  // Temporary log files for stdout and stderr of real command.
+  std::string LogDir;
+  std::string LogOut;
+  std::string LogErr;
+
+  // Labels associated with the build rule.
+  std::set<std::string> Labels;
+  void LoadLabels();
+  bool SourceMatches(std::string const& lhs, std::string const& rhs);
+
+  // Regular expressions to match warnings and their exceptions.
+  std::vector<cmsys::RegularExpression> RegexWarning;
+  std::vector<cmsys::RegularExpression> RegexWarningSuppress;
+  bool Match(std::string const& line,
+             std::vector<cmsys::RegularExpression>& regexps);
+  bool MatchesFilterPrefix(std::string const& line) const;
+
+  // Methods to generate the xml fragment.
+  void WriteXML();
+  void WriteXMLAction(cmXMLElement&);
+  void WriteXMLCommand(cmXMLElement&);
+  void WriteXMLResult(cmXMLElement&);
+  void WriteXMLLabels(cmXMLElement&);
+  void DumpFileToXML(cmXMLElement&, const char* tag, std::string const& fname);
+
+  // Configuration
+  std::string SourceDir;
+};