| /* 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 "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. |
| std::string d; |
| if (!cmSystemTools::GetEnv("CTEST_LAUNCH_LOGS", d) || d.empty()) { |
| 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); |
| } |