/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmCTestLaunch.h" #include #include #include #include #include #include #include "cmsys/FStream.hxx" #include "cmsys/RegularExpression.hxx" #include "cm_fileno.hxx" #include "cmCTestLaunchReporter.h" #include "cmGlobalGenerator.h" #include "cmMakefile.h" #include "cmProcessOutput.h" #include "cmState.h" #include "cmStateSnapshot.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmUVHandlePtr.h" #include "cmUVProcessChain.h" #include "cmUVStream.h" #include "cmake.h" #ifdef _WIN32 # include // for std{out,err} and fileno # include // for _O_BINARY # include // for _setmode #endif cmCTestLaunch::cmCTestLaunch(int argc, const char* const* argv) { if (!this->ParseArguments(argc, argv)) { return; } this->Reporter.RealArgs = this->RealArgs; this->Reporter.ComputeFileNames(); this->ScrapeRulesLoaded = false; this->HaveOut = false; this->HaveErr = false; } cmCTestLaunch::~cmCTestLaunch() = default; bool cmCTestLaunch::ParseArguments(int argc, const char* const* argv) { // Launcher options occur first and are separated from the real // command line by a '--' option. enum Doing { DoingNone, DoingOutput, DoingSource, DoingLanguage, DoingTargetName, DoingTargetType, DoingBuildDir, DoingCount, DoingFilterPrefix }; Doing doing = DoingNone; int arg0 = 0; for (int i = 1; !arg0 && i < argc; ++i) { const char* arg = argv[i]; if (strcmp(arg, "--") == 0) { arg0 = i + 1; } else if (strcmp(arg, "--output") == 0) { doing = DoingOutput; } else if (strcmp(arg, "--source") == 0) { doing = DoingSource; } else if (strcmp(arg, "--language") == 0) { doing = DoingLanguage; } else if (strcmp(arg, "--target-name") == 0) { doing = DoingTargetName; } else if (strcmp(arg, "--target-type") == 0) { doing = DoingTargetType; } else if (strcmp(arg, "--build-dir") == 0) { doing = DoingBuildDir; } else if (strcmp(arg, "--filter-prefix") == 0) { doing = DoingFilterPrefix; } else if (doing == DoingOutput) { this->Reporter.OptionOutput = arg; doing = DoingNone; } else if (doing == DoingSource) { this->Reporter.OptionSource = arg; doing = DoingNone; } else if (doing == DoingLanguage) { this->Reporter.OptionLanguage = arg; if (this->Reporter.OptionLanguage == "CXX") { this->Reporter.OptionLanguage = "C++"; } doing = DoingNone; } else if (doing == DoingTargetName) { this->Reporter.OptionTargetName = arg; doing = DoingNone; } else if (doing == DoingTargetType) { this->Reporter.OptionTargetType = arg; doing = DoingNone; } else if (doing == DoingBuildDir) { this->Reporter.OptionBuildDir = arg; doing = DoingNone; } else if (doing == DoingFilterPrefix) { this->Reporter.OptionFilterPrefix = arg; doing = DoingNone; } } // Extract the real command line. if (arg0) { for (int i = 0; i < argc - arg0; ++i) { this->RealArgV.emplace_back((argv + arg0)[i]); this->HandleRealArg((argv + arg0)[i]); } return true; } std::cerr << "No launch/command separator ('--') found!\n"; return false; } void cmCTestLaunch::HandleRealArg(const char* arg) { #ifdef _WIN32 // Expand response file arguments. if (arg[0] == '@' && cmSystemTools::FileExists(arg + 1)) { cmsys::ifstream fin(arg + 1); std::string line; while (cmSystemTools::GetLineFromStream(fin, line)) { cmSystemTools::ParseWindowsCommandLine(line.c_str(), this->RealArgs); } return; } #endif this->RealArgs.emplace_back(arg); } void cmCTestLaunch::RunChild() { // Ignore noopt make rules if (this->RealArgs.empty() || this->RealArgs[0] == ":") { this->Reporter.ExitCode = 0; return; } // Prepare to run the real command. cmUVProcessChainBuilder builder; builder.AddCommand(this->RealArgV); cmsys::ofstream fout; cmsys::ofstream ferr; if (this->Reporter.Passthru) { // In passthru mode we just share the output pipes. builder .SetExternalStream(cmUVProcessChainBuilder::Stream_OUTPUT, cm_fileno(stdout)) .SetExternalStream(cmUVProcessChainBuilder::Stream_ERROR, cm_fileno(stderr)); } else { // In full mode we record the child output pipes to log files. builder.SetBuiltinStream(cmUVProcessChainBuilder::Stream_OUTPUT) .SetBuiltinStream(cmUVProcessChainBuilder::Stream_ERROR); 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 // Do this so that newline transformation is not done when writing to cout // and cerr below. _setmode(fileno(stdout), _O_BINARY); _setmode(fileno(stderr), _O_BINARY); #endif // Run the real command. auto chain = builder.Start(); // Record child stdout and stderr if necessary. cm::uv_pipe_ptr outPipe; cm::uv_pipe_ptr errPipe; bool outFinished = true; bool errFinished = true; cmProcessOutput processOutput; std::unique_ptr outputHandle; std::unique_ptr errorHandle; if (!this->Reporter.Passthru) { auto beginRead = [&chain, &processOutput]( cm::uv_pipe_ptr& pipe, int stream, std::ostream& out, cmsys::ofstream& file, bool& haveData, bool& finished, int id) -> std::unique_ptr { pipe.init(chain.GetLoop(), 0); uv_pipe_open(pipe, stream); finished = false; return cmUVStreamRead( pipe, [&processOutput, &out, &file, id, &haveData](std::vector data) { std::string strdata; processOutput.DecodeText(data.data(), data.size(), strdata, id); file.write(strdata.c_str(), strdata.size()); out.write(strdata.c_str(), strdata.size()); haveData = true; }, [&processOutput, &out, &file, &finished, id]() { std::string strdata; processOutput.DecodeText(std::string(), strdata, id); if (!strdata.empty()) { file.write(strdata.c_str(), strdata.size()); out.write(strdata.c_str(), strdata.size()); } finished = true; }); }; outputHandle = beginRead(outPipe, chain.OutputStream(), std::cout, fout, this->HaveOut, outFinished, 1); errorHandle = beginRead(errPipe, chain.ErrorStream(), std::cerr, ferr, this->HaveErr, errFinished, 2); } // Wait for the real command to finish. while (!(chain.Finished() && outFinished && errFinished)) { uv_run(&chain.GetLoop(), UV_RUN_ONCE); } this->Reporter.Status = chain.GetStatus(0); if (this->Reporter.Status.GetException().first == cmUVProcessChain::ExceptionCode::Spawn) { this->Reporter.ExitCode = 1; } else { this->Reporter.ExitCode = static_cast(this->Reporter.Status.ExitStatus); } } int cmCTestLaunch::Run() { this->RunChild(); if (this->CheckResults()) { return this->Reporter.ExitCode; } this->LoadConfig(); this->Reporter.WriteXML(); return this->Reporter.ExitCode; } bool cmCTestLaunch::CheckResults() { // Skip XML in passthru mode. if (this->Reporter.Passthru) { return true; } // We always report failure for error conditions. if (this->Reporter.IsError()) { return false; } // Scrape the output logs to look for warnings. if ((this->HaveErr && this->ScrapeLog(this->Reporter.LogErr)) || (this->HaveOut && this->ScrapeLog(this->Reporter.LogOut))) { return false; } return true; } void cmCTestLaunch::LoadScrapeRules() { if (this->ScrapeRulesLoaded) { return; } this->ScrapeRulesLoaded = true; // Load custom match rules given to us by CTest. this->LoadScrapeRules("Warning", this->Reporter.RegexWarning); this->LoadScrapeRules("WarningSuppress", this->Reporter.RegexWarningSuppress); } void cmCTestLaunch::LoadScrapeRules( const char* purpose, std::vector& regexps) const { 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; while (cmSystemTools::GetLineFromStream(fin, line)) { if (rex.compile(line)) { regexps.push_back(rex); } } } bool cmCTestLaunch::ScrapeLog(std::string const& fname) { this->LoadScrapeRules(); // Look for log file lines matching warning expressions but not // suppression expressions. cmsys::ifstream fin(fname.c_str(), std::ios::in | std::ios::binary); std::string line; while (cmSystemTools::GetLineFromStream(fin, line)) { if (this->Reporter.MatchesFilterPrefix(line)) { continue; } if (this->Reporter.Match(line, this->Reporter.RegexWarning) && !this->Reporter.Match(line, this->Reporter.RegexWarningSuppress)) { return true; } } return false; } int cmCTestLaunch::Main(int argc, const char* const argv[]) { if (argc == 2) { std::cerr << "ctest --launch: this mode is for internal CTest use only" << std::endl; return 1; } cmCTestLaunch self(argc, argv); return self.Run(); } void cmCTestLaunch::LoadConfig() { cmake cm(cmake::RoleScript, cmState::CTest); cm.SetHomeDirectory(""); cm.SetHomeOutputDirectory(""); cm.GetCurrentSnapshot().SetDefaultDefinitions(); cmGlobalGenerator gg(&cm); cmMakefile mf(&gg, cm.GetCurrentSnapshot()); std::string fname = cmStrCat(this->Reporter.LogDir, "CTestLaunchConfig.cmake"); if (cmSystemTools::FileExists(fname) && mf.ReadListFile(fname)) { this->Reporter.SourceDir = mf.GetSafeDefinition("CTEST_SOURCE_DIRECTORY"); cmSystemTools::ConvertToUnixSlashes(this->Reporter.SourceDir); } }