| /* 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 <cstdio> |
| #include <cstring> |
| #include <iostream> |
| #include <memory> |
| #include <utility> |
| |
| #include <cm3p/uv.h> |
| |
| #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 <cstdio> // for std{out,err} and fileno |
| |
| # include <fcntl.h> // for _O_BINARY |
| # include <io.h> // 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<cmUVStreamReadHandle> outputHandle; |
| std::unique_ptr<cmUVStreamReadHandle> 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<cmUVStreamReadHandle> { |
| pipe.init(chain.GetLoop(), 0); |
| uv_pipe_open(pipe, stream); |
| finished = false; |
| return cmUVStreamRead( |
| pipe, |
| [&processOutput, &out, &file, id, &haveData](std::vector<char> 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<int>(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<cmsys::RegularExpression>& 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); |
| } |
| } |