/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file LICENSE.rst or https://cmake.org/licensing for details. */ #include "cmCTest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cmsys/Base64.h" #include "cmsys/Directory.hxx" #include "cmsys/FStream.hxx" #include "cmsys/RegularExpression.hxx" #include "cmsys/SystemInformation.hxx" #ifndef _WIN32 # include // IWYU pragma: keep #endif #include "cm_parse_date.h" #include "cmCMakePresetsGraph.h" #include "cmCTestBuildAndTest.h" #include "cmCTestScriptHandler.h" #include "cmCTestTestHandler.h" #include "cmCTestTypes.h" #include "cmCommandLineArgument.h" #include "cmExecutionStatus.h" #include "cmGeneratedFileStream.h" #include "cmGlobalGenerator.h" #include "cmInstrumentation.h" #include "cmInstrumentationQuery.h" #include "cmJSONState.h" #include "cmList.h" #include "cmListFileCache.h" #include "cmMakefile.h" #include "cmProcessOutput.h" #include "cmState.h" #include "cmStateSnapshot.h" #include "cmStateTypes.h" #include "cmStdIoStream.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmUVHandlePtr.h" #include "cmUVProcessChain.h" #include "cmUVStream.h" #include "cmValue.h" #include "cmVersion.h" #include "cmVersionConfig.h" #include "cmWorkingDirectory.h" #include "cmXMLWriter.h" #include "cmake.h" #if defined(__BEOS__) || defined(__HAIKU__) # include /* disable_debugger() API. */ #endif struct cmCTest::Private { Private(cmCTest* ctest) : BuildAndTest(ctest) { } /** Representation of one part. */ struct PartInfo { void SetName(std::string const& name) { this->Name = name; } std::string const& GetName() const { return this->Name; } void Enable() { this->Enabled = true; } explicit operator bool() const { return this->Enabled; } std::vector SubmitFiles; private: bool Enabled = false; std::string Name; }; int RepeatCount = 1; // default to run each test once cmCTest::Repeat RepeatMode = cmCTest::Repeat::Never; std::string ConfigType; std::string ScheduleType; std::chrono::system_clock::time_point StopTime; bool StopOnFailure = false; bool TestProgressOutput = false; bool Verbose = false; bool ExtraVerbose = false; bool ProduceXML = false; bool LabelSummary = true; bool SubprojectSummary = true; bool UseHTTP10 = false; bool PrintLabels = false; bool Failover = false; cmJSONState parseState; bool FlushTestProgressLine = false; // these are helper classes cmCTestBuildAndTest BuildAndTest; bool ShowOnly = false; bool OutputAsJson = false; int OutputAsJsonVersion = 1; // TODO: The ctest configuration should be a hierarchy of // configuration option sources: command-line, script, ini file. // Then the ini file can get re-loaded whenever it changes without // affecting any higher-precedence settings. std::map CTestConfiguration; std::map CTestConfigurationOverwrites; PartInfo Parts[PartCount]; std::map PartMap; std::string CurrentTag; bool TomorrowTag = false; int TestModel = cmCTest::EXPERIMENTAL; std::string SpecificGroup; cmDuration TimeOut = cmDuration::zero(); cmDuration GlobalTimeout = cmDuration::zero(); std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now(); cmDuration TimeLimit = cmCTest::MaxDuration(); int MaxTestNameWidth = 30; cm::optional ParallelLevel = 1; bool ParallelLevelSetInCli = false; unsigned long TestLoad = 0; int CompatibilityMode; // information for the --build-and-test options std::string BinaryDir; std::string TestDir; std::string NotesFiles; bool InteractiveDebugMode = true; bool ShortDateFormat = true; bool CompressXMLFiles = false; bool CompressTestOutput = true; bool Debug = false; bool Quiet = false; std::string BuildID; std::vector InitialCommandLineArguments; int SubmitIndex = 0; std::unique_ptr OutputLogFile; cm::optional OutputLogFileLastTag; bool OutputTestOutputOnTestFailure = false; bool OutputColorCode = cmCTest::ColoredOutputSupportedByConsole(); std::map Definitions; cmCTest::NoTests NoTestsMode = cmCTest::NoTests::Legacy; bool NoTestsModeSetInCli = false; cmCTestTestOptions TestOptions; std::vector CommandLineHttpHeaders; std::unique_ptr Instrumentation; }; struct tm* cmCTest::GetNightlyTime(std::string const& str, bool tomorrowtag) { struct tm* lctime; time_t tctime = time(nullptr); lctime = gmtime(&tctime); cmCTestLog(this, OUTPUT, "Determine Nightly Start Time" << std::endl << " Specified time: " << str << std::endl); // Convert the nightly start time to seconds. The current date of // the local machine is assumed. Consequently, nightlySeconds // is the time at which the nightly dashboard was opened or // will be opened on the date of the current client machine. // As such, this time may be in the past or in the future. char buf[1024]; std::snprintf(buf, sizeof(buf), "%d%02d%02d %s", lctime->tm_year + 1900, lctime->tm_mon + 1, lctime->tm_mday, str.c_str()); time_t ntime = cm_parse_date(tctime, buf); cmCTestLog(this, DEBUG, " Get the nightly start time: " << ntime << std::endl); tctime = time(nullptr); cmCTestLog(this, DEBUG, " Get the current time: " << tctime << std::endl); int const dayLength = 24 * 60 * 60; cmCTestLog(this, DEBUG, "Seconds: " << tctime << std::endl); while (ntime > tctime) { // If nightlySeconds is in the past, this is the current // open dashboard, then return nightlySeconds. If // nightlySeconds is in the future, this is the next // dashboard to be opened, so subtract 24 hours to get the // time of the current open dashboard ntime -= dayLength; cmCTestLog(this, DEBUG, "Pick yesterday" << std::endl); cmCTestLog(this, DEBUG, " Future time, subtract day: " << ntime << std::endl); } while (tctime > (ntime + dayLength)) { ntime += dayLength; cmCTestLog(this, DEBUG, " Past time, add day: " << ntime << std::endl); } cmCTestLog(this, DEBUG, "nightlySeconds: " << ntime << std::endl); cmCTestLog(this, DEBUG, " Current time: " << tctime << " Nightly time: " << ntime << std::endl); if (tomorrowtag) { cmCTestLog(this, OUTPUT, " Use future tag, Add a day" << std::endl); ntime += dayLength; } lctime = gmtime(&ntime); return lctime; } bool cmCTest::GetTomorrowTag() const { return this->Impl->TomorrowTag; } std::string cmCTest::CleanString(std::string const& str, std::string::size_type spos) { spos = str.find_first_not_of(" \n\t\r\f\v", spos); std::string::size_type epos = str.find_last_not_of(" \n\t\r\f\v"); if (spos == std::string::npos) { return std::string(); } if (epos != std::string::npos) { epos = epos - spos + 1; } return str.substr(spos, epos); } std::string cmCTest::CurrentTime() { time_t currenttime = time(nullptr); struct tm* t = localtime(¤ttime); // return ::CleanString(ctime(¤ttime)); char current_time[1024]; if (this->Impl->ShortDateFormat) { strftime(current_time, 1000, "%b %d %H:%M %Z", t); } else { strftime(current_time, 1000, "%a %b %d %H:%M:%S %Z %Y", t); } cmCTestLog(this, DEBUG, " Current_Time: " << current_time << std::endl); return cmCTest::CleanString(current_time); } std::string cmCTest::GetCostDataFile() { std::string fname = this->GetCTestConfiguration("CostDataFile"); if (fname.empty()) { fname = this->GetBinaryDir() + "/Testing/Temporary/CTestCostData.txt"; } return fname; } std::string cmCTest::DecodeURL(std::string const& in) { std::string out; for (char const* c = in.c_str(); *c; ++c) { if (*c == '%' && isxdigit(*(c + 1)) && isxdigit(*(c + 2))) { char buf[3] = { *(c + 1), *(c + 2), 0 }; out.append(1, static_cast(strtoul(buf, nullptr, 16))); c += 2; } else { out.append(1, *c); } } return out; } cmCTest::cmCTest() : Impl(cm::make_unique(this)) { std::string envValue; if (cmSystemTools::GetEnv("CTEST_OUTPUT_ON_FAILURE", envValue)) { this->Impl->OutputTestOutputOnTestFailure = !cmIsOff(envValue); } envValue.clear(); if (cmSystemTools::GetEnv("CTEST_PROGRESS_OUTPUT", envValue)) { this->Impl->TestProgressOutput = !cmIsOff(envValue); } envValue.clear(); this->Impl->Parts[PartStart].SetName("Start"); this->Impl->Parts[PartUpdate].SetName("Update"); this->Impl->Parts[PartConfigure].SetName("Configure"); this->Impl->Parts[PartBuild].SetName("Build"); this->Impl->Parts[PartTest].SetName("Test"); this->Impl->Parts[PartCoverage].SetName("Coverage"); this->Impl->Parts[PartMemCheck].SetName("MemCheck"); this->Impl->Parts[PartSubmit].SetName("Submit"); this->Impl->Parts[PartNotes].SetName("Notes"); this->Impl->Parts[PartExtraFiles].SetName("ExtraFiles"); this->Impl->Parts[PartUpload].SetName("Upload"); this->Impl->Parts[PartDone].SetName("Done"); // Fill the part name-to-id map. for (Part p = PartStart; p != PartCount; p = static_cast(p + 1)) { this->Impl ->PartMap[cmSystemTools::LowerCase(this->Impl->Parts[p].GetName())] = p; } // Make sure we can capture the build tool output. cmSystemTools::EnableVSConsoleOutput(); } cmCTest::~cmCTest() = default; cm::optional cmCTest::GetParallelLevel() const { return this->Impl->ParallelLevel; } void cmCTest::SetParallelLevel(cm::optional level) { this->Impl->ParallelLevel = level; } unsigned long cmCTest::GetTestLoad() const { return this->Impl->TestLoad; } void cmCTest::SetTestLoad(unsigned long load) { this->Impl->TestLoad = load; } bool cmCTest::ShouldCompressTestOutput() { return this->Impl->CompressTestOutput; } cmCTest::Part cmCTest::GetPartFromName(std::string const& name) { // Look up by lower-case to make names case-insensitive. std::string lower_name = cmSystemTools::LowerCase(name); auto const i = this->Impl->PartMap.find(lower_name); if (i != this->Impl->PartMap.end()) { return i->second; } // The string does not name a valid part. return PartCount; } void cmCTest::Initialize(std::string const& binary_dir) { this->Impl->BuildID = ""; for (Part p = PartStart; p != PartCount; p = static_cast(p + 1)) { this->Impl->Parts[p].SubmitFiles.clear(); } if (!this->Impl->InteractiveDebugMode) { this->BlockTestErrorDiagnostics(); } else { cmSystemTools::PutEnv("CTEST_INTERACTIVE_DEBUG_MODE=1"); } this->Impl->BinaryDir = binary_dir; cmSystemTools::ConvertToUnixSlashes(this->Impl->BinaryDir); } bool cmCTest::CreateNewTag(bool quiet) { std::string const testingDir = this->Impl->BinaryDir + "/Testing"; std::string const tagfile = testingDir + "/TAG"; auto const result = cmSystemTools::MakeDirectory(testingDir); if (!result.IsSuccess()) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create directory \"" << testingDir << "\": " << result.GetString() << std::endl); return false; } cmCTestOptionalLog(this, DEBUG, "TestModel: " << this->GetTestGroupString() << std::endl, quiet); cmCTestOptionalLog( this, DEBUG, "TestModel: " << this->Impl->TestModel << std::endl, quiet); struct tm* lctime = [this]() -> tm* { if (this->Impl->TestModel == cmCTest::NIGHTLY) { return this->GetNightlyTime( this->GetCTestConfiguration("NightlyStartTime"), this->Impl->TomorrowTag); } time_t tctime = time(nullptr); if (this->Impl->TomorrowTag) { tctime += (24 * 60 * 60); } return gmtime(&tctime); }(); char datestring[100]; snprintf(datestring, sizeof(datestring), "%04d%02d%02d-%02d%02d", lctime->tm_year + 1900, lctime->tm_mon + 1, lctime->tm_mday, lctime->tm_hour, lctime->tm_min); this->Impl->CurrentTag = datestring; cmsys::ofstream ofs(tagfile.c_str()); ofs << this->Impl->CurrentTag << std::endl; ofs << this->GetTestGroupString() << std::endl; ofs << this->GetTestModelString() << std::endl; return true; } bool cmCTest::ReadExistingTag(bool quiet) { std::string const testingDir = this->Impl->BinaryDir + "/Testing"; std::string const tagfile = testingDir + "/TAG"; std::string tag; std::string group; std::string modelStr; int model = cmCTest::UNKNOWN; cmsys::ifstream tfin(tagfile.c_str()); if (tfin) { cmSystemTools::GetLineFromStream(tfin, tag); cmSystemTools::GetLineFromStream(tfin, group); if (cmSystemTools::GetLineFromStream(tfin, modelStr)) { model = GetTestModelFromString(modelStr); } tfin.close(); } if (tag.empty()) { if (!quiet) { cmCTestLog(this, ERROR_MESSAGE, "Cannot read existing TAG file in " << testingDir << std::endl); } return false; } if (this->Impl->TestModel == cmCTest::UNKNOWN) { if (model == cmCTest::UNKNOWN) { cmCTestLog(this, ERROR_MESSAGE, "TAG file does not contain model and " "no model specified in start command" << std::endl); return false; } this->SetTestModel(model); } if (model != this->Impl->TestModel && model != cmCTest::UNKNOWN && this->Impl->TestModel != cmCTest::UNKNOWN) { cmCTestOptionalLog(this, WARNING, "Model given in TAG does not match " "model given in ctest_start()" << std::endl, quiet); } if (!this->Impl->SpecificGroup.empty() && group != this->Impl->SpecificGroup) { cmCTestOptionalLog(this, WARNING, "Group given in TAG does not match " "group given in ctest_start()" << std::endl, quiet); } else { this->Impl->SpecificGroup = group; } cmCTestOptionalLog(this, OUTPUT, " Use existing tag: " << tag << " - " << this->GetTestGroupString() << std::endl, quiet); this->Impl->CurrentTag = tag; return true; } bool cmCTest::UpdateCTestConfiguration() { std::string fileName = this->Impl->BinaryDir + "/CTestConfiguration.ini"; if (!cmSystemTools::FileExists(fileName)) { fileName = this->Impl->BinaryDir + "/DartConfiguration.tcl"; } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "UpdateCTestConfiguration from :" << fileName << "\n"); if (!cmSystemTools::FileExists(fileName)) { // No need to exit if we are not producing XML if (this->Impl->ProduceXML) { cmCTestLog(this, WARNING, "Cannot find file: " << fileName << std::endl); return false; } } else { cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "Parse Config file:" << fileName << "\n"); // parse the dart test file cmsys::ifstream fin(fileName.c_str()); if (!fin) { return false; } char buffer[1024]; while (fin) { buffer[0] = 0; fin.getline(buffer, 1023); buffer[1023] = 0; std::string line = cmCTest::CleanString(buffer); if (line.empty()) { continue; } while (fin && (line.back() == '\\')) { line.resize(line.size() - 1); buffer[0] = 0; fin.getline(buffer, 1023); buffer[1023] = 0; line += cmCTest::CleanString(buffer); } if (line[0] == '#') { continue; } std::string::size_type cpos = line.find_first_of(':'); if (cpos == std::string::npos) { continue; } std::string key = line.substr(0, cpos); std::string value = cmCTest::CleanString(line, cpos + 1); this->Impl->CTestConfiguration[key] = value; } fin.close(); } if (!this->GetCTestConfiguration("BuildDirectory").empty()) { this->Impl->BinaryDir = this->GetCTestConfiguration("BuildDirectory"); if (this->Impl->TestDir.empty()) { cmSystemTools::SetLogicalWorkingDirectory(this->Impl->BinaryDir); } } this->Impl->TimeOut = std::chrono::seconds(atoi(this->GetCTestConfiguration("TimeOut").c_str())); std::string const& testLoad = this->GetCTestConfiguration("TestLoad"); if (!testLoad.empty()) { unsigned long load; if (cmStrToULong(testLoad, &load)) { this->SetTestLoad(load); } else { cmCTestLog(this, WARNING, "Invalid value for 'Test Load' : " << testLoad << std::endl); } } if (this->Impl->ProduceXML) { this->Impl->CompressXMLFiles = cmIsOn(this->GetCTestConfiguration("CompressSubmission")); } return true; } void cmCTest::BlockTestErrorDiagnostics() { cmSystemTools::PutEnv("DART_TEST_FROM_DART=1"); cmSystemTools::PutEnv("DASHBOARD_TEST_FROM_CTEST=" CMake_VERSION); #if defined(_WIN32) SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); #elif defined(__BEOS__) || defined(__HAIKU__) disable_debugger(1); #endif } void cmCTest::SetTestModel(int mode) { this->Impl->InteractiveDebugMode = false; this->Impl->TestModel = mode; } int cmCTest::GetTestModel() const { return this->Impl->TestModel; } bool cmCTest::SetTest(std::string const& ttype, bool report) { if (cmSystemTools::LowerCase(ttype) == "all") { for (Part p = PartStart; p != PartCount; p = static_cast(p + 1)) { this->Impl->Parts[p].Enable(); } return true; } Part p = this->GetPartFromName(ttype); if (p != PartCount) { this->Impl->Parts[p].Enable(); return true; } if (report) { cmCTestLog(this, ERROR_MESSAGE, "Don't know about test \"" << ttype << "\" yet..." << std::endl); } return false; } bool cmCTest::OpenOutputFile(std::string const& path, std::string const& name, cmGeneratedFileStream& stream, bool compress) { std::string testingDir = this->Impl->BinaryDir + "/Testing"; if (!path.empty()) { testingDir += "/" + path; } if (cmSystemTools::FileExists(testingDir)) { if (!cmSystemTools::FileIsDirectory(testingDir)) { cmCTestLog(this, ERROR_MESSAGE, "File " << testingDir << " is in the place of the testing directory" << std::endl); return false; } } else { if (!cmSystemTools::MakeDirectory(testingDir)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create directory " << testingDir << std::endl); return false; } } std::string filename = testingDir + "/" + name; stream.Open(filename); if (!stream) { cmCTestLog(this, ERROR_MESSAGE, "Problem opening file: " << filename << std::endl); return false; } if (compress) { if (this->Impl->CompressXMLFiles) { stream.SetCompression(true); } } return true; } bool cmCTest::AddIfExists(Part part, std::string const& file) { if (this->CTestFileExists(file)) { this->AddSubmitFile(part, file); } else { std::string name = cmStrCat(file, ".gz"); if (this->CTestFileExists(name)) { this->AddSubmitFile(part, file); } else { return false; } } return true; } bool cmCTest::CTestFileExists(std::string const& filename) { std::string testingDir = this->Impl->BinaryDir + "/Testing/" + this->Impl->CurrentTag + "/" + filename; return cmSystemTools::FileExists(testingDir); } int cmCTest::ProcessSteps() { this->Impl->ExtraVerbose = this->Impl->Verbose; this->Impl->Verbose = true; this->Impl->ProduceXML = true; // Minimal dashboard client script configuration. this->SetCTestConfiguration("BuildDirectory", this->Impl->BinaryDir); this->UpdateCTestConfiguration(); this->BlockTestErrorDiagnostics(); if (this->GetCTestConfiguration("TimeOut").empty()) { this->SetCTestConfiguration( "TimeOut", std::to_string(cmDurationTo(cmCTest::MaxDuration())), true); } int res = 0; cmCTestScriptHandler script(this); script.CreateCMake(); cmMakefile& mf = *script.GetMakefile(); this->ReadCustomConfigurationFileTree(this->Impl->BinaryDir, &mf); this->SetTimeLimit(mf.GetDefinition("CTEST_TIME_LIMIT")); this->SetCMakeVariables(mf); std::vector args{ cmListFileArgument("RETURN_VALUE"_s, cmListFileArgument::Unquoted, 0), cmListFileArgument("return_value"_s, cmListFileArgument::Unquoted, 0), }; if (this->Impl->Parts[PartStart]) { auto const func = cmListFileFunction( "ctest_start", 0, 0, { { this->GetTestModelString(), cmListFileArgument::Unquoted, 0 }, { "GROUP", cmListFileArgument::Unquoted, 0 }, { this->GetTestGroupString(), cmListFileArgument::Unquoted, 0 }, }); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status)) { return 12; } } else if (!this->ReadExistingTag(true) && !this->CreateNewTag(false)) { cmCTestLog(this, ERROR_MESSAGE, "Problem initializing the dashboard." << std::endl); return 12; } if (this->Impl->Parts[PartUpdate] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { auto const func = cmListFileFunction("ctest_update", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status)) { res |= cmCTest::UPDATE_ERRORS; } } if (this->Impl->TestModel == cmCTest::CONTINUOUS && mf.GetDefinition("return_value").IsOff()) { return 0; } if (this->Impl->Parts[PartConfigure] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { auto const func = cmListFileFunction("ctest_configure", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::CONFIGURE_ERRORS; } } if (this->Impl->Parts[PartBuild] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); this->SetCMakeVariables(mf); auto const func = cmListFileFunction("ctest_build", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::BUILD_ERRORS; } } if (this->Impl->Parts[PartTest] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); this->SetCMakeVariables(mf); auto const func = cmListFileFunction("ctest_test", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::TEST_ERRORS; } } if (this->Impl->Parts[PartCoverage] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); this->SetCMakeVariables(mf); auto const func = cmListFileFunction("ctest_coverage", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::COVERAGE_ERRORS; } } if (this->Impl->Parts[PartMemCheck] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); this->SetCMakeVariables(mf); auto const func = cmListFileFunction("ctest_memcheck", 0, 0, args); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::MEMORY_ERRORS; } } std::string notes_dir = this->Impl->BinaryDir + "/Testing/Notes"; if (cmSystemTools::FileIsDirectory(notes_dir)) { cmsys::Directory d; d.Load(notes_dir); unsigned long kk; for (kk = 0; kk < d.GetNumberOfFiles(); kk++) { char const* file = d.GetFile(kk); std::string fullname = notes_dir + "/" + file; if (cmSystemTools::FileExists(fullname, true)) { if (!this->Impl->NotesFiles.empty()) { this->Impl->NotesFiles += ";"; } this->Impl->NotesFiles += fullname; this->Impl->Parts[PartNotes].Enable(); } } } if (this->Impl->Parts[PartNotes]) { this->UpdateCTestConfiguration(); if (!this->Impl->NotesFiles.empty()) { this->GenerateNotesFile(script.GetCMake(), this->Impl->NotesFiles); } } if (this->Impl->Parts[PartSubmit]) { this->UpdateCTestConfiguration(); this->SetCMakeVariables(mf); std::string count = this->GetCTestConfiguration("CTestSubmitRetryCount"); std::string delay = this->GetCTestConfiguration("CTestSubmitRetryDelay"); auto const func = cmListFileFunction( "ctest_submit", 0, 0, { cmListFileArgument("RETRY_COUNT"_s, cmListFileArgument::Unquoted, 0), cmListFileArgument(count, cmListFileArgument::Quoted, 0), cmListFileArgument("RETRY_DELAY"_s, cmListFileArgument::Unquoted, 0), cmListFileArgument(delay, cmListFileArgument::Quoted, 0), cmListFileArgument("RETURN_VALUE"_s, cmListFileArgument::Unquoted, 0), cmListFileArgument("return_value"_s, cmListFileArgument::Unquoted, 0), }); auto status = cmExecutionStatus(mf); if (!mf.ExecuteCommand(func, status) || std::stoi(mf.GetDefinition("return_value")) < 0) { res |= cmCTest::SUBMIT_ERRORS; } } return res; } std::string cmCTest::GetTestModelString() const { switch (this->Impl->TestModel) { case cmCTest::NIGHTLY: return "Nightly"; case cmCTest::CONTINUOUS: return "Continuous"; } return "Experimental"; } std::string cmCTest::GetTestGroupString() const { if (!this->Impl->SpecificGroup.empty()) { return this->Impl->SpecificGroup; } return this->GetTestModelString(); } int cmCTest::GetTestModelFromString(std::string const& str) { if (str.empty()) { return cmCTest::EXPERIMENTAL; } std::string rstr = cmSystemTools::LowerCase(str); if (cmHasLiteralPrefix(rstr, "cont")) { return cmCTest::CONTINUOUS; } if (cmHasLiteralPrefix(rstr, "nigh")) { return cmCTest::NIGHTLY; } return cmCTest::EXPERIMENTAL; } bool cmCTest::RunMakeCommand(std::string const& command, std::string& output, int* retVal, char const* dir, cmDuration timeout, std::ostream& ofs, Encoding encoding) { // First generate the command and arguments std::vector args = cmSystemTools::ParseArguments(command); if (args.empty()) { return false; } output.clear(); cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "Run command:"); for (auto const& arg : args) { cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, " \"" << arg << "\""); } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, std::endl); // Now create process object cmUVProcessChainBuilder builder; builder.AddCommand(args).SetMergedBuiltinStreams(); if (dir) { builder.SetWorkingDirectory(dir); } auto chain = builder.Start(); cm::uv_pipe_ptr outputStream; outputStream.init(chain.GetLoop(), 0); uv_pipe_open(outputStream, chain.OutputStream()); // Initialize tick's std::string::size_type tick = 0; std::string::size_type tick_len = 1024; std::string::size_type tick_line_len = 50; cmProcessOutput processOutput(encoding); cmCTestLog(this, HANDLER_PROGRESS_OUTPUT, " Each . represents " << tick_len << " bytes of output\n" " " << std::flush); auto outputHandle = cmUVStreamRead( outputStream, [this, &processOutput, &output, &tick, &tick_len, &tick_line_len, &ofs](std::vector data) { std::string strdata; processOutput.DecodeText(data.data(), data.size(), strdata); for (char& cc : strdata) { if (cc == 0) { cc = '\n'; } } output.append(strdata); while (output.size() > (tick * tick_len)) { tick++; cmCTestLog(this, HANDLER_PROGRESS_OUTPUT, "." << std::flush); if (tick % tick_line_len == 0 && tick > 0) { cmCTestLog(this, HANDLER_PROGRESS_OUTPUT, " Size: " << int((double(output.size()) / 1024.0) + 1) << "K\n " << std::flush); } } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, strdata); if (ofs) { ofs << strdata; } }, [this, &processOutput, &output, &ofs]() { std::string strdata; processOutput.DecodeText(std::string(), strdata); if (!strdata.empty()) { output.append(strdata); cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, strdata); if (ofs) { ofs << strdata; } } }); bool finished = chain.Wait(static_cast(timeout.count() * 1000.0)); cmCTestLog(this, HANDLER_PROGRESS_OUTPUT, " Size of output: " << int(double(output.size()) / 1024.0) << "K" << std::endl); if (finished) { auto const& status = chain.GetStatus(0); auto exception = status.GetException(); switch (exception.first) { case cmUVProcessChain::ExceptionCode::None: *retVal = static_cast(status.ExitStatus); cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "Command exited with the value: " << *retVal << std::endl); break; case cmUVProcessChain::ExceptionCode::Spawn: output += "\n*** ERROR executing: "; output += exception.second; output += "\n***The build process failed."; cmCTestLog(this, ERROR_MESSAGE, "There was an error: " << exception.second << std::endl); break; default: *retVal = static_cast(exception.first); cmCTestLog(this, WARNING, "There was an exception: " << *retVal << std::endl); break; } } else { cmCTestLog(this, WARNING, "There was a timeout" << std::endl); } return true; } std::string cmCTest::SafeBuildIdField(std::string const& value) { return value.empty() ? "(empty)" : value; } void cmCTest::StartXML(cmXMLWriter& xml, cmake* cm, bool append) { if (this->Impl->CurrentTag.empty()) { cmCTestLog(this, ERROR_MESSAGE, "Current Tag empty, this may mean" " NightlStartTime was not set correctly." << std::endl); cmSystemTools::SetFatalErrorOccurred(); } // find out about the system cmsys::SystemInformation info; info.RunCPUCheck(); info.RunOSCheck(); info.RunMemoryCheck(); std::string buildname = cmCTest::SafeBuildIdField(this->GetCTestConfiguration("BuildName")); std::string stamp = cmCTest::SafeBuildIdField(this->Impl->CurrentTag + "-" + this->GetTestGroupString()); std::string site = cmCTest::SafeBuildIdField(this->GetCTestConfiguration("Site")); xml.StartDocument(); xml.StartElement("Site"); xml.Attribute("BuildName", buildname); xml.BreakAttributes(); xml.Attribute("BuildStamp", stamp); xml.Attribute("Name", site); xml.Attribute("Generator", std::string("ctest-") + cmVersion::GetCMakeVersion()); if (append) { xml.Attribute("Append", "true"); } xml.Attribute("CompilerName", this->GetCTestConfiguration("Compiler")); xml.Attribute("CompilerVersion", this->GetCTestConfiguration("CompilerVersion")); xml.Attribute("OSName", info.GetOSName()); xml.Attribute("Hostname", info.GetHostname()); xml.Attribute("OSRelease", info.GetOSRelease()); xml.Attribute("OSVersion", info.GetOSVersion()); xml.Attribute("OSPlatform", info.GetOSPlatform()); xml.Attribute("Is64Bits", info.Is64Bits()); xml.Attribute("VendorString", info.GetVendorString()); xml.Attribute("VendorID", info.GetVendorID()); xml.Attribute("FamilyID", info.GetFamilyID()); xml.Attribute("ModelID", info.GetModelID()); xml.Attribute("ModelName", info.GetModelName()); xml.Attribute("ProcessorCacheSize", info.GetProcessorCacheSize()); xml.Attribute("NumberOfLogicalCPU", info.GetNumberOfLogicalCPU()); xml.Attribute("NumberOfPhysicalCPU", info.GetNumberOfPhysicalCPU()); xml.Attribute("TotalVirtualMemory", info.GetTotalVirtualMemory()); xml.Attribute("TotalPhysicalMemory", info.GetTotalPhysicalMemory()); xml.Attribute("LogicalProcessorsPerPhysical", info.GetLogicalProcessorsPerPhysical()); xml.Attribute("ProcessorClockFrequency", info.GetProcessorClockFrequency()); std::string changeId = this->GetCTestConfiguration("ChangeId"); if (!changeId.empty()) { xml.Attribute("ChangeId", changeId); } this->AddSiteProperties(xml, cm); } void cmCTest::AddSiteProperties(cmXMLWriter& xml, cmake* cm) { // This code should go when cdash is changed to use labels only cmValue subproject = cm->GetState()->GetGlobalProperty("SubProject"); if (subproject) { xml.StartElement("Subproject"); xml.Attribute("name", *subproject); cmValue labels = cm->GetState()->GetGlobalProperty("SubProjectLabels"); if (labels) { xml.StartElement("Labels"); cmList args{ *labels }; for (std::string const& i : args) { xml.Element("Label", i); } xml.EndElement(); } xml.EndElement(); } // This code should stay when cdash only does label based sub-projects cmValue label = cm->GetState()->GetGlobalProperty("Label"); if (label) { xml.StartElement("Labels"); xml.Element("Label", *label); xml.EndElement(); } } void cmCTest::GenerateSubprojectsOutput(cmXMLWriter& xml) { for (std::string const& subproj : this->GetLabelsForSubprojects()) { xml.StartElement("Subproject"); xml.Attribute("name", subproj); xml.Element("Label", subproj); xml.EndElement(); // Subproject } } std::vector cmCTest::GetLabelsForSubprojects() { std::string labelsForSubprojects = this->GetCTestConfiguration("LabelsForSubprojects"); cmList subprojects{ labelsForSubprojects }; // sort the array std::sort(subprojects.begin(), subprojects.end()); // remove duplicates auto new_end = std::unique(subprojects.begin(), subprojects.end()); subprojects.erase(new_end, subprojects.end()); return std::move(subprojects.data()); } void cmCTest::EndXML(cmXMLWriter& xml) { xml.EndElement(); // Site xml.EndDocument(); } int cmCTest::GenerateCTestNotesOutput(cmXMLWriter& xml, cmake* cm, std::vector const& files) { std::string buildname = cmCTest::SafeBuildIdField(this->GetCTestConfiguration("BuildName")); xml.StartDocument(); xml.ProcessingInstruction("xml-stylesheet", "type=\"text/xsl\" " "href=\"Dart/Source/Server/XSL/Build.xsl " " \""); xml.StartElement("Site"); xml.Attribute("BuildName", buildname); xml.Attribute("BuildStamp", this->Impl->CurrentTag + "-" + this->GetTestGroupString()); xml.Attribute("Name", this->GetCTestConfiguration("Site")); xml.Attribute("Generator", std::string("ctest-") + cmVersion::GetCMakeVersion()); this->AddSiteProperties(xml, cm); xml.StartElement("Notes"); for (std::string const& file : files) { cmCTestLog(this, OUTPUT, "\tAdd file: " << file << std::endl); std::string note_time = this->CurrentTime(); xml.StartElement("Note"); xml.Attribute("Name", file); xml.Element("Time", std::chrono::system_clock::now()); xml.Element("DateTime", note_time); xml.StartElement("Text"); cmsys::ifstream ifs(file.c_str()); if (ifs) { std::string line; while (cmSystemTools::GetLineFromStream(ifs, line)) { xml.Content(line); xml.Content("\n"); } ifs.close(); } else { xml.Content("Problem reading file: " + file + "\n"); cmCTestLog(this, ERROR_MESSAGE, "Problem reading file: " << file << " while creating notes" << std::endl); } xml.EndElement(); // Text xml.EndElement(); // Note } xml.EndElement(); // Notes xml.EndElement(); // Site xml.EndDocument(); return 1; } int cmCTest::GenerateNotesFile(cmake* cm, std::vector const& files) { cmGeneratedFileStream ofs; if (!this->OpenOutputFile(this->Impl->CurrentTag, "Notes.xml", ofs)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot open notes file" << std::endl); return 1; } cmXMLWriter xml(ofs); this->GenerateCTestNotesOutput(xml, cm, files); return 0; } int cmCTest::GenerateNotesFile(cmake* cm, std::string const& cfiles) { if (cfiles.empty()) { return 1; } cmCTestLog(this, OUTPUT, "Create notes file" << std::endl); std::vector const files = cmSystemTools::SplitString(cfiles, ';'); if (files.empty()) { return 1; } return this->GenerateNotesFile(cm, files); } int cmCTest::GenerateDoneFile() { cmGeneratedFileStream ofs; if (!this->OpenOutputFile(this->Impl->CurrentTag, "Done.xml", ofs)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot open done file" << std::endl); return 1; } cmXMLWriter xml(ofs); xml.StartDocument(); xml.StartElement("Done"); xml.Element("buildId", this->Impl->BuildID); xml.Element("time", std::chrono::system_clock::now()); xml.EndElement(); // Done xml.EndDocument(); return 0; } std::string cmCTest::Base64GzipEncodeFile(std::string const& file) { // Temporarily change to the file's directory so the tar gets created // with a flat directory structure. cmWorkingDirectory workdir(cmSystemTools::GetParentDirectory(file)); if (workdir.Failed()) { cmCTestLog(this, ERROR_MESSAGE, workdir.GetError() << std::endl); return ""; } std::string tarFile = file + "_temp.tar.gz"; std::vector files; files.push_back(file); if (!cmSystemTools::CreateTar(tarFile, files, {}, cmSystemTools::TarCompressGZip, false)) { cmCTestLog(this, ERROR_MESSAGE, "Error creating tar while " "encoding file: " << file << std::endl); return ""; } std::string base64 = this->Base64EncodeFile(tarFile); cmSystemTools::RemoveFile(tarFile); return base64; } std::string cmCTest::Base64EncodeFile(std::string const& file) { size_t const len = cmSystemTools::FileLength(file); cmsys::ifstream ifs(file.c_str(), std::ios::in #ifdef _WIN32 | std::ios::binary #endif ); std::vector file_buffer(len + 1); ifs.read(file_buffer.data(), len); ifs.close(); std::vector encoded_buffer((len * 3) / 2 + 5); size_t const rlen = cmsysBase64_Encode( reinterpret_cast(file_buffer.data()), len, reinterpret_cast(encoded_buffer.data()), 1); return std::string(encoded_buffer.data(), rlen); } bool cmCTest::SubmitExtraFiles(std::vector const& files) { for (std::string const& file : files) { if (!cmSystemTools::FileExists(file)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot find extra file: " << file << " to submit." << std::endl); return false; } this->AddSubmitFile(PartExtraFiles, file); } return true; } bool cmCTest::SubmitExtraFiles(std::string const& cfiles) { if (cfiles.empty()) { return true; } cmCTestLog(this, OUTPUT, "Submit extra files" << std::endl); std::vector const files = cmSystemTools::SplitString(cfiles, ';'); if (files.empty()) { return true; } return this->SubmitExtraFiles(files); } // for a -D argument convert the next argument into // the proper list of dashboard steps via SetTest bool cmCTest::AddTestsForDashboardType(std::string const& targ) { if (targ == "Experimental") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Start"); this->SetTest("Configure"); this->SetTest("Build"); this->SetTest("Test"); this->SetTest("Coverage"); this->SetTest("Submit"); } else if (targ == "ExperimentalStart") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Start"); } else if (targ == "ExperimentalUpdate") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Update"); } else if (targ == "ExperimentalConfigure") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Configure"); } else if (targ == "ExperimentalBuild") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Build"); } else if (targ == "ExperimentalTest") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Test"); } else if (targ == "ExperimentalMemCheck" || targ == "ExperimentalPurify") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("MemCheck"); } else if (targ == "ExperimentalCoverage") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Coverage"); } else if (targ == "ExperimentalSubmit") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Submit"); } else if (targ == "Continuous") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Start"); this->SetTest("Update"); this->SetTest("Configure"); this->SetTest("Build"); this->SetTest("Test"); this->SetTest("Coverage"); this->SetTest("Submit"); } else if (targ == "ContinuousStart") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Start"); } else if (targ == "ContinuousUpdate") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Update"); } else if (targ == "ContinuousConfigure") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Configure"); } else if (targ == "ContinuousBuild") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Build"); } else if (targ == "ContinuousTest") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Test"); } else if (targ == "ContinuousMemCheck" || targ == "ContinuousPurify") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("MemCheck"); } else if (targ == "ContinuousCoverage") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Coverage"); } else if (targ == "ContinuousSubmit") { this->SetTestModel(cmCTest::CONTINUOUS); this->SetTest("Submit"); } else if (targ == "Nightly") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Start"); this->SetTest("Update"); this->SetTest("Configure"); this->SetTest("Build"); this->SetTest("Test"); this->SetTest("Coverage"); this->SetTest("Submit"); } else if (targ == "NightlyStart") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Start"); } else if (targ == "NightlyUpdate") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Update"); } else if (targ == "NightlyConfigure") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Configure"); } else if (targ == "NightlyBuild") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Build"); } else if (targ == "NightlyTest") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Test"); } else if (targ == "NightlyMemCheck" || targ == "NightlyPurify") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("MemCheck"); } else if (targ == "NightlyCoverage") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Coverage"); } else if (targ == "NightlySubmit") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Submit"); } else if (targ == "MemoryCheck") { this->SetTestModel(cmCTest::EXPERIMENTAL); this->SetTest("Start"); this->SetTest("Configure"); this->SetTest("Build"); this->SetTest("MemCheck"); this->SetTest("Coverage"); this->SetTest("Submit"); } else if (targ == "NightlyMemoryCheck") { this->SetTestModel(cmCTest::NIGHTLY); this->SetTest("Start"); this->SetTest("Update"); this->SetTest("Configure"); this->SetTest("Build"); this->SetTest("MemCheck"); this->SetTest("Coverage"); this->SetTest("Submit"); } else { return false; } return true; } void cmCTest::ErrorMessageUnknownDashDValue(std::string const& val) { cmCTestLog(this, ERROR_MESSAGE, "CTest -D called with incorrect option: " << val << '\n'); cmCTestLog(this, ERROR_MESSAGE, "Available options are:\n" " ctest -D Continuous\n" " ctest -D Continuous(Start|Update|Configure|Build)\n" " ctest -D Continuous(Test|Coverage|MemCheck|Submit)\n" " ctest -D Experimental\n" " ctest -D Experimental(Start|Update|Configure|Build)\n" " ctest -D Experimental(Test|Coverage|MemCheck|Submit)\n" " ctest -D Nightly\n" " ctest -D Nightly(Start|Update|Configure|Build)\n" " ctest -D Nightly(Test|Coverage|MemCheck|Submit)\n" " ctest -D NightlyMemoryCheck\n"); } bool cmCTest::CheckArgument(std::string const& arg, cm::string_view varg1, char const* varg2) { return (arg == varg1) || (varg2 && arg == varg2); } bool cmCTest::ProgressOutputSupportedByConsole() { return cm::StdIo::Out().Kind() == cm::StdIo::TermKind::VT100; } bool cmCTest::ColoredOutputSupportedByConsole() { std::string clicolor_force; if (cmSystemTools::GetEnv("CLICOLOR_FORCE", clicolor_force) && !clicolor_force.empty() && clicolor_force != "0") { return true; } std::string clicolor; if (cmSystemTools::GetEnv("CLICOLOR", clicolor) && clicolor == "0") { return false; } return cm::StdIo::Out().Kind() == cm::StdIo::TermKind::VT100; } bool cmCTest::AddVariableDefinition(std::string const& arg) { std::string name; std::string value; cmStateEnums::CacheEntryType type = cmStateEnums::UNINITIALIZED; if (cmake::ParseCacheEntry(arg, name, value, type)) { this->Impl->Definitions[name] = value; return true; } return false; } bool cmCTest::SetArgsFromPreset(std::string const& presetName, bool listPresets) { auto const workingDirectory = cmSystemTools::GetLogicalWorkingDirectory(); cmCMakePresetsGraph settingsFile; auto result = settingsFile.ReadProjectPresets(workingDirectory); if (result != true) { cmSystemTools::Error(cmStrCat("Could not read presets from ", workingDirectory, ":\n", settingsFile.parseState.GetErrorMessage())); return false; } if (listPresets) { settingsFile.PrintTestPresetList(); return true; } auto presetPair = settingsFile.TestPresets.find(presetName); if (presetPair == settingsFile.TestPresets.end()) { cmSystemTools::Error(cmStrCat("No such test preset in ", workingDirectory, ": \"", presetName, '"')); settingsFile.PrintTestPresetList(); return false; } if (presetPair->second.Unexpanded.Hidden) { cmSystemTools::Error(cmStrCat("Cannot use hidden test preset in ", workingDirectory, ": \"", presetName, '"')); settingsFile.PrintTestPresetList(); return false; } auto const& expandedPreset = presetPair->second.Expanded; if (!expandedPreset) { cmSystemTools::Error(cmStrCat("Could not evaluate test preset \"", presetName, "\": Invalid macro expansion")); settingsFile.PrintTestPresetList(); return false; } if (!expandedPreset->ConditionResult) { cmSystemTools::Error(cmStrCat("Cannot use disabled test preset in ", workingDirectory, ": \"", presetName, '"')); settingsFile.PrintTestPresetList(); return false; } auto configurePresetPair = settingsFile.ConfigurePresets.find(expandedPreset->ConfigurePreset); if (configurePresetPair == settingsFile.ConfigurePresets.end()) { cmSystemTools::Error(cmStrCat("No such configure preset in ", workingDirectory, ": \"", expandedPreset->ConfigurePreset, '"')); settingsFile.PrintConfigurePresetList(); return false; } if (configurePresetPair->second.Unexpanded.Hidden) { cmSystemTools::Error(cmStrCat("Cannot use hidden configure preset in ", workingDirectory, ": \"", expandedPreset->ConfigurePreset, '"')); settingsFile.PrintConfigurePresetList(); return false; } auto const& expandedConfigurePreset = configurePresetPair->second.Expanded; if (!expandedConfigurePreset) { cmSystemTools::Error(cmStrCat("Could not evaluate configure preset \"", expandedPreset->ConfigurePreset, "\": Invalid macro expansion")); return false; } auto presetEnvironment = expandedPreset->Environment; for (auto const& var : presetEnvironment) { if (var.second) { cmSystemTools::PutEnv(cmStrCat(var.first, '=', *var.second)); } } if (!expandedPreset->Configuration.empty()) { this->SetConfigType(expandedPreset->Configuration); } // Set build directory to value specified by the configure preset. this->AddCTestConfigurationOverwrite( cmStrCat("BuildDirectory=", expandedConfigurePreset->BinaryDir)); for (auto const& kvp : expandedPreset->OverwriteConfigurationFile) { this->AddCTestConfigurationOverwrite(kvp); } if (expandedPreset->Output) { this->Impl->TestProgressOutput = expandedPreset->Output->ShortProgress.value_or(false); if (expandedPreset->Output->Verbosity) { auto const& verbosity = *expandedPreset->Output->Verbosity; switch (verbosity) { case cmCMakePresetsGraph::TestPreset::OutputOptions::VerbosityEnum:: Extra: this->Impl->ExtraVerbose = true; CM_FALLTHROUGH; case cmCMakePresetsGraph::TestPreset::OutputOptions::VerbosityEnum:: Verbose: this->Impl->Verbose = true; break; case cmCMakePresetsGraph::TestPreset::OutputOptions::VerbosityEnum:: Default: default: // leave default settings break; } } this->Impl->Debug = expandedPreset->Output->Debug.value_or(false); this->Impl->OutputTestOutputOnTestFailure = expandedPreset->Output->OutputOnFailure.value_or(false); this->Impl->Quiet = expandedPreset->Output->Quiet.value_or(false); if (!expandedPreset->Output->OutputLogFile.empty()) { this->SetOutputLogFileName(expandedPreset->Output->OutputLogFile); } if (!expandedPreset->Output->OutputJUnitFile.empty()) { this->SetOutputJUnitFileName(expandedPreset->Output->OutputJUnitFile); } this->Impl->LabelSummary = expandedPreset->Output->LabelSummary.value_or(true); this->Impl->SubprojectSummary = expandedPreset->Output->SubprojectSummary.value_or(true); if (expandedPreset->Output->MaxPassedTestOutputSize) { this->Impl->TestOptions.OutputSizePassed = *expandedPreset->Output->MaxPassedTestOutputSize; } if (expandedPreset->Output->MaxFailedTestOutputSize) { this->Impl->TestOptions.OutputSizeFailed = *expandedPreset->Output->MaxFailedTestOutputSize; } if (expandedPreset->Output->TestOutputTruncation) { this->Impl->TestOptions.OutputTruncation = *expandedPreset->Output->TestOutputTruncation; } if (expandedPreset->Output->MaxTestNameWidth) { this->Impl->MaxTestNameWidth = *expandedPreset->Output->MaxTestNameWidth; } } if (expandedPreset->Filter) { if (expandedPreset->Filter->Include) { this->Impl->TestOptions.IncludeRegularExpression = expandedPreset->Filter->Include->Name; if (!expandedPreset->Filter->Include->Label.empty()) { this->Impl->TestOptions.LabelRegularExpression.push_back( expandedPreset->Filter->Include->Label); } if (expandedPreset->Filter->Include->Index) { if (expandedPreset->Filter->Include->Index->IndexFile.empty()) { auto const& start = expandedPreset->Filter->Include->Index->Start; auto const& end = expandedPreset->Filter->Include->Index->End; auto const& stride = expandedPreset->Filter->Include->Index->Stride; std::string indexOptions; indexOptions += (start ? std::to_string(*start) : "") + ","; indexOptions += (end ? std::to_string(*end) : "") + ","; indexOptions += (stride ? std::to_string(*stride) : "") + ","; indexOptions += cmJoin(expandedPreset->Filter->Include->Index->SpecificTests, ","); this->Impl->TestOptions.TestsToRunInformation = indexOptions; } else { this->Impl->TestOptions.TestsToRunInformation = expandedPreset->Filter->Include->Index->IndexFile; } } this->Impl->TestOptions.UseUnion = expandedPreset->Filter->Include->UseUnion.value_or(false); } if (expandedPreset->Filter->Exclude) { this->Impl->TestOptions.ExcludeRegularExpression = expandedPreset->Filter->Exclude->Name; if (!expandedPreset->Filter->Exclude->Label.empty()) { this->Impl->TestOptions.ExcludeLabelRegularExpression.push_back( expandedPreset->Filter->Exclude->Label); } if (expandedPreset->Filter->Exclude->Fixtures) { this->Impl->TestOptions.ExcludeFixtureRegularExpression = expandedPreset->Filter->Exclude->Fixtures->Any; this->Impl->TestOptions.ExcludeFixtureSetupRegularExpression = expandedPreset->Filter->Exclude->Fixtures->Setup; this->Impl->TestOptions.ExcludeFixtureCleanupRegularExpression = expandedPreset->Filter->Exclude->Fixtures->Cleanup; } } } if (expandedPreset->Execution) { this->Impl->StopOnFailure = expandedPreset->Execution->StopOnFailure.value_or(false); this->Impl->Failover = expandedPreset->Execution->EnableFailover.value_or(false); if (expandedPreset->Execution->Jobs) { auto jobs = *expandedPreset->Execution->Jobs; this->SetParallelLevel(jobs); this->Impl->ParallelLevelSetInCli = true; } this->Impl->TestOptions.ResourceSpecFile = expandedPreset->Execution->ResourceSpecFile; if (expandedPreset->Execution->TestLoad) { auto testLoad = *expandedPreset->Execution->TestLoad; this->SetTestLoad(testLoad); } if (expandedPreset->Execution->ShowOnly) { this->Impl->ShowOnly = true; switch (*expandedPreset->Execution->ShowOnly) { case cmCMakePresetsGraph::TestPreset::ExecutionOptions::ShowOnlyEnum:: JsonV1: this->Impl->Quiet = true; this->Impl->OutputAsJson = true; this->Impl->OutputAsJsonVersion = 1; break; case cmCMakePresetsGraph::TestPreset::ExecutionOptions::ShowOnlyEnum:: Human: // intentional fallthrough (human is the default) default: break; } } if (expandedPreset->Execution->Repeat) { this->Impl->RepeatCount = expandedPreset->Execution->Repeat->Count; switch (expandedPreset->Execution->Repeat->Mode) { case cmCMakePresetsGraph::TestPreset::ExecutionOptions::RepeatOptions:: ModeEnum::UntilFail: this->Impl->RepeatMode = cmCTest::Repeat::UntilFail; break; case cmCMakePresetsGraph::TestPreset::ExecutionOptions::RepeatOptions:: ModeEnum::UntilPass: this->Impl->RepeatMode = cmCTest::Repeat::UntilPass; break; case cmCMakePresetsGraph::TestPreset::ExecutionOptions::RepeatOptions:: ModeEnum::AfterTimeout: this->Impl->RepeatMode = cmCTest::Repeat::AfterTimeout; break; default: // should never default since mode is required return false; } } if (expandedPreset->Execution->InteractiveDebugging) { this->Impl->InteractiveDebugMode = *expandedPreset->Execution->InteractiveDebugging; } if (expandedPreset->Execution->ScheduleRandom.value_or(false)) { this->Impl->ScheduleType = "Random"; } if (expandedPreset->Execution->Timeout) { this->Impl->GlobalTimeout = cmDuration(*expandedPreset->Execution->Timeout); } if (expandedPreset->Execution->NoTestsAction) { switch (*expandedPreset->Execution->NoTestsAction) { case cmCMakePresetsGraph::TestPreset::ExecutionOptions:: NoTestsActionEnum::Error: this->Impl->NoTestsMode = cmCTest::NoTests::Error; break; case cmCMakePresetsGraph::TestPreset::ExecutionOptions:: NoTestsActionEnum::Ignore: this->Impl->NoTestsMode = cmCTest::NoTests::Ignore; break; case cmCMakePresetsGraph::TestPreset::ExecutionOptions:: NoTestsActionEnum::Default: break; default: // should never default return false; } } } return true; } // the main entry point of ctest, called from main int cmCTest::Run(std::vector const& args) { char const* ctestExec = "ctest"; bool cmakeAndTest = false; bool processSteps = false; bool SRArgumentSpecified = false; std::vector> runScripts; // copy the command line cm::append(this->Impl->InitialCommandLineArguments, args); // check if a test preset was specified bool listPresets = find(args.begin(), args.end(), "--list-presets") != args.end(); auto it = std::find_if(args.begin(), args.end(), [](std::string const& arg) -> bool { return arg == "--preset" || cmHasLiteralPrefix(arg, "--preset="); }); if (listPresets || it != args.end()) { std::string errormsg; bool success; if (listPresets) { // If listing presets we don't need a presetName success = this->SetArgsFromPreset("", listPresets); } else { if (cmHasLiteralPrefix(*it, "--preset=")) { auto const& presetName = it->substr(9); success = this->SetArgsFromPreset(presetName, listPresets); } else if (++it != args.end()) { auto const& presetName = *it; success = this->SetArgsFromPreset(presetName, listPresets); } else { cmSystemTools::Error("'--preset' requires an argument"); success = false; } } if (listPresets) { return success ? 0 : 1; } if (!success) { return 1; } } auto const dashD = [this, &processSteps](std::string const& targ) -> bool { // AddTestsForDashboard parses the dashboard type and converts it // into the separate stages if (this->AddTestsForDashboardType(targ)) { processSteps = true; return true; } if (this->AddVariableDefinition(targ)) { return true; } this->ErrorMessageUnknownDashDValue(targ); return false; }; auto const dashT = [this, &processSteps, ctestExec](std::string const& action) -> bool { if (!this->SetTest(action, false)) { cmCTestLog(this, ERROR_MESSAGE, "CTest -T called with incorrect option: " << action << '\n'); /* clang-format off */ cmCTestLog(this, ERROR_MESSAGE, "Available options are:\n" " " << ctestExec << " -T all\n" " " << ctestExec << " -T start\n" " " << ctestExec << " -T update\n" " " << ctestExec << " -T configure\n" " " << ctestExec << " -T build\n" " " << ctestExec << " -T test\n" " " << ctestExec << " -T coverage\n" " " << ctestExec << " -T memcheck\n" " " << ctestExec << " -T notes\n" " " << ctestExec << " -T submit\n"); /* clang-format on */ return false; } processSteps = true; return true; }; auto const dashM = [this, &processSteps, ctestExec](std::string const& model) -> bool { if (cmSystemTools::LowerCase(model) == "nightly"_s) { this->SetTestModel(cmCTest::NIGHTLY); } else if (cmSystemTools::LowerCase(model) == "continuous"_s) { this->SetTestModel(cmCTest::CONTINUOUS); } else if (cmSystemTools::LowerCase(model) == "experimental"_s) { this->SetTestModel(cmCTest::EXPERIMENTAL); } else { cmCTestLog(this, ERROR_MESSAGE, "CTest -M called with incorrect option: " << model << '\n'); /* clang-format off */ cmCTestLog(this, ERROR_MESSAGE, "Available options are:\n" " " << ctestExec << " -M Continuous\n" " " << ctestExec << " -M Experimental\n" " " << ctestExec << " -M Nightly\n"); /* clang-format on */ return false; } processSteps = true; return true; }; auto const dashSP = [&runScripts, &SRArgumentSpecified](std::string const& script) -> bool { // -SR is an internal argument, -SP should be ignored when it is passed if (!SRArgumentSpecified) { runScripts.emplace_back(cmSystemTools::ToNormalizedPathOnDisk(script), false); } return true; }; auto const dashSR = [&runScripts, &SRArgumentSpecified](std::string const& script) -> bool { // -SR should be processed only once if (!SRArgumentSpecified) { SRArgumentSpecified = true; runScripts.emplace_back(cmSystemTools::ToNormalizedPathOnDisk(script), true); } return true; }; auto const dash_S = [&runScripts, &SRArgumentSpecified](std::string const& script) -> bool { // -SR is an internal argument, -S should be ignored when it is passed if (!SRArgumentSpecified) { runScripts.emplace_back(cmSystemTools::ToNormalizedPathOnDisk(script), true); } return true; }; auto const dashJ = [this](cm::string_view arg, std::string const& j) -> bool { cm::optional parallelLevel; // No value or an empty value tells ctest to choose a default. if (!j.empty()) { // A non-empty value must be a non-negative integer. unsigned long plevel = 0; if (!cmStrToULong(j, &plevel)) { cmSystemTools::Error( cmStrCat('\'', arg, "' given invalid value '", j, '\'')); return false; } parallelLevel = plevel; } this->SetParallelLevel(parallelLevel); this->Impl->ParallelLevelSetInCli = true; return true; }; auto const dashC = [this](std::string const& config) -> bool { this->SetConfigType(config); return true; }; auto const dashGroup = [this](std::string const& group) -> bool { this->Impl->SpecificGroup = group; return true; }; auto const dashQ = [this](std::string const&) -> bool { this->Impl->Quiet = true; return true; }; auto const dashV = [this](std::string const&) -> bool { this->Impl->Verbose = true; return true; }; auto const dashVV = [this](std::string const&) -> bool { this->Impl->ExtraVerbose = true; this->Impl->Verbose = true; return true; }; auto const dashO = [this](std::string const& log) -> bool { this->SetOutputLogFileName(log); return true; }; auto const dashW = [this](std::string const& width) -> bool { this->Impl->MaxTestNameWidth = atoi(width.c_str()); return true; }; auto const dashA = [this, &processSteps](std::string const& notes) -> bool { processSteps = true; this->SetTest("Notes"); this->SetNotesFiles(notes); return true; }; auto const dashI = [this](std::string const& tests) -> bool { this->Impl->TestOptions.TestsToRunInformation = tests; return true; }; auto const dashU = [this](std::string const&) -> bool { this->Impl->TestOptions.UseUnion = true; return true; }; auto const dashR = [this](std::string const& expr) -> bool { this->Impl->TestOptions.IncludeRegularExpression = expr; return true; }; auto const dashE = [this](std::string const& expr) -> bool { this->Impl->TestOptions.ExcludeRegularExpression = expr; return true; }; auto const dashL = [this](std::string const& expr) -> bool { this->Impl->TestOptions.LabelRegularExpression.push_back(expr); return true; }; auto const dashLE = [this](std::string const& expr) -> bool { this->Impl->TestOptions.ExcludeLabelRegularExpression.push_back(expr); return true; }; auto const dashFA = [this](std::string const& expr) -> bool { this->Impl->TestOptions.ExcludeFixtureRegularExpression = expr; return true; }; auto const dashFS = [this](std::string const& expr) -> bool { this->Impl->TestOptions.ExcludeFixtureSetupRegularExpression = expr; return true; }; auto const dashFC = [this](std::string const& expr) -> bool { this->Impl->TestOptions.ExcludeFixtureCleanupRegularExpression = expr; return true; }; using CommandArgument = cmCommandLineArgument; auto const arguments = std::vector{ CommandArgument{ "--dashboard", CommandArgument::Values::One, dashD }, CommandArgument{ "-D", "-D must be followed by dashboard mode or VAR=VALUE.", CommandArgument::Values::One, dashD }, CommandArgument{ "-D", "-D must be followed by dashboard mode or VAR=VALUE.", CommandArgument::Values::One, CommandArgument::RequiresSeparator::No, [this](std::string const& def) -> bool { // Unsuccessful parsing of VAR=VALUE has historically // been ignored. this->AddVariableDefinition(def); return true; } }, CommandArgument{ "-T", CommandArgument::Values::One, dashT }, CommandArgument{ "--test-action", CommandArgument::Values::One, dashT }, CommandArgument{ "-M", CommandArgument::Values::One, dashM }, CommandArgument{ "--test-model", CommandArgument::Values::One, dashM }, CommandArgument{ "--extra-submit", CommandArgument::Values::One, [this, &processSteps](std::string const& extra) -> bool { processSteps = true; this->SetTest("Submit"); return this->SubmitExtraFiles(extra); } }, CommandArgument{ "--build-and-test", "--build-and-test must have source and binary dir", CommandArgument::Values::Two, [this, &cmakeAndTest](std::string const& dirs) -> bool { cmakeAndTest = true; cmList dirList{ dirs }; if (dirList.size() != 2) { return false; } this->Impl->BuildAndTest.SourceDir = cmSystemTools::ToNormalizedPathOnDisk(dirList[0]); this->Impl->BuildAndTest.BinaryDir = cmSystemTools::ToNormalizedPathOnDisk(dirList[1]); cmSystemTools::MakeDirectory(this->Impl->BuildAndTest.BinaryDir); return true; } }, CommandArgument{ "--build-target", CommandArgument::Values::One, [this](std::string const& t) -> bool { this->Impl->BuildAndTest.BuildTargets.emplace_back(t); return true; } }, CommandArgument{ "--build-noclean", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->BuildAndTest.BuildNoClean = true; return true; } }, CommandArgument{ "--build-nocmake", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->BuildAndTest.BuildNoCMake = true; return true; } }, CommandArgument{ "--build-two-config", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->BuildAndTest.BuildTwoConfig = true; return true; } }, CommandArgument{ "--build-run-dir", CommandArgument::Values::One, [this](std::string const& dir) -> bool { this->Impl->BuildAndTest.BuildRunDir = dir; return true; } }, CommandArgument{ "--build-exe-dir", CommandArgument::Values::One, [this](std::string const& dir) -> bool { this->Impl->BuildAndTest.ExecutableDirectory = dir; return true; } }, CommandArgument{ "--test-timeout", CommandArgument::Values::One, [this](std::string const& t) -> bool { this->Impl->BuildAndTest.Timeout = cmDuration(atof(t.c_str())); return true; } }, CommandArgument{ "--build-generator", CommandArgument::Values::One, [this](std::string const& g) -> bool { this->Impl->BuildAndTest.BuildGenerator = g; return true; } }, CommandArgument{ "--build-generator-platform", CommandArgument::Values::One, [this](std::string const& p) -> bool { this->Impl->BuildAndTest.BuildGeneratorPlatform = p; return true; } }, CommandArgument{ "--build-generator-toolset", CommandArgument::Values::One, [this](std::string const& t) -> bool { this->Impl->BuildAndTest.BuildGeneratorToolset = t; return true; } }, CommandArgument{ "--build-project", CommandArgument::Values::One, [this](std::string const& p) -> bool { this->Impl->BuildAndTest.BuildProject = p; return true; } }, CommandArgument{ "--build-makeprogram", CommandArgument::Values::One, [this](std::string const& p) -> bool { this->Impl->BuildAndTest.BuildMakeProgram = p; return true; } }, CommandArgument{ "--build-config-sample", CommandArgument::Values::One, [this](std::string const& s) -> bool { this->Impl->BuildAndTest.ConfigSample = s; return true; } }, CommandArgument{ "-SP", CommandArgument::Values::One, dashSP }, CommandArgument{ "--script-new-process", CommandArgument::Values::One, dashSP }, CommandArgument{ "-SR", CommandArgument::Values::One, dashSR }, CommandArgument{ "--script-run", CommandArgument::Values::One, dashSR }, CommandArgument{ "-S", CommandArgument::Values::One, dash_S }, CommandArgument{ "--script", CommandArgument::Values::One, dash_S }, CommandArgument{ "-F", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->Failover = true; return true; } }, CommandArgument{ "-j", CommandArgument::Values::ZeroOrOne, [&dashJ](std::string const& j) -> bool { return dashJ("-j"_s, j); } }, CommandArgument{ "--parallel", CommandArgument::Values::ZeroOrOne, [&dashJ](std::string const& j) -> bool { return dashJ("--parallel"_s, j); } }, CommandArgument{ "-j", CommandArgument::Values::One, CommandArgument::RequiresSeparator::No, [this](std::string const& j) -> bool { // The value must be a non-negative integer. unsigned long plevel = 0; if (!cmStrToULong(j, &plevel)) { cmSystemTools::Error( cmStrCat("'-j' given invalid value '", j, '\'')); return false; } this->SetParallelLevel(plevel); this->Impl->ParallelLevelSetInCli = true; return true; } }, CommandArgument{ "--repeat-until-fail", "'--repeat-until-fail' requires an argument", CommandArgument::Values::One, [this](std::string const& r) -> bool { if (this->Impl->RepeatMode != cmCTest::Repeat::Never) { cmSystemTools::Error("At most one '--repeat' option may be used."); return false; } long repeat = 1; if (!cmStrToLong(r, &repeat)) { cmSystemTools::Error(cmStrCat( "'--repeat-until-fail' given non-integer value '", r, '\'')); return false; } this->Impl->RepeatCount = static_cast(repeat); if (repeat > 1) { this->Impl->RepeatMode = cmCTest::Repeat::UntilFail; } return true; } }, CommandArgument{ "--repeat", CommandArgument::Values::One, [this](std::string const& r) -> bool { if (this->Impl->RepeatMode != cmCTest::Repeat::Never) { cmSystemTools::Error("At most one '--repeat' option may be used."); return false; } cmsys::RegularExpression repeatRegex( "^(until-fail|until-pass|after-timeout):([0-9]+)$"); if (repeatRegex.find(r)) { std::string const& count = repeatRegex.match(2); unsigned long n = 1; cmStrToULong(count, &n); // regex guarantees success this->Impl->RepeatCount = static_cast(n); if (this->Impl->RepeatCount > 1) { std::string const& mode = repeatRegex.match(1); if (mode == "until-fail") { this->Impl->RepeatMode = cmCTest::Repeat::UntilFail; } else if (mode == "until-pass") { this->Impl->RepeatMode = cmCTest::Repeat::UntilPass; } else if (mode == "after-timeout") { this->Impl->RepeatMode = cmCTest::Repeat::AfterTimeout; } } } else { cmSystemTools::Error( cmStrCat("'--repeat' given invalid value '", r, '\'')); return false; } return true; } }, CommandArgument{ "--test-load", CommandArgument::Values::One, [this](std::string const& l) -> bool { unsigned long load; if (cmStrToULong(l, &load)) { this->SetTestLoad(load); } else { cmCTestLog( this, WARNING, "Invalid value for 'Test Load' : " << l << '\n'); } return true; } }, CommandArgument{ "--no-compress-output", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->CompressTestOutput = false; return true; } }, CommandArgument{ "--print-labels", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->PrintLabels = true; return true; } }, CommandArgument{ "--http1.0", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->UseHTTP10 = true; return true; } }, CommandArgument{ "--timeout", CommandArgument::Values::One, [this](std::string const& t) -> bool { auto timeout = cmDuration(atof(t.c_str())); this->Impl->GlobalTimeout = timeout; return true; } }, CommandArgument{ "--stop-time", CommandArgument::Values::One, [this](std::string const& t) -> bool { this->SetStopTime(t); return true; } }, CommandArgument{ "--stop-on-failure", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->StopOnFailure = true; return true; } }, CommandArgument{ "-C", CommandArgument::Values::One, dashC }, CommandArgument{ "--build-config", CommandArgument::Values::One, dashC }, CommandArgument{ "--debug", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->Debug = true; return true; } }, CommandArgument{ "--group", CommandArgument::Values::One, dashGroup }, // This is an undocumented / deprecated option. // "Track" has been renamed to "Group". CommandArgument{ "--track", CommandArgument::Values::One, dashGroup }, CommandArgument{ "--show-line-numbers", CommandArgument::Values::Zero, [](std::string const&) -> bool { // Silently ignore this never-documented and now-removed // option. return true; } }, CommandArgument{ "--no-label-summary", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->LabelSummary = false; return true; } }, CommandArgument{ "--no-subproject-summary", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->SubprojectSummary = false; return true; } }, CommandArgument{ "--progress", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->TestProgressOutput = true; return true; } }, CommandArgument{ "-Q", CommandArgument::Values::Zero, dashQ }, CommandArgument{ "--quiet", CommandArgument::Values::Zero, dashQ }, CommandArgument{ "-V", CommandArgument::Values::Zero, dashV }, CommandArgument{ "--verbose", CommandArgument::Values::Zero, dashV }, CommandArgument{ "-VV", CommandArgument::Values::Zero, dashVV }, CommandArgument{ "--extra-verbose", CommandArgument::Values::Zero, dashVV }, CommandArgument{ "--output-on-failure", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->OutputTestOutputOnTestFailure = true; return true; } }, CommandArgument{ "--test-output-size-passed", CommandArgument::Values::One, [this](std::string const& sz) -> bool { long outputSize; if (cmStrToLong(sz, &outputSize)) { this->Impl->TestOptions.OutputSizePassed = static_cast(outputSize); } else { cmCTestLog( this, WARNING, "Invalid value for '--test-output-size-passed': " << sz << "\n"); } return true; } }, CommandArgument{ "--test-output-size-failed", CommandArgument::Values::One, [this](std::string const& sz) -> bool { long outputSize; if (cmStrToLong(sz, &outputSize)) { this->Impl->TestOptions.OutputSizeFailed = static_cast(outputSize); } else { cmCTestLog( this, WARNING, "Invalid value for '--test-output-size-failed': " << sz << "\n"); } return true; } }, CommandArgument{ "--test-output-truncation", CommandArgument::Values::One, [this](std::string const& mode) -> bool { if (!SetTruncationMode(this->Impl->TestOptions.OutputTruncation, mode)) { cmSystemTools::Error( cmStrCat("Invalid value for '--test-output-truncation': ", mode)); return false; } return true; } }, CommandArgument{ "--show-only", CommandArgument::Values::ZeroOrOne, [this](std::string const& format) -> bool { this->Impl->ShowOnly = true; // Check if a specific format is requested. // Defaults to human readable text. if (format == "json-v1") { // Force quiet mode so the only output // is the json object model. this->Impl->Quiet = true; this->Impl->OutputAsJson = true; this->Impl->OutputAsJsonVersion = 1; } else if (format == "human") { } else if (!format.empty()) { cmSystemTools::Error( cmStrCat("'--show-only=' given unknown value '", format, '\'')); return false; } return true; } }, CommandArgument{ "-N", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->ShowOnly = true; return true; } }, CommandArgument{ "-O", CommandArgument::Values::One, dashO }, CommandArgument{ "--output-log", CommandArgument::Values::One, dashO }, CommandArgument{ "--tomorrow-tag", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->TomorrowTag = true; return true; } }, CommandArgument{ "--force-new-ctest-process", CommandArgument::Values::Zero, [](std::string const&) -> bool { // Silently ignore now-removed option. return true; } }, CommandArgument{ "-W", CommandArgument::Values::One, dashW }, CommandArgument{ "--max-width", CommandArgument::Values::One, dashW }, CommandArgument{ "--interactive-debug-mode", CommandArgument::Values::One, [this](std::string const& idm) -> bool { this->Impl->InteractiveDebugMode = cmIsOn(idm); return true; } }, CommandArgument{ "--http-header", CommandArgument::Values::One, [this](std::string const& h) -> bool { this->Impl->CommandLineHttpHeaders.push_back(h); return true; } }, CommandArgument{ "--submit-index", CommandArgument::Values::One, [this](std::string const& index) -> bool { this->Impl->SubmitIndex = atoi(index.c_str()); if (this->Impl->SubmitIndex < 0) { this->Impl->SubmitIndex = 0; } return true; } }, CommandArgument{ "--overwrite", CommandArgument::Values::One, [this](std::string const& opt) -> bool { this->AddCTestConfigurationOverwrite(opt); return true; } }, CommandArgument{ "-A", CommandArgument::Values::One, dashA }, CommandArgument{ "--add-notes", CommandArgument::Values::One, dashA }, CommandArgument{ "--test-dir", "'--test-dir' requires an argument", CommandArgument::Values::One, [this](std::string const& dir) -> bool { this->Impl->TestDir = dir; return true; } }, CommandArgument{ "--output-junit", CommandArgument::Values::One, [this](std::string const& file) -> bool { this->SetOutputJUnitFileName(file); return true; } }, CommandArgument{ "--no-tests", CommandArgument::Values::One, [this](std::string const& action) -> bool { if (action == "error"_s) { this->Impl->NoTestsMode = cmCTest::NoTests::Error; } else if (action == "ignore"_s) { this->Impl->NoTestsMode = cmCTest::NoTests::Ignore; } else { cmSystemTools::Error( cmStrCat("'--no-tests=' given unknown value '", action, '\'')); return false; } this->Impl->NoTestsModeSetInCli = true; return true; } }, CommandArgument{ "-I", CommandArgument::Values::One, dashI }, CommandArgument{ "--tests-information", CommandArgument::Values::One, dashI }, CommandArgument{ "-U", CommandArgument::Values::One, dashU }, CommandArgument{ "--union", CommandArgument::Values::One, dashU }, CommandArgument{ "-R", CommandArgument::Values::One, dashR }, CommandArgument{ "--tests-regex", CommandArgument::Values::One, dashR }, CommandArgument{ "-E", CommandArgument::Values::One, dashE }, CommandArgument{ "--exclude-regex", CommandArgument::Values::One, dashE }, CommandArgument{ "-L", CommandArgument::Values::One, dashL }, CommandArgument{ "--label-regex", CommandArgument::Values::One, dashL }, CommandArgument{ "-LE", CommandArgument::Values::One, dashLE }, CommandArgument{ "--label-exclude", CommandArgument::Values::One, dashLE }, CommandArgument{ "-FA", CommandArgument::Values::One, dashFA }, CommandArgument{ "--fixture-exclude-any", CommandArgument::Values::One, dashFA }, CommandArgument{ "-FS", CommandArgument::Values::One, dashFS }, CommandArgument{ "--fixture-exclude-setup", CommandArgument::Values::One, dashFS }, CommandArgument{ "-FC", CommandArgument::Values::One, dashFC }, CommandArgument{ "--fixture-exclude-cleanup", CommandArgument::Values::One, dashFC }, CommandArgument{ "--resource-spec-file", CommandArgument::Values::One, [this](std::string const& file) -> bool { this->Impl->TestOptions.ResourceSpecFile = file; return true; } }, CommandArgument{ "--tests-from-file", CommandArgument::Values::One, [this](std::string const& file) -> bool { this->Impl->TestOptions.TestListFile = file; return true; } }, CommandArgument{ "--exclude-from-file", CommandArgument::Values::One, [this](std::string const& file) -> bool { this->Impl->TestOptions.ExcludeTestListFile = file; return true; } }, CommandArgument{ "--schedule-random", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->TestOptions.ScheduleRandom = true; return true; } }, CommandArgument{ "--schedule-random-seed", CommandArgument::Values::One, [this](std::string const& sz) -> bool { unsigned long seed_value; if (cmStrToULong(sz, &seed_value)) { this->Impl->TestOptions.ScheduleRandomSeed = static_cast(seed_value); } else { cmCTestLog(this, WARNING, "Invalid value for '--schedule-random-seed': " << sz << "\n"); } return true; } }, CommandArgument{ "--rerun-failed", CommandArgument::Values::Zero, [this](std::string const&) -> bool { this->Impl->TestOptions.RerunFailed = true; return true; } }, }; // process the command line arguments for (size_t i = 1; i < args.size(); ++i) { std::string const& arg = args[i]; bool matched = false; for (auto const& m : arguments) { if (m.matches(arg)) { matched = true; if (!m.parse(arg, i, args)) { return 1; } break; } } if (!matched && arg == "--build-options"_s) { matched = true; while (i + 1 < args.size() && args[i + 1] != "--build-target"_s && args[i + 1] != "--test-command"_s) { ++i; this->Impl->BuildAndTest.BuildOptions.emplace_back(args[i]); } } if (!matched && arg == "--test-command"_s && i + 1 < args.size()) { matched = true; ++i; this->Impl->BuildAndTest.TestCommand = args[i]; while (i + 1 < args.size()) { ++i; this->Impl->BuildAndTest.TestCommandArgs.emplace_back(args[i]); } } if (!matched && cmHasLiteralPrefix(arg, "-") && !cmHasLiteralPrefix(arg, "--preset")) { cmSystemTools::Error(cmStrCat("Unknown argument: ", arg)); cmSystemTools::Error("Run 'ctest --help' for all supported options."); return 1; } } // handle CTEST_PARALLEL_LEVEL environment variable if (!this->Impl->ParallelLevelSetInCli) { if (cm::optional parallelEnv = cmSystemTools::GetEnvVar("CTEST_PARALLEL_LEVEL")) { if (parallelEnv->empty() || parallelEnv->find_first_not_of(" \t") == std::string::npos) { // An empty value tells ctest to choose a default. this->SetParallelLevel(cm::nullopt); } else { // A non-empty value must be a non-negative integer. // Otherwise, ignore it. unsigned long plevel = 0; if (cmStrToULong(*parallelEnv, &plevel)) { this->SetParallelLevel(plevel); } } } } // handle CTEST_NO_TESTS_ACTION environment variable if (!this->Impl->NoTestsModeSetInCli) { std::string action; if (cmSystemTools::GetEnv("CTEST_NO_TESTS_ACTION", action) && !action.empty()) { if (action == "error"_s) { this->Impl->NoTestsMode = cmCTest::NoTests::Error; } else if (action == "ignore"_s) { this->Impl->NoTestsMode = cmCTest::NoTests::Ignore; } else { cmCTestLog(this, ERROR_MESSAGE, "Unknown value for CTEST_NO_TESTS_ACTION: '" << action << '\''); return 1; } } } // TestProgressOutput only supported if console supports it and not logging // to a file this->Impl->TestProgressOutput = this->Impl->TestProgressOutput && !this->Impl->OutputLogFile && this->ProgressOutputSupportedByConsole(); #ifdef _WIN32 if (this->Impl->TestProgressOutput) { // Disable output line buffering so we can print content without // a newline. std::setvbuf(stdout, nullptr, _IONBF, 0); } #endif // now what should cmake do? if --build-and-test was specified then // we run the build and test handler and return if (cmakeAndTest) { return this->RunCMakeAndTest(); } // -S, -SP, and/or -SP was specified if (!runScripts.empty()) { return this->RunScripts(runScripts); } // Establish the working directory. std::string const currDir = cmSystemTools::GetLogicalWorkingDirectory(); std::string workDir = currDir; if (!this->Impl->TestDir.empty()) { workDir = cmSystemTools::ToNormalizedPathOnDisk(this->Impl->TestDir); } cmWorkingDirectory changeDir(workDir); if (changeDir.Failed()) { cmCTestLog(this, ERROR_MESSAGE, changeDir.GetError() << std::endl); return 1; } this->Impl->BinaryDir = workDir; // -D, -T, and/or -M was specified if (processSteps) { return this->ProcessSteps(); } return this->ExecuteTests(args); } int cmCTest::RunScripts( std::vector> const& scripts) { if (this->Impl->ExtraVerbose) { cmCTestLog(this, OUTPUT, "* Extra verbosity turned on" << std::endl); } auto ch = cm::make_unique(this); for (auto const& script : scripts) { ch->AddConfigurationScript(script.first, script.second); } int res = ch->ProcessHandler(); if (res != 0) { cmCTestLog(this, DEBUG, "running script failing returning: " << res << std::endl); } return res; } int cmCTest::ExecuteTests(std::vector const& args) { this->Impl->ExtraVerbose = this->Impl->Verbose; this->Impl->Verbose = true; cmCTestLog(this, DEBUG, "Here: " << __LINE__ << std::endl); if (!this->Impl->InteractiveDebugMode) { this->BlockTestErrorDiagnostics(); } else { cmSystemTools::PutEnv("CTEST_INTERACTIVE_DEBUG_MODE=1"); } this->UpdateCTestConfiguration(); cmCTestLog(this, DEBUG, "Here: " << __LINE__ << std::endl); cmCTestTestHandler handler(this); { cmake cm(cmake::RoleScript, cmState::CTest); cm.SetHomeDirectory(""); cm.SetHomeOutputDirectory(""); cm.GetCurrentSnapshot().SetDefaultDefinitions(); cmGlobalGenerator gg(&cm); cmMakefile mf(&gg, cm.GetCurrentSnapshot()); this->ReadCustomConfigurationFileTree(this->Impl->BinaryDir, &mf); handler.PopulateCustomVectors(&mf); } handler.SetVerbose(this->Impl->Verbose); cmInstrumentation instrumentation(this->GetBinaryDir()); auto processHandler = [&handler]() -> int { return handler.ProcessHandler(); }; int ret = instrumentation.InstrumentCommand("ctest", args, processHandler); instrumentation.CollectTimingData(cmInstrumentationQuery::Hook::PostCTest); if (ret < 0) { cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest\n"); if (!this->Impl->OutputTestOutputOnTestFailure) { std::string const lastTestLog = this->GetBinaryDir() + "/Testing/Temporary/LastTest.log"; cmCTestLog(this, ERROR_MESSAGE, "Output from these tests are in: " << lastTestLog << '\n'); cmCTestLog(this, ERROR_MESSAGE, "Use \"--rerun-failed --output-on-failure\" to re-run the " "failed cases verbosely.\n"); } return cmCTest::TEST_ERRORS; } return 0; } int cmCTest::RunCMakeAndTest() { return this->Impl->BuildAndTest.Run(); } void cmCTest::SetNotesFiles(std::string const& notes) { this->Impl->NotesFiles = notes; } bool cmCTest::GetStopOnFailure() const { return this->Impl->StopOnFailure; } void cmCTest::SetStopOnFailure(bool stop) { this->Impl->StopOnFailure = stop; } std::chrono::system_clock::time_point cmCTest::GetStopTime() const { return this->Impl->StopTime; } void cmCTest::SetStopTime(std::string const& time_str) { struct tm* lctime; time_t current_time = time(nullptr); lctime = gmtime(¤t_time); int gm_hour = lctime->tm_hour; time_t gm_time = mktime(lctime); lctime = localtime(¤t_time); int local_hour = lctime->tm_hour; int tzone_offset = local_hour - gm_hour; if (gm_time > current_time && gm_hour < local_hour) { // this means gm_time is on the next day tzone_offset -= 24; } else if (gm_time < current_time && gm_hour > local_hour) { // this means gm_time is on the previous day tzone_offset += 24; } tzone_offset *= 100; char buf[1024]; snprintf(buf, sizeof(buf), "%d%02d%02d %s %+05i", lctime->tm_year + 1900, lctime->tm_mon + 1, lctime->tm_mday, time_str.c_str(), tzone_offset); time_t stop_time = cm_parse_date(current_time, buf); if (stop_time == -1) { this->Impl->StopTime = std::chrono::system_clock::time_point(); return; } this->Impl->StopTime = std::chrono::system_clock::from_time_t(stop_time); if (stop_time < current_time) { this->Impl->StopTime += std::chrono::hours(24); } } cm::optional cmCTest::GetRandomSeed() const { return this->Impl->TestOptions.ScheduleRandomSeed; } std::string cmCTest::GetScheduleType() const { return this->Impl->ScheduleType; } void cmCTest::SetScheduleType(std::string const& type) { this->Impl->ScheduleType = type; } void cmCTest::ReadCustomConfigurationFileTree(std::string const& dir, cmMakefile* mf) { cmCTestLog(this, DEBUG, "* Read custom CTest configuration directory: " << dir << std::endl); auto const fname = [this, &dir]() -> std::string { for (char const* ext : { ".cmake", ".ctest" }) { std::string path = cmStrCat(dir, "/CTestCustom", ext); cmCTestLog(this, DEBUG, "* Check for file: " << path << std::endl); if (cmSystemTools::FileExists(path)) { return path; } } return ""; }(); if (!fname.empty()) { cmCTestLog(this, DEBUG, "* Read custom CTest configuration file: " << fname << std::endl); bool erroroc = cmSystemTools::GetErrorOccurredFlag(); cmSystemTools::ResetErrorOccurredFlag(); if (!mf->ReadListFile(fname) || cmSystemTools::GetErrorOccurredFlag()) { cmCTestLog(this, ERROR_MESSAGE, "Problem reading custom configuration: " << fname << std::endl); } if (erroroc) { cmSystemTools::SetErrorOccurred(); } } } void cmCTest::PopulateCustomVector(cmMakefile* mf, std::string const& def, std::vector& vec) { cmValue dval = mf->GetDefinition(def); if (!dval) { return; } cmCTestLog(this, DEBUG, "PopulateCustomVector: " << def << std::endl); cmList::assign(vec, *dval); for (std::string const& it : vec) { cmCTestLog(this, DEBUG, " -- " << it << std::endl); } } void cmCTest::PopulateCustomInteger(cmMakefile* mf, std::string const& def, int& val) { cmValue dval = mf->GetDefinition(def); if (!dval) { return; } val = atoi(dval->c_str()); } std::string cmCTest::GetShortPathToFile(std::string const& cfname) { std::string const& sourceDir = this->GetCTestConfiguration("SourceDirectory"); std::string const& buildDir = this->GetCTestConfiguration("BuildDirectory"); std::string fname = cmSystemTools::CollapseFullPath(cfname); // Find relative paths to both directories std::string srcRelpath = cmSystemTools::RelativePath(sourceDir, fname); std::string bldRelpath = cmSystemTools::RelativePath(buildDir, fname); // If any contains "." it is not parent directory bool inSrc = srcRelpath.find("..") == std::string::npos; bool inBld = bldRelpath.find("..") == std::string::npos; // TODO: Handle files with .. in their name std::string* res = nullptr; if (inSrc && inBld) { // If both have relative path with no dots, pick the shorter one if (srcRelpath.size() < bldRelpath.size()) { res = &srcRelpath; } else { res = &bldRelpath; } } else if (inSrc) { res = &srcRelpath; } else if (inBld) { res = &bldRelpath; } std::string path; if (!res) { path = fname; } else { cmSystemTools::ConvertToUnixSlashes(*res); path = "./" + *res; if (path.back() == '/') { path.resize(path.size() - 1); } } cmsys::SystemTools::ReplaceString(path, ":", "_"); cmsys::SystemTools::ReplaceString(path, " ", "_"); return path; } std::string cmCTest::GetCTestConfiguration(std::string const& name) { if (this->Impl->CTestConfigurationOverwrites.find(name) != this->Impl->CTestConfigurationOverwrites.end()) { return this->Impl->CTestConfigurationOverwrites[name]; } return this->Impl->CTestConfiguration[name]; } void cmCTest::EmptyCTestConfiguration() { this->Impl->CTestConfiguration.clear(); } void cmCTest::SetCTestConfiguration(char const* name, std::string const& value, bool suppress) { cmCTestOptionalLog(this, HANDLER_VERBOSE_OUTPUT, "SetCTestConfiguration:" << name << ":" << value << "\n", suppress); if (!name) { return; } if (value.empty()) { this->Impl->CTestConfiguration.erase(name); return; } this->Impl->CTestConfiguration[name] = value; } std::string cmCTest::GetSubmitURL() { std::string url = this->GetCTestConfiguration("SubmitURL"); if (url.empty()) { std::string method = this->GetCTestConfiguration("DropMethod"); std::string user = this->GetCTestConfiguration("DropSiteUser"); std::string password = this->GetCTestConfiguration("DropSitePassword"); std::string site = this->GetCTestConfiguration("DropSite"); std::string location = this->GetCTestConfiguration("DropLocation"); url = cmStrCat(method.empty() ? "http" : method, "://"_s); if (!user.empty()) { url += user; if (!password.empty()) { url += ':'; url += password; } url += '@'; } url += site; url += location; } return url; } std::string cmCTest::GetCurrentTag() { return this->Impl->CurrentTag; } std::string cmCTest::GetBinaryDir() { return this->Impl->BinaryDir; } std::string const& cmCTest::GetConfigType() { return this->Impl->ConfigType; } cmDuration cmCTest::GetTimeOut() const { return this->Impl->TimeOut; } void cmCTest::SetTimeOut(cmDuration t) { this->Impl->TimeOut = t; } cmDuration cmCTest::GetGlobalTimeout() const { return this->Impl->GlobalTimeout; } bool cmCTest::GetShowOnly() { return this->Impl->ShowOnly; } bool cmCTest::GetOutputAsJson() { return this->Impl->OutputAsJson; } int cmCTest::GetOutputAsJsonVersion() { return this->Impl->OutputAsJsonVersion; } bool cmCTest::ShouldUseHTTP10() const { return this->Impl->UseHTTP10; } bool cmCTest::ShouldPrintLabels() const { return this->Impl->PrintLabels; } int cmCTest::GetMaxTestNameWidth() const { return this->Impl->MaxTestNameWidth; } void cmCTest::SetMaxTestNameWidth(int w) { this->Impl->MaxTestNameWidth = w; } void cmCTest::SetProduceXML(bool v) { this->Impl->ProduceXML = v; } bool cmCTest::GetProduceXML() { return this->Impl->ProduceXML; } std::vector& cmCTest::GetInitialCommandLineArguments() { return this->Impl->InitialCommandLineArguments; } char const* cmCTest::GetSpecificGroup() { if (this->Impl->SpecificGroup.empty()) { return nullptr; } return this->Impl->SpecificGroup.c_str(); } void cmCTest::SetSpecificGroup(char const* group) { if (!group) { this->Impl->SpecificGroup.clear(); return; } this->Impl->SpecificGroup = group; } void cmCTest::SetFailover(bool failover) { this->Impl->Failover = failover; } bool cmCTest::GetFailover() const { return this->Impl->Failover; } bool cmCTest::GetTestProgressOutput() const { return this->Impl->TestProgressOutput && !GetExtraVerbose(); } bool cmCTest::GetVerbose() const { return this->Impl->Verbose; } bool cmCTest::GetExtraVerbose() const { return this->Impl->ExtraVerbose; } bool cmCTest::GetInteractiveDebugMode() const { return this->Impl->InteractiveDebugMode; } bool cmCTest::GetLabelSummary() const { return this->Impl->LabelSummary; } bool cmCTest::GetSubprojectSummary() const { return this->Impl->SubprojectSummary; } bool cmCTest::GetOutputTestOutputOnTestFailure() const { return this->Impl->OutputTestOutputOnTestFailure; } std::map const& cmCTest::GetDefinitions() const { return this->Impl->Definitions; } int cmCTest::GetRepeatCount() const { return this->Impl->RepeatCount; } cmCTest::Repeat cmCTest::GetRepeatMode() const { return this->Impl->RepeatMode; } cmCTest::NoTests cmCTest::GetNoTestsMode() const { return this->Impl->NoTestsMode; } void cmCTest::SetBuildID(std::string const& id) { this->Impl->BuildID = id; } std::string cmCTest::GetBuildID() const { return this->Impl->BuildID; } cmCTestTestOptions const& cmCTest::GetTestOptions() const { return this->Impl->TestOptions; } std::vector cmCTest::GetCommandLineHttpHeaders() const { return this->Impl->CommandLineHttpHeaders; } void cmCTest::AddSubmitFile(Part part, std::string const& name) { this->Impl->Parts[part].SubmitFiles.emplace_back(name); } std::vector const& cmCTest::GetSubmitFiles(Part part) const { return this->Impl->Parts[part].SubmitFiles; } void cmCTest::ClearSubmitFiles(Part part) { this->Impl->Parts[part].SubmitFiles.clear(); } void cmCTest::AddCTestConfigurationOverwrite(std::string const& overStr) { size_t epos = overStr.find('='); if (epos == std::string::npos) { cmCTestLog(this, ERROR_MESSAGE, "CTest configuration overwrite specified in the wrong format.\n" "Valid format is: --overwrite key=value\n" "The specified was: --overwrite " << overStr << '\n'); return; } std::string key = overStr.substr(0, epos); std::string value = overStr.substr(epos + 1); this->Impl->CTestConfigurationOverwrites[key] = value; } void cmCTest::SetConfigType(std::string const& ct) { this->Impl->ConfigType = ct; cmSystemTools::ReplaceString(this->Impl->ConfigType, ".\\", ""); std::string confTypeEnv = "CMAKE_CONFIG_TYPE=" + this->Impl->ConfigType; cmSystemTools::PutEnv(confTypeEnv); } bool cmCTest::SetCTestConfigurationFromCMakeVariable( cmMakefile* mf, char const* dconfig, std::string const& cmake_var, bool suppress) { cmValue ctvar = mf->GetDefinition(cmake_var); if (!ctvar) { return false; } cmCTestOptionalLog(this, HANDLER_VERBOSE_OUTPUT, "SetCTestConfigurationFromCMakeVariable:" << dconfig << ":" << cmake_var << std::endl, suppress); this->SetCTestConfiguration(dconfig, *ctvar, suppress); return true; } void cmCTest::SetCMakeVariables(cmMakefile& mf) { auto set = [&](char const* cmake_var, char const* ctest_opt) { std::string val = this->GetCTestConfiguration(ctest_opt); if (!val.empty()) { cmCTestOptionalLog( this, HANDLER_VERBOSE_OUTPUT, "SetCMakeVariable:" << cmake_var << ":" << val << std::endl, false); mf.AddDefinition(cmake_var, val); } }; set("CTEST_SITE", "Site"); set("CTEST_BUILD_NAME", "BuildName"); set("CTEST_NIGHTLY_START_TIME", "NightlyStartTime"); set("CTEST_SOURCE_DIRECTORY", "SourceDirectory"); set("CTEST_BINARY_DIRECTORY", "BuildDirectory"); // CTest Update Step set("CTEST_UPDATE_COMMAND", "UpdateCommand"); set("CTEST_UPDATE_OPTIONS", "UpdateOptions"); set("CTEST_UPDATE_TYPE", "UpdateType"); set("CTEST_CVS_COMMAND", "CVSCommand"); set("CTEST_CVS_UPDATE_OPTIONS", "CVSUpdateOptions"); set("CTEST_SVN_COMMAND", "SVNCommand"); set("CTEST_SVN_UPDATE_OPTIONS", "SVNUpdateOptions"); set("CTEST_SVN_OPTIONS", "SVNOptions"); set("CTEST_BZR_COMMAND", "BZRCommand"); set("CTEST_BZR_UPDATE_OPTIONS", "BZRUpdateOptions"); set("CTEST_GIT_COMMAND", "GITCommand"); set("CTEST_GIT_UPDATE_OPTIONS", "GITUpdateOptions"); set("CTEST_GIT_INIT_SUBMODULES", "GITInitSubmodules"); set("CTEST_GIT_UPDATE_CUSTOM", "GITUpdateCustom"); set("CTEST_UPDATE_VERSION_ONLY", "UpdateVersionOnly"); set("CTEST_UPDATE_VERSION_OVERRIDE", "UpdateVersionOverride"); set("CTEST_HG_COMMAND", "HGCommand"); set("CTEST_HG_UPDATE_OPTIONS", "HGUpdateOptions"); set("CTEST_P4_COMMAND", "P4Command"); set("CTEST_P4_UPDATE_CUSTOM", "P4UpdateCustom"); set("CTEST_P4_UPDATE_OPTIONS", "P4UpdateOptions"); set("CTEST_P4_CLIENT", "P4Client"); set("CTEST_P4_OPTIONS", "P4Options"); // CTest Configure Step set("CTEST_CONFIGURE_COMMAND", "ConfigureCommand"); set("CTEST_LABELS_FOR_SUBPROJECTS", "LabelsForSubprojects"); // CTest Build Step set("CTEST_BUILD_COMMAND", "MakeCommand"); set("CTEST_USE_LAUNCHERS", "UseLaunchers"); // CTest Test Step set("CTEST_TEST_TIMEOUT", "TimeOut"); // CTest Coverage Step set("CTEST_COVERAGE_COMMAND", "CoverageCommand"); set("CTEST_COVERAGE_EXTRA_FLAGS", "CoverageExtraFlags"); // CTest MemCheck Step set("CTEST_MEMORYCHECK_TYPE", "MemoryCheckType"); set("CTEST_MEMORYCHECK_SANITIZER_OPTIONS", "MemoryCheckSanitizerOptions"); set("CTEST_MEMORYCHECK_COMMAND", "MemoryCheckCommand"); set("CTEST_MEMORYCHECK_COMMAND_OPTIONS", "MemoryCheckCommandOptions"); set("CTEST_MEMORYCHECK_SUPPRESSIONS_FILE", "MemoryCheckSuppressionFile"); // CTest Submit Step set("CTEST_SUBMIT_URL", "SubmitURL"); set("CTEST_DROP_METHOD", "DropMethod"); set("CTEST_DROP_SITE_USER", "DropSiteUser"); set("CTEST_DROP_SITE_PASSWORD", "DropSitePassword"); set("CTEST_DROP_SITE", "DropSite"); set("CTEST_DROP_LOCATION", "DropLocation"); set("CTEST_TLS_VERIFY", "TLSVerify"); set("CTEST_TLS_VERSION", "TLSVersion"); set("CTEST_CURL_OPTIONS", "CurlOptions"); set("CTEST_SUBMIT_INACTIVITY_TIMEOUT", "SubmitInactivityTimeout"); } bool cmCTest::RunCommand(std::vector const& args, std::string* stdOut, std::string* stdErr, int* retVal, char const* dir, cmDuration timeout, Encoding encoding) { std::vector argv; argv.reserve(args.size() + 1); for (std::string const& a : args) { argv.push_back(a.c_str()); } argv.push_back(nullptr); stdOut->clear(); stdErr->clear(); cmUVProcessChainBuilder builder; builder.AddCommand(args) .SetBuiltinStream(cmUVProcessChainBuilder::Stream_OUTPUT) .SetBuiltinStream(cmUVProcessChainBuilder::Stream_ERROR); if (dir) { builder.SetWorkingDirectory(dir); } auto chain = builder.Start(); cm::uv_timer_ptr timer; bool timedOut = false; if (timeout.count()) { timer.init(chain.GetLoop(), &timedOut); timer.start( [](uv_timer_t* t) { auto* timedOutPtr = static_cast(t->data); *timedOutPtr = true; }, static_cast(timeout.count() * 1000.0), 0); } std::vector tempOutput; bool outFinished = false; cm::uv_pipe_ptr outStream; std::vector tempError; bool errFinished = false; cm::uv_pipe_ptr errStream; cmProcessOutput processOutput(encoding); auto startRead = [this, &chain, &processOutput]( cm::uv_pipe_ptr& pipe, int stream, std::vector& temp, bool& finished) -> std::unique_ptr { pipe.init(chain.GetLoop(), 0); uv_pipe_open(pipe, stream); return cmUVStreamRead( pipe, [this, &temp, &processOutput](std::vector data) { cm::append(temp, data); if (this->Impl->ExtraVerbose) { std::string strdata; processOutput.DecodeText(data.data(), data.size(), strdata); cmSystemTools::Stdout(strdata); } }, [&finished]() { finished = true; }); }; auto outputHandle = startRead(outStream, chain.OutputStream(), tempOutput, outFinished); auto errorHandle = startRead(errStream, chain.ErrorStream(), tempError, errFinished); while (!timedOut && !(outFinished && errFinished)) { uv_run(&chain.GetLoop(), UV_RUN_ONCE); } if (this->Impl->ExtraVerbose) { std::string strdata; processOutput.DecodeText(std::string(), strdata); if (!strdata.empty()) { cmSystemTools::Stdout(strdata); } } while (!timedOut && !chain.Finished()) { uv_run(&chain.GetLoop(), UV_RUN_ONCE); } if (!tempOutput.empty()) { processOutput.DecodeText(tempOutput, tempOutput); stdOut->append(tempOutput.data(), tempOutput.size()); } if (!tempError.empty()) { processOutput.DecodeText(tempError, tempError); stdErr->append(tempError.data(), tempError.size()); } bool result = true; if (timedOut) { char const* error_str = "Process terminated due to timeout\n"; cmCTestLog(this, ERROR_MESSAGE, error_str << std::endl); stdErr->append(error_str, strlen(error_str)); result = false; } else { auto const& status = chain.GetStatus(0); auto exception = status.GetException(); switch (exception.first) { case cmUVProcessChain::ExceptionCode::None: if (retVal) { *retVal = static_cast(status.ExitStatus); } else { if (status.ExitStatus != 0) { result = false; } } break; default: { cmCTestLog(this, ERROR_MESSAGE, exception.second << std::endl); stdErr->append(exception.second); result = false; } break; } } return result; } void cmCTest::SetOutputLogFileName(std::string const& name) { if (!name.empty()) { this->Impl->OutputLogFile = cm::make_unique(name); } else { this->Impl->OutputLogFile.reset(); } } void cmCTest::SetOutputJUnitFileName(std::string const& name) { this->Impl->TestOptions.JUnitXMLFileName = name; // Turn test output compression off. // This makes it easier to include test output in the resulting // JUnit XML report. this->Impl->CompressTestOutput = false; } static char const* cmCTestStringLogType[] = { "DEBUG", "OUTPUT", "HANDLER_OUTPUT", "HANDLER_PROGRESS_OUTPUT", "HANDLER_TEST_PROGRESS_OUTPUT", "HANDLER_VERBOSE_OUTPUT", "WARNING", "ERROR_MESSAGE" }; void cmCTest::Log(LogType logType, std::string msg, bool suppress) { if (msg.empty()) { return; } if (suppress && logType != cmCTest::ERROR_MESSAGE) { return; } if (logType == cmCTest::HANDLER_PROGRESS_OUTPUT && (this->Impl->Debug || this->Impl->ExtraVerbose)) { return; } if (this->Impl->OutputLogFile) { bool display = true; if (logType == cmCTest::DEBUG && !this->Impl->Debug) { display = false; } if (logType == cmCTest::HANDLER_VERBOSE_OUTPUT && !this->Impl->Debug && !this->Impl->ExtraVerbose) { display = false; } if (display) { if (this->Impl->OutputLogFileLastTag && logType != *this->Impl->OutputLogFileLastTag) { *this->Impl->OutputLogFile << "[" << cmCTestStringLogType[logType] << "] " << std::endl; } *this->Impl->OutputLogFile << msg << std::flush; if (this->Impl->OutputLogFileLastTag && logType != *this->Impl->OutputLogFileLastTag) { *this->Impl->OutputLogFile << std::endl; this->Impl->OutputLogFileLastTag = logType; } } } if (!this->Impl->Quiet) { if (logType == HANDLER_TEST_PROGRESS_OUTPUT) { if (this->Impl->TestProgressOutput) { if (this->Impl->FlushTestProgressLine) { printf("\r"); this->Impl->FlushTestProgressLine = false; std::cout.flush(); } if (msg.find('\n') != std::string::npos) { this->Impl->FlushTestProgressLine = true; msg.erase(std::remove(msg.begin(), msg.end(), '\n'), msg.end()); } std::cout << msg; #ifndef _WIN32 printf("\x1B[K"); // move caret to end #endif std::cout.flush(); return; } logType = HANDLER_OUTPUT; } switch (logType) { case DEBUG: if (this->Impl->Debug) { std::cout << msg << std::flush; } break; case OUTPUT: case HANDLER_OUTPUT: if (this->Impl->Debug || this->Impl->Verbose) { std::cout << msg << std::flush; } break; case HANDLER_VERBOSE_OUTPUT: if (this->Impl->Debug || this->Impl->ExtraVerbose) { std::cout << msg << std::flush; } break; case WARNING: std::cerr << msg << std::flush; break; case ERROR_MESSAGE: std::cerr << msg << std::flush; cmSystemTools::SetErrorOccurred(); break; default: std::cout << msg << std::flush; } } } std::string cmCTest::GetColorCode(Color color) const { if (this->Impl->OutputColorCode) { return cmStrCat("\033[0;", static_cast(color), 'm'); } return {}; } void cmCTest::SetTimeLimit(cmValue val) { this->Impl->TimeLimit = val ? cmDuration(atof(val->c_str())) : cmCTest::MaxDuration(); } cmDuration cmCTest::GetElapsedTime() const { return std::chrono::duration_cast( std::chrono::steady_clock::now() - this->Impl->StartTime); } cmDuration cmCTest::GetRemainingTimeAllowed() const { if (this->Impl->TimeLimit == cmCTest::MaxDuration()) { return cmCTest::MaxDuration(); } return this->Impl->TimeLimit - this->GetElapsedTime(); } cmDuration cmCTest::MaxDuration() { return cmDuration(1.0e7); } bool cmCTest::CompressString(std::string& str) { int ret; z_stream strm; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; ret = deflateInit(&strm, -1); // default compression level if (ret != Z_OK) { return false; } unsigned char* in = reinterpret_cast(const_cast(str.c_str())); // zlib makes the guarantee that this is the maximum output size int outSize = static_cast(static_cast(str.size()) * 1.001 + 13.0); std::vector out(outSize); strm.avail_in = static_cast(str.size()); strm.next_in = in; strm.avail_out = outSize; strm.next_out = out.data(); ret = deflate(&strm, Z_FINISH); if (ret != Z_STREAM_END) { cmCTestLog(this, ERROR_MESSAGE, "Error during gzip compression." << std::endl); return false; } (void)deflateEnd(&strm); // Now base64 encode the resulting binary string std::vector base64EncodedBuffer((outSize * 3) / 2); size_t rlen = cmsysBase64_Encode(out.data(), strm.total_out, base64EncodedBuffer.data(), 1); str.assign(reinterpret_cast(base64EncodedBuffer.data()), rlen); return true; } bool cmCTest::StartResultingXML(Part part, char const* name, int submitIndex, cmGeneratedFileStream& xofs) { if (!name) { cmCTestLog( this, ERROR_MESSAGE, "Cannot create resulting XML file without providing the name\n"); return false; } if (submitIndex == 0) { submitIndex = this->Impl->SubmitIndex; } std::ostringstream ostr; ostr << name; if (submitIndex > 0) { ostr << "_" << submitIndex; } ostr << ".xml"; if (this->Impl->CurrentTag.empty()) { cmCTestLog(this, ERROR_MESSAGE, "Current Tag empty, this may mean NightlyStartTime / " "CTEST_NIGHTLY_START_TIME was not set correctly. Or " "maybe you forgot to call ctest_start() before calling " "ctest_configure().\n"); cmSystemTools::SetFatalErrorOccurred(); return false; } if (!this->OpenOutputFile(this->Impl->CurrentTag, ostr.str(), xofs, true)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create resulting XML file: " << ostr.str() << '\n'); return false; } this->AddSubmitFile(part, ostr.str()); return true; } bool cmCTest::StartLogFile(char const* name, int submitIndex, cmGeneratedFileStream& xofs) { if (!name) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create log file without providing the name\n"); return false; } if (submitIndex == 0) { submitIndex = this->Impl->SubmitIndex; } std::ostringstream ostr; ostr << "Last" << name; if (submitIndex > 0) { ostr << "_" << submitIndex; } if (!this->Impl->CurrentTag.empty()) { ostr << "_" << this->Impl->CurrentTag; } ostr << ".log"; if (!this->OpenOutputFile("Temporary", ostr.str(), xofs)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create log file: " << ostr.str() << '\n'); return false; } return true; } cmInstrumentation& cmCTest::GetInstrumentation() { if (!this->Impl->Instrumentation) { this->Impl->Instrumentation = cm::make_unique(this->GetBinaryDir()); } return *this->Impl->Instrumentation; } void cmCTest::ConvertInstrumentationSnippetsToXML(cmXMLWriter& xml, std::string const& subdir) { std::string data_dir = cmStrCat(this->GetInstrumentation().GetCDashDir(), '/', subdir); cmsys::Directory d; if (!d.Load(data_dir) || d.GetNumberOfFiles() == 0) { return; } xml.StartElement("Commands"); for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) { std::string fpath = d.GetFilePath(i); std::string fname = d.GetFile(i); if (fname.rfind('.', 0) == 0) { continue; } this->ConvertInstrumentationJSONFileToXML(fpath, xml); } xml.EndElement(); // Commands } bool cmCTest::ConvertInstrumentationJSONFileToXML(std::string const& fpath, cmXMLWriter& xml) { bool verboseCommands = this->GetInstrumentation().HasOption( cmInstrumentationQuery::Option::CDashVerbose); Json::Value root; this->Impl->parseState = cmJSONState(fpath, &root); if (!this->Impl->parseState.errors.empty()) { cmCTestLog(this, ERROR_MESSAGE, this->Impl->parseState.GetErrorMessage(true) << std::endl); return false; } if (root.type() != Json::objectValue) { cmCTestLog(this, ERROR_MESSAGE, "Expected object, found " << root.type() << " for " << root.asString() << std::endl); return false; } std::vector required_members = { "command", "role", "dynamicSystemInformation", }; for (std::string const& required_member : required_members) { if (!root.isMember(required_member)) { cmCTestLog(this, ERROR_MESSAGE, fpath << " is missing the '" << required_member << "' key" << std::endl); return false; } } // Do not record command-level data for Test.xml files because // it is redundant with information actually captured by CTest. bool generating_test_xml = root["role"] == "test"; if (!generating_test_xml) { std::string element_name = root["role"].asString(); element_name[0] = static_cast(std::toupper(element_name[0])); xml.StartElement(element_name); std::vector keys = root.getMemberNames(); for (auto const& key : keys) { auto key_type = root[key].type(); if (key_type == Json::objectValue || key_type == Json::arrayValue) { continue; } if (key == "role" || key == "target" || key == "targetType" || key == "targetLabels") { continue; } // Truncate the full command line if verbose instrumentation // was not requested. if (key == "command" && !verboseCommands) { std::string command_str = root[key].asString(); std::string truncated = command_str.substr(0, command_str.find(' ')); if (command_str != truncated) { truncated = cmStrCat(truncated, " (truncated)"); } xml.Attribute(key.c_str(), truncated); continue; } xml.Attribute(key.c_str(), root[key].asString()); } } // Record dynamicSystemInformation section as XML. auto dynamic_information = root["dynamicSystemInformation"]; std::vector keys = dynamic_information.getMemberNames(); for (auto const& key : keys) { std::string measurement_name = key; measurement_name[0] = static_cast(std::toupper(measurement_name[0])); xml.StartElement("NamedMeasurement"); xml.Attribute("type", "numeric/double"); xml.Attribute("name", measurement_name); xml.Element("Value", dynamic_information[key].asString()); xml.EndElement(); // NamedMeasurement } // Record information about outputs and their sizes if found. if (root.isMember("outputs") && root.isMember("outputSizes")) { Json::ArrayIndex num_outputs = std::min(root["outputs"].size(), root["outputSizes"].size()); if (num_outputs > 0) { xml.StartElement("Outputs"); for (Json::ArrayIndex i = 0; i < num_outputs; ++i) { xml.StartElement("Output"); xml.Attribute("name", root["outputs"][i].asString()); xml.Attribute("size", root["outputSizes"][i].asString()); xml.EndElement(); // Output } xml.EndElement(); // Outputs } } if (!generating_test_xml) { xml.EndElement(); // role } cmSystemTools::RemoveFile(fpath); return true; }