/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt 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 "cmsys/Base64.h" #include "cmsys/Directory.hxx" #include "cmsys/FStream.hxx" #include "cmsys/Glob.hxx" #include "cmsys/RegularExpression.hxx" #include "cmsys/SystemInformation.hxx" #if defined(_WIN32) # include // IWYU pragma: keep #else # include // IWYU pragma: keep #endif #include "cmCMakePresetsGraph.h" #include "cmCTestBuildAndTestHandler.h" #include "cmCTestBuildHandler.h" #include "cmCTestConfigureHandler.h" #include "cmCTestCoverageHandler.h" #include "cmCTestGenericHandler.h" #include "cmCTestMemCheckHandler.h" #include "cmCTestScriptHandler.h" #include "cmCTestStartCommand.h" #include "cmCTestSubmitHandler.h" #include "cmCTestTestHandler.h" #include "cmCTestUpdateHandler.h" #include "cmCTestUploadHandler.h" #include "cmDynamicLoader.h" #include "cmGeneratedFileStream.h" #include "cmGlobalGenerator.h" #include "cmJSONState.h" #include "cmList.h" #include "cmMakefile.h" #include "cmProcessOutput.h" #include "cmState.h" #include "cmStateSnapshot.h" #include "cmStateTypes.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 "cmXMLWriter.h" #include "cmake.h" #if defined(__BEOS__) || defined(__HAIKU__) # include /* disable_debugger() API. */ #endif struct cmCTest::Private { /** Representation of one part. */ struct PartInfo { void SetName(const std::string& name) { this->Name = name; } const std::string& 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; bool FlushTestProgressLine = false; bool ForceNewCTestProcess = false; bool RunConfigurationScript = false; // these are helper classes cmCTestBuildHandler BuildHandler; cmCTestBuildAndTestHandler BuildAndTestHandler; cmCTestCoverageHandler CoverageHandler; cmCTestScriptHandler ScriptHandler; cmCTestTestHandler TestHandler; cmCTestUpdateHandler UpdateHandler; cmCTestConfigureHandler ConfigureHandler; cmCTestMemCheckHandler MemCheckHandler; cmCTestSubmitHandler SubmitHandler; cmCTestUploadHandler UploadHandler; std::vector GetTestingHandlers() { return { &this->BuildHandler, &this->BuildAndTestHandler, &this->CoverageHandler, &this->ScriptHandler, &this->TestHandler, &this->UpdateHandler, &this->ConfigureHandler, &this->MemCheckHandler, &this->SubmitHandler, &this->UploadHandler }; } std::map GetNamedTestingHandlers() { return { { "build", &this->BuildHandler }, { "buildtest", &this->BuildAndTestHandler }, { "coverage", &this->CoverageHandler }, { "script", &this->ScriptHandler }, { "test", &this->TestHandler }, { "update", &this->UpdateHandler }, { "configure", &this->ConfigureHandler }, { "memcheck", &this->MemCheckHandler }, { "submit", &this->SubmitHandler }, { "upload", &this->UploadHandler } }; } 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(); 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; // By default we write output to the process output streams. std::ostream* StreamOut = &std::cout; std::ostream* StreamErr = &std::cerr; bool SuppressUpdatingCTestConfiguration = false; bool Debug = false; bool ShowLineNumbers = false; bool Quiet = false; std::string BuildID; std::vector InitialCommandLineArguments; int SubmitIndex = 0; std::unique_ptr OutputLogFile; int OutputLogFileLastTag = -1; bool OutputTestOutputOnTestFailure = false; bool OutputColorCode = cmCTest::ColoredOutputSupportedByConsole(); std::map Definitions; cmCTest::NoTests NoTestsMode = cmCTest::NoTests::Legacy; bool NoTestsModeSetInCli = false; }; struct tm* cmCTest::GetNightlyTime(std::string const& str, bool tomorrowtag) { struct tm* lctime; time_t tctime = time(nullptr); lctime = gmtime(&tctime); char buf[1024]; // add todays year day and month to the time in str because // curl_getdate no longer assumes the day is today snprintf(buf, sizeof(buf), "%d%02d%02d %s", lctime->tm_year + 1900, lctime->tm_mon + 1, lctime->tm_mday, str.c_str()); cmCTestLog(this, OUTPUT, "Determine Nightly Start Time" << std::endl << " Specified time: " << str << std::endl); // Convert the nightly start time to seconds. Since we are // providing only a time and a timezone, 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. time_t ntime = curl_getdate(buf, &tctime); cmCTestLog(this, DEBUG, " Get curl time: " << ntime << std::endl); tctime = time(nullptr); cmCTestLog(this, DEBUG, " Get the current time: " << tctime << std::endl); const int 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(const std::string& 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(const std::string& in) { std::string out; for (const char* 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(new Private) { 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); } 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; } for (auto& handler : this->Impl->GetTestingHandlers()) { handler->SetCTestInstance(this); } // 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(const std::string& 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; } int cmCTest::Initialize(const std::string& binary_dir, cmCTestStartCommand* command) { bool quiet = false; if (command && command->ShouldBeQuiet()) { quiet = true; } cmCTestOptionalLog(this, DEBUG, "Here: " << __LINE__ << std::endl, quiet); if (!this->Impl->InteractiveDebugMode) { this->BlockTestErrorDiagnostics(); } else { cmSystemTools::PutEnv("CTEST_INTERACTIVE_DEBUG_MODE=1"); } this->Impl->BinaryDir = binary_dir; cmSystemTools::ConvertToUnixSlashes(this->Impl->BinaryDir); this->UpdateCTestConfiguration(); cmCTestOptionalLog(this, DEBUG, "Here: " << __LINE__ << std::endl, quiet); if (this->Impl->ProduceXML) { cmCTestOptionalLog(this, DEBUG, "Here: " << __LINE__ << std::endl, quiet); cmCTestOptionalLog(this, OUTPUT, " Site: " << this->GetCTestConfiguration("Site") << std::endl << " Build name: " << cmCTest::SafeBuildIdField( this->GetCTestConfiguration("BuildName")) << std::endl, quiet); cmCTestOptionalLog(this, DEBUG, "Produce XML is on" << std::endl, quiet); if (this->Impl->TestModel == cmCTest::NIGHTLY && this->GetCTestConfiguration("NightlyStartTime").empty()) { cmCTestOptionalLog( this, WARNING, "WARNING: No nightly start time found please set in CTestConfig.cmake" " or DartConfig.cmake" << std::endl, quiet); cmCTestOptionalLog(this, DEBUG, "Here: " << __LINE__ << std::endl, quiet); return 0; } } cmake cm(cmake::RoleScript, cmState::CTest); cm.SetHomeDirectory(""); cm.SetHomeOutputDirectory(""); cm.GetCurrentSnapshot().SetDefaultDefinitions(); cmGlobalGenerator gg(&cm); cmMakefile mf(&gg, cm.GetCurrentSnapshot()); if (!this->ReadCustomConfigurationFileTree(this->Impl->BinaryDir, &mf)) { cmCTestOptionalLog( this, DEBUG, "Cannot find custom configuration file tree" << std::endl, quiet); return 0; } if (this->Impl->ProduceXML) { // Verify "Testing" directory exists: // std::string testingDir = this->Impl->BinaryDir + "/Testing"; 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 0; } } else { if (!cmSystemTools::MakeDirectory(testingDir)) { cmCTestLog(this, ERROR_MESSAGE, "Cannot create directory " << testingDir << std::endl); return 0; } } // Create new "TAG" file or read existing one: // bool createNewTag = true; if (command) { createNewTag = command->ShouldCreateNewTag(); } std::string tagfile = testingDir + "/TAG"; cmsys::ifstream tfin(tagfile.c_str()); std::string tag; if (createNewTag) { time_t tctime = time(nullptr); if (this->Impl->TomorrowTag) { tctime += (24 * 60 * 60); } struct tm* lctime = gmtime(&tctime); if (tfin && cmSystemTools::GetLineFromStream(tfin, tag)) { int year = 0; int mon = 0; int day = 0; int hour = 0; int min = 0; sscanf(tag.c_str(), "%04d%02d%02d-%02d%02d", &year, &mon, &day, &hour, &min); if (year != lctime->tm_year + 1900 || mon != lctime->tm_mon + 1 || day != lctime->tm_mday) { tag.clear(); } std::string group; if (cmSystemTools::GetLineFromStream(tfin, group) && !this->Impl->Parts[PartStart] && !command) { this->Impl->SpecificGroup = group; } std::string model; if (cmSystemTools::GetLineFromStream(tfin, model) && !this->Impl->Parts[PartStart] && !command) { this->Impl->TestModel = GetTestModelFromString(model); } tfin.close(); } if (tag.empty() || (nullptr != command) || this->Impl->Parts[PartStart]) { cmCTestOptionalLog( this, DEBUG, "TestModel: " << this->GetTestModelString() << std::endl, quiet); cmCTestOptionalLog(this, DEBUG, "TestModel: " << this->Impl->TestModel << std::endl, quiet); if (this->Impl->TestModel == cmCTest::NIGHTLY) { lctime = this->GetNightlyTime( this->GetCTestConfiguration("NightlyStartTime"), this->Impl->TomorrowTag); } 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); tag = datestring; cmsys::ofstream ofs(tagfile.c_str()); if (ofs) { ofs << tag << std::endl; ofs << this->GetTestModelString() << std::endl; switch (this->Impl->TestModel) { case cmCTest::EXPERIMENTAL: ofs << "Experimental" << std::endl; break; case cmCTest::NIGHTLY: ofs << "Nightly" << std::endl; break; case cmCTest::CONTINUOUS: ofs << "Continuous" << std::endl; break; } } ofs.close(); if (nullptr == command) { cmCTestOptionalLog(this, OUTPUT, "Create new tag: " << tag << " - " << this->GetTestModelString() << std::endl, quiet); } } } else { std::string group; std::string modelStr; int model = cmCTest::UNKNOWN; if (tfin) { cmSystemTools::GetLineFromStream(tfin, tag); cmSystemTools::GetLineFromStream(tfin, group); if (cmSystemTools::GetLineFromStream(tfin, modelStr)) { model = GetTestModelFromString(modelStr); } tfin.close(); } if (tag.empty()) { cmCTestLog(this, ERROR_MESSAGE, "Cannot read existing TAG file in " << testingDir << std::endl); return 0; } 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 0; } 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->GetTestModelString() << std::endl, quiet); } this->Impl->CurrentTag = tag; } return 1; } bool cmCTest::InitializeFromCommand(cmCTestStartCommand* command) { std::string src_dir = this->GetCTestConfiguration("SourceDirectory"); std::string bld_dir = this->GetCTestConfiguration("BuildDirectory"); this->Impl->BuildID = ""; for (Part p = PartStart; p != PartCount; p = static_cast(p + 1)) { this->Impl->Parts[p].SubmitFiles.clear(); } cmMakefile* mf = command->GetMakefile(); std::string fname; std::string src_dir_fname = cmStrCat(src_dir, "/CTestConfig.cmake"); cmSystemTools::ConvertToUnixSlashes(src_dir_fname); std::string bld_dir_fname = cmStrCat(bld_dir, "/CTestConfig.cmake"); cmSystemTools::ConvertToUnixSlashes(bld_dir_fname); if (cmSystemTools::FileExists(bld_dir_fname)) { fname = bld_dir_fname; } else if (cmSystemTools::FileExists(src_dir_fname)) { fname = src_dir_fname; } if (!fname.empty()) { cmCTestOptionalLog(this, OUTPUT, " Reading ctest configuration file: " << fname << std::endl, command->ShouldBeQuiet()); bool readit = mf->ReadDependentFile(fname); if (!readit) { std::string m = cmStrCat("Could not find include file: ", fname); command->SetError(m); return false; } } this->SetCTestConfigurationFromCMakeVariable(mf, "NightlyStartTime", "CTEST_NIGHTLY_START_TIME", command->ShouldBeQuiet()); this->SetCTestConfigurationFromCMakeVariable(mf, "Site", "CTEST_SITE", command->ShouldBeQuiet()); this->SetCTestConfigurationFromCMakeVariable( mf, "BuildName", "CTEST_BUILD_NAME", command->ShouldBeQuiet()); if (!this->Initialize(bld_dir, command)) { return false; } cmCTestOptionalLog(this, OUTPUT, " Use " << this->GetTestModelString() << " tag: " << this->GetCurrentTag() << std::endl, command->ShouldBeQuiet()); return true; } bool cmCTest::UpdateCTestConfiguration() { if (this->Impl->SuppressUpdatingCTestConfiguration) { return true; } 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::ChangeDirectory(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(const std::string& 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; } void cmCTest::Finalize() { } bool cmCTest::OpenOutputFile(const std::string& path, const std::string& 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.SetTempExt("tmp"); 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, const std::string& 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(const std::string& filename) { std::string testingDir = this->Impl->BinaryDir + "/Testing/" + this->Impl->CurrentTag + "/" + filename; return cmSystemTools::FileExists(testingDir); } cmCTestBuildHandler* cmCTest::GetBuildHandler() { return &this->Impl->BuildHandler; } cmCTestBuildAndTestHandler* cmCTest::GetBuildAndTestHandler() { return &this->Impl->BuildAndTestHandler; } cmCTestCoverageHandler* cmCTest::GetCoverageHandler() { return &this->Impl->CoverageHandler; } cmCTestScriptHandler* cmCTest::GetScriptHandler() { return &this->Impl->ScriptHandler; } cmCTestTestHandler* cmCTest::GetTestHandler() { return &this->Impl->TestHandler; } cmCTestUpdateHandler* cmCTest::GetUpdateHandler() { return &this->Impl->UpdateHandler; } cmCTestConfigureHandler* cmCTest::GetConfigureHandler() { return &this->Impl->ConfigureHandler; } cmCTestMemCheckHandler* cmCTest::GetMemCheckHandler() { return &this->Impl->MemCheckHandler; } cmCTestSubmitHandler* cmCTest::GetSubmitHandler() { return &this->Impl->SubmitHandler; } cmCTestUploadHandler* cmCTest::GetUploadHandler() { return &this->Impl->UploadHandler; } int cmCTest::ProcessSteps() { int res = 0; bool notest = true; int update_count = 0; for (Part p = PartStart; notest && p != PartCount; p = static_cast(p + 1)) { notest = !this->Impl->Parts[p]; } if (this->Impl->Parts[PartUpdate] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { cmCTestUpdateHandler* uphandler = this->GetUpdateHandler(); uphandler->SetPersistentOption( "SourceDirectory", this->GetCTestConfiguration("SourceDirectory")); update_count = uphandler->ProcessHandler(); if (update_count < 0) { res |= cmCTest::UPDATE_ERRORS; } } if (this->Impl->TestModel == cmCTest::CONTINUOUS && !update_count) { return 0; } if (this->Impl->Parts[PartConfigure] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { if (this->GetConfigureHandler()->ProcessHandler() < 0) { res |= cmCTest::CONFIGURE_ERRORS; } } if (this->Impl->Parts[PartBuild] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); if (this->GetBuildHandler()->ProcessHandler() < 0) { res |= cmCTest::BUILD_ERRORS; } } if ((this->Impl->Parts[PartTest] || notest) && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); if (this->GetTestHandler()->ProcessHandler() < 0) { res |= cmCTest::TEST_ERRORS; } } if (this->Impl->Parts[PartCoverage] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); if (this->GetCoverageHandler()->ProcessHandler() < 0) { res |= cmCTest::COVERAGE_ERRORS; } } if (this->Impl->Parts[PartMemCheck] && (this->GetRemainingTimeAllowed() > std::chrono::minutes(2))) { this->UpdateCTestConfiguration(); if (this->GetMemCheckHandler()->ProcessHandler() < 0) { res |= cmCTest::MEMORY_ERRORS; } } if (!notest) { 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++) { const char* 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(this->Impl->NotesFiles); } } if (this->Impl->Parts[PartSubmit]) { this->UpdateCTestConfiguration(); if (this->GetSubmitHandler()->ProcessHandler() < 0) { res |= cmCTest::SUBMIT_ERRORS; } } if (res != 0) { cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest" << std::endl); if (!this->Impl->OutputTestOutputOnTestFailure) { const std::string lastTestLog = this->GetBinaryDir() + "/Testing/Temporary/LastTest.log"; cmCTestLog(this, ERROR_MESSAGE, "Output from these tests are in: " << lastTestLog << std::endl); cmCTestLog(this, ERROR_MESSAGE, "Use \"--rerun-failed --output-on-failure\" to re-run the " "failed cases verbosely." << std::endl); } } return res; } std::string cmCTest::GetTestModelString() { if (!this->Impl->SpecificGroup.empty()) { return this->Impl->SpecificGroup; } switch (this->Impl->TestModel) { case cmCTest::NIGHTLY: return "Nightly"; case cmCTest::CONTINUOUS: return "Continuous"; } return "Experimental"; } int cmCTest::GetTestModelFromString(const std::string& 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(const std::string& command, std::string& output, int* retVal, const char* 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, cmCTestLogWrite(strdata.c_str(), strdata.size())); if (ofs) { ofs << cmCTestLogWrite(strdata.c_str(), strdata.size()); } }, [this, &processOutput, &output, &ofs]() { std::string strdata; processOutput.DecodeText(std::string(), strdata); if (!strdata.empty()) { output.append(strdata); cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, cmCTestLogWrite(strdata.c_str(), strdata.size())); if (ofs) { ofs << cmCTestLogWrite(strdata.c_str(), strdata.size()); } } }); 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; } // ###################################################################### // ###################################################################### // ###################################################################### // ###################################################################### bool cmCTest::RunTest(const std::vector& argv, std::string* output, int* retVal, std::ostream* log, cmDuration testTimeOut, std::vector* environment, Encoding encoding) { bool modifyEnv = (environment && !environment->empty()); // determine how much time we have cmDuration timeout = this->GetRemainingTimeAllowed(); if (timeout != cmCTest::MaxDuration()) { timeout -= std::chrono::minutes(2); } if (this->Impl->TimeOut > cmDuration::zero() && this->Impl->TimeOut < timeout) { timeout = this->Impl->TimeOut; } if (testTimeOut > cmDuration::zero() && testTimeOut < this->GetRemainingTimeAllowed()) { timeout = testTimeOut; } // always have at least 1 second if we got to here if (timeout <= cmDuration::zero()) { timeout = std::chrono::seconds(1); } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "Test timeout computed to be: " << (timeout == cmCTest::MaxDuration() ? std::string("infinite") : std::to_string(cmDurationTo(timeout))) << "\n"); if (cmSystemTools::SameFile(argv[0], cmSystemTools::GetCTestCommand()) && !this->Impl->ForceNewCTestProcess) { cmCTest inst; inst.Impl->ConfigType = this->Impl->ConfigType; inst.Impl->TimeOut = timeout; // Capture output of the child ctest. std::ostringstream oss; inst.SetStreams(&oss, &oss); std::vector args; for (auto const& i : argv) { // make sure we pass the timeout in for any build and test // invocations. Since --build-generator is required this is a // good place to check for it, and to add the arguments in if (i == "--build-generator" && timeout != cmCTest::MaxDuration() && timeout > cmDuration::zero()) { args.emplace_back("--test-timeout"); args.push_back(std::to_string(cmDurationTo(timeout))); } args.emplace_back(i); } if (log) { *log << "* Run internal CTest" << std::endl; } std::unique_ptr saveEnv; if (modifyEnv) { saveEnv = cm::make_unique(); cmSystemTools::AppendEnv(*environment); } *retVal = inst.Run(args, output); if (output) { *output += oss.str(); } if (log && output) { *log << *output; } if (output) { cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "Internal cmCTest object used to run test." << std::endl << *output << std::endl); } return true; } std::vector tempOutput; if (output) { output->clear(); } std::unique_ptr saveEnv; if (modifyEnv) { saveEnv = cm::make_unique(); cmSystemTools::AppendEnv(*environment); } cmUVProcessChainBuilder builder; builder.AddCommand(argv).SetMergedBuiltinStreams(); cmCTestLog(this, DEBUG, "Command is: " << argv[0] << std::endl); auto chain = builder.Start(); cmProcessOutput processOutput(encoding); cm::uv_pipe_ptr outputStream; outputStream.init(chain.GetLoop(), 0); uv_pipe_open(outputStream, chain.OutputStream()); auto outputHandle = cmUVStreamRead( outputStream, [this, &processOutput, &output, &tempOutput, &log](std::vector data) { std::string strdata; processOutput.DecodeText(data.data(), data.size(), strdata); if (output) { cm::append(tempOutput, data.data(), data.data() + data.size()); } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, cmCTestLogWrite(strdata.c_str(), strdata.size())); if (log) { log->write(strdata.c_str(), strdata.size()); } }, [this, &processOutput, &log]() { std::string strdata; processOutput.DecodeText(std::string(), strdata); if (!strdata.empty()) { cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, cmCTestLogWrite(strdata.c_str(), strdata.size())); if (log) { log->write(strdata.c_str(), strdata.size()); } } }); bool complete = chain.Wait(static_cast(timeout.count() * 1000.0)); processOutput.DecodeText(tempOutput, tempOutput); if (output && tempOutput.begin() != tempOutput.end()) { output->append(tempOutput.data(), tempOutput.size()); } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, "-- Process completed" << std::endl); bool result = false; if (complete) { auto const& status = chain.GetStatus(0); auto exception = status.GetException(); switch (exception.first) { case cmUVProcessChain::ExceptionCode::None: *retVal = static_cast(status.ExitStatus); if (*retVal != 0 && this->Impl->OutputTestOutputOnTestFailure) { this->OutputTestErrors(tempOutput); } result = true; break; case cmUVProcessChain::ExceptionCode::Spawn: { std::string outerr = cmStrCat("\n*** ERROR executing: ", exception.second); if (output) { *output += outerr; } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, outerr << std::endl); } break; default: { if (this->Impl->OutputTestOutputOnTestFailure) { this->OutputTestErrors(tempOutput); } *retVal = status.TermSignal; std::string outerr = cmStrCat("\n*** Exception executing: ", exception.second); if (output) { *output += outerr; } cmCTestLog(this, HANDLER_VERBOSE_OUTPUT, outerr << std::endl); } break; } } return result; } std::string cmCTest::SafeBuildIdField(const std::string& value) { std::string safevalue(value); if (!safevalue.empty()) { // Disallow non-filename and non-space whitespace characters. // If they occur, replace them with "" // const char* disallowed = "\\:*?\"<>|\n\r\t\f\v"; if (safevalue.find_first_of(disallowed) != std::string::npos) { std::string::size_type i = 0; std::string::size_type n = strlen(disallowed); char replace[2]; replace[1] = 0; for (i = 0; i < n; ++i) { replace[0] = disallowed[i]; cmSystemTools::ReplaceString(safevalue, replace, ""); } } } if (safevalue.empty()) { safevalue = "(empty)"; } return safevalue; } void cmCTest::StartXML(cmXMLWriter& xml, 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->GetTestModelString()); 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("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); } void cmCTest::AddSiteProperties(cmXMLWriter& xml) { cmCTestScriptHandler* ch = this->GetScriptHandler(); cmake* cm = ch->GetCMake(); // if no CMake then this is the old style script and props like // this will not work anyway. if (!cm) { return; } // 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 = ch->GetCMake()->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, 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->GetTestModelString()); xml.Attribute("Name", this->GetCTestConfiguration("Site")); xml.Attribute("Generator", std::string("ctest-") + cmVersion::GetCMakeVersion()); this->AddSiteProperties(xml); 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(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, files); return 0; } int cmCTest::GenerateNotesFile(const std::string& 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(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; } bool cmCTest::TryToChangeDirectory(std::string const& dir) { cmCTestLog(this, OUTPUT, "Internal ctest changing into directory: " << dir << std::endl); cmsys::Status status = cmSystemTools::ChangeDirectory(dir); if (!status) { auto msg = "Failed to change working directory to \"" + dir + "\" : " + status.GetString() + "\n"; cmCTestLog(this, ERROR_MESSAGE, msg); return false; } return true; } std::string cmCTest::Base64GzipEncodeFile(std::string const& file) { const std::string currDir = cmSystemTools::GetCurrentWorkingDirectory(); std::string parentDir = cmSystemTools::GetParentDirectory(file); // Temporarily change to the file's directory so the tar gets created // with a flat directory structure. if (currDir != parentDir) { if (!this->TryToChangeDirectory(parentDir)) { 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); // Change back to the directory we started in. if (currDir != parentDir) { cmSystemTools::ChangeDirectory(currDir); } 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(const std::string& 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& 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& val) { cmCTestLog(this, ERROR_MESSAGE, "CTest -D called with incorrect option: " << val << std::endl); cmCTestLog( this, ERROR_MESSAGE, "Available options are:" << std::endl << " ctest -D Continuous" << std::endl << " ctest -D Continuous(Start|Update|Configure|Build)" << std::endl << " ctest -D Continuous(Test|Coverage|MemCheck|Submit)" << std::endl << " ctest -D Experimental" << std::endl << " ctest -D Experimental(Start|Update|Configure|Build)" << std::endl << " ctest -D Experimental(Test|Coverage|MemCheck|Submit)" << std::endl << " ctest -D Nightly" << std::endl << " ctest -D Nightly(Start|Update|Configure|Build)" << std::endl << " ctest -D Nightly(Test|Coverage|MemCheck|Submit)" << std::endl << " ctest -D NightlyMemoryCheck" << std::endl); } bool cmCTest::CheckArgument(const std::string& arg, cm::string_view varg1, const char* varg2) { return (arg == varg1) || (varg2 && arg == varg2); } // Processes one command line argument (and its arguments if any) // for many simple options and then returns bool cmCTest::HandleCommandLineArguments(size_t& i, std::vector& args, std::string& errormsg) { std::string arg = args[i]; cm::string_view noTestsPrefix = "--no-tests="; if (this->CheckArgument(arg, "-F"_s)) { this->Impl->Failover = true; } else if (this->CheckArgument(arg, "-j"_s, "--parallel")) { cm::optional parallelLevel; // No value or an empty value tells ctest to choose a default. if (i + 1 < args.size() && !cmHasLiteralPrefix(args[i + 1], "-")) { ++i; if (!args[i].empty()) { // A non-empty value must be a non-negative integer. unsigned long plevel = 0; if (!cmStrToULong(args[i], &plevel)) { errormsg = cmStrCat("'", arg, "' given invalid value '", args[i], "'"); return false; } parallelLevel = plevel; } } this->SetParallelLevel(parallelLevel); this->Impl->ParallelLevelSetInCli = true; } else if (cmHasPrefix(arg, "-j")) { // The value must be a non-negative integer. unsigned long plevel = 0; if (!cmStrToULong(arg.substr(2), &plevel)) { errormsg = cmStrCat("'", arg, "' given invalid value '", args[i], "'"); return false; } this->SetParallelLevel(plevel); this->Impl->ParallelLevelSetInCli = true; } else if (this->CheckArgument(arg, "--repeat-until-fail"_s)) { if (i >= args.size() - 1) { errormsg = "'--repeat-until-fail' requires an argument"; return false; } if (this->Impl->RepeatMode != cmCTest::Repeat::Never) { errormsg = "At most one '--repeat' option may be used."; return false; } i++; long repeat = 1; if (!cmStrToLong(args[i], &repeat)) { errormsg = cmStrCat("'--repeat-until-fail' given non-integer value '", args[i], "'"); return false; } this->Impl->RepeatCount = static_cast(repeat); if (repeat > 1) { this->Impl->RepeatMode = cmCTest::Repeat::UntilFail; } } else if (this->CheckArgument(arg, "--repeat"_s)) { if (i >= args.size() - 1) { errormsg = "'--repeat' requires an argument"; return false; } if (this->Impl->RepeatMode != cmCTest::Repeat::Never) { errormsg = "At most one '--repeat' option may be used."; return false; } i++; cmsys::RegularExpression repeatRegex( "^(until-fail|until-pass|after-timeout):([0-9]+)$"); if (repeatRegex.find(args[i])) { 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 { errormsg = cmStrCat("'--repeat' given invalid value '", args[i], "'"); return false; } } else if (this->CheckArgument(arg, "--test-load"_s) && i < args.size() - 1) { i++; unsigned long load; if (cmStrToULong(args[i], &load)) { this->SetTestLoad(load); } else { cmCTestLog(this, WARNING, "Invalid value for 'Test Load' : " << args[i] << std::endl); } } else if (this->CheckArgument(arg, "--no-compress-output"_s)) { this->Impl->CompressTestOutput = false; } else if (this->CheckArgument(arg, "--print-labels"_s)) { this->Impl->PrintLabels = true; } else if (this->CheckArgument(arg, "--http1.0"_s)) { this->Impl->UseHTTP10 = true; } else if (this->CheckArgument(arg, "--timeout"_s) && i < args.size() - 1) { i++; auto timeout = cmDuration(atof(args[i].c_str())); this->Impl->GlobalTimeout = timeout; } else if (this->CheckArgument(arg, "--stop-time"_s) && i < args.size() - 1) { i++; this->SetStopTime(args[i]); } else if (this->CheckArgument(arg, "--stop-on-failure"_s)) { this->Impl->StopOnFailure = true; } else if (this->CheckArgument(arg, "-C"_s, "--build-config") && i < args.size() - 1) { i++; this->SetConfigType(args[i]); } else if (this->CheckArgument(arg, "--debug"_s)) { this->Impl->Debug = true; this->Impl->ShowLineNumbers = true; } else if ((this->CheckArgument(arg, "--group"_s) || // This is an undocumented / deprecated option. // "Track" has been renamed to "Group". this->CheckArgument(arg, "--track"_s)) && i < args.size() - 1) { i++; this->Impl->SpecificGroup = args[i]; } else if (this->CheckArgument(arg, "--show-line-numbers"_s)) { this->Impl->ShowLineNumbers = true; } else if (this->CheckArgument(arg, "--no-label-summary"_s)) { this->Impl->LabelSummary = false; } else if (this->CheckArgument(arg, "--no-subproject-summary"_s)) { this->Impl->SubprojectSummary = false; } else if (this->CheckArgument(arg, "-Q"_s, "--quiet")) { this->Impl->Quiet = true; } else if (this->CheckArgument(arg, "--progress"_s)) { this->Impl->TestProgressOutput = true; } else if (this->CheckArgument(arg, "-V"_s, "--verbose")) { this->Impl->Verbose = true; } else if (this->CheckArgument(arg, "-VV"_s, "--extra-verbose")) { this->Impl->ExtraVerbose = true; this->Impl->Verbose = true; } else if (this->CheckArgument(arg, "--output-on-failure"_s)) { this->Impl->OutputTestOutputOnTestFailure = true; } else if (this->CheckArgument(arg, "--test-output-size-passed"_s) && i < args.size() - 1) { i++; long outputSize; if (cmStrToLong(args[i], &outputSize)) { this->Impl->TestHandler.SetTestOutputSizePassed( static_cast(outputSize)); } else { cmCTestLog(this, WARNING, "Invalid value for '--test-output-size-passed': " << args[i] << "\n"); } } else if (this->CheckArgument(arg, "--test-output-size-failed"_s) && i < args.size() - 1) { i++; long outputSize; if (cmStrToLong(args[i], &outputSize)) { this->Impl->TestHandler.SetTestOutputSizeFailed( static_cast(outputSize)); } else { cmCTestLog(this, WARNING, "Invalid value for '--test-output-size-failed': " << args[i] << "\n"); } } else if (this->CheckArgument(arg, "--test-output-truncation"_s) && i < args.size() - 1) { i++; if (!this->Impl->TestHandler.SetTestOutputTruncation(args[i])) { errormsg = "Invalid value for '--test-output-truncation': " + args[i]; return false; } } else if (this->CheckArgument(arg, "-N"_s, "--show-only")) { this->Impl->ShowOnly = true; } else if (cmHasLiteralPrefix(arg, "--show-only=")) { this->Impl->ShowOnly = true; // Check if a specific format is requested. Defaults to human readable // text. std::string argWithFormat = "--show-only="; std::string format = arg.substr(argWithFormat.length()); 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") { errormsg = "'--show-only=' given unknown value '" + format + "'"; return false; } } else if (this->CheckArgument(arg, "-O"_s, "--output-log") && i < args.size() - 1) { i++; this->SetOutputLogFileName(args[i]); } else if (this->CheckArgument(arg, "--tomorrow-tag"_s)) { this->Impl->TomorrowTag = true; } else if (this->CheckArgument(arg, "--force-new-ctest-process"_s)) { this->Impl->ForceNewCTestProcess = true; } else if (this->CheckArgument(arg, "-W"_s, "--max-width") && i < args.size() - 1) { i++; this->Impl->MaxTestNameWidth = atoi(args[i].c_str()); } else if (this->CheckArgument(arg, "--interactive-debug-mode"_s) && i < args.size() - 1) { i++; this->Impl->InteractiveDebugMode = cmIsOn(args[i]); } else if (this->CheckArgument(arg, "--submit-index"_s) && i < args.size() - 1) { i++; this->Impl->SubmitIndex = atoi(args[i].c_str()); if (this->Impl->SubmitIndex < 0) { this->Impl->SubmitIndex = 0; } } else if (this->CheckArgument(arg, "--overwrite"_s) && i < args.size() - 1) { i++; this->AddCTestConfigurationOverwrite(args[i]); } else if (this->CheckArgument(arg, "-A"_s, "--add-notes") && i < args.size() - 1) { this->Impl->ProduceXML = true; this->SetTest("Notes"); i++; this->SetNotesFiles(args[i]); return true; } else if (this->CheckArgument(arg, "--test-dir"_s)) { if (i >= args.size() - 1) { errormsg = "'--test-dir' requires an argument"; return false; } i++; this->Impl->TestDir = std::string(args[i]); } else if (this->CheckArgument(arg, "--output-junit"_s)) { if (i >= args.size() - 1) { errormsg = "'--output-junit' requires an argument"; return false; } i++; this->SetOutputJUnitFileName(std::string(args[i])); } else if (cmHasPrefix(arg, noTestsPrefix)) { cm::string_view noTestsMode = cm::string_view(arg).substr(noTestsPrefix.length()); if (noTestsMode == "error") { this->Impl->NoTestsMode = cmCTest::NoTests::Error; } else if (noTestsMode != "ignore") { errormsg = cmStrCat("'--no-tests=' given unknown value '", noTestsMode, '\''); return false; } else { this->Impl->NoTestsMode = cmCTest::NoTests::Ignore; } this->Impl->NoTestsModeSetInCli = true; } // options that control what tests are run else if (this->CheckArgument(arg, "-I"_s, "--tests-information") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("TestsToRunInformation", args[i]); this->GetMemCheckHandler()->SetPersistentOption("TestsToRunInformation", args[i]); } else if (this->CheckArgument(arg, "-U"_s, "--union")) { this->GetTestHandler()->SetPersistentOption("UseUnion", "true"); this->GetMemCheckHandler()->SetPersistentOption("UseUnion", "true"); } else if (this->CheckArgument(arg, "-R"_s, "--tests-regex") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("IncludeRegularExpression", args[i]); this->GetMemCheckHandler()->SetPersistentOption("IncludeRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-L"_s, "--label-regex") && i < args.size() - 1) { i++; this->GetTestHandler()->AddPersistentMultiOption("LabelRegularExpression", args[i]); this->GetMemCheckHandler()->AddPersistentMultiOption( "LabelRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-LE"_s, "--label-exclude") && i < args.size() - 1) { i++; this->GetTestHandler()->AddPersistentMultiOption( "ExcludeLabelRegularExpression", args[i]); this->GetMemCheckHandler()->AddPersistentMultiOption( "ExcludeLabelRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-E"_s, "--exclude-regex") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("ExcludeRegularExpression", args[i]); this->GetMemCheckHandler()->SetPersistentOption("ExcludeRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-FA"_s, "--fixture-exclude-any") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption( "ExcludeFixtureRegularExpression", args[i]); this->GetMemCheckHandler()->SetPersistentOption( "ExcludeFixtureRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-FS"_s, "--fixture-exclude-setup") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption( "ExcludeFixtureSetupRegularExpression", args[i]); this->GetMemCheckHandler()->SetPersistentOption( "ExcludeFixtureSetupRegularExpression", args[i]); } else if (this->CheckArgument(arg, "-FC"_s, "--fixture-exclude-cleanup") && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption( "ExcludeFixtureCleanupRegularExpression", args[i]); this->GetMemCheckHandler()->SetPersistentOption( "ExcludeFixtureCleanupRegularExpression", args[i]); } else if (this->CheckArgument(arg, "--resource-spec-file"_s) && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("ResourceSpecFile", args[i]); this->GetMemCheckHandler()->SetPersistentOption("ResourceSpecFile", args[i]); } else if (this->CheckArgument(arg, "--tests-from-file"_s) && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("TestListFile", args[i]); this->GetMemCheckHandler()->SetPersistentOption("TestListFile", args[i]); } else if (this->CheckArgument(arg, "--exclude-from-file"_s) && i < args.size() - 1) { i++; this->GetTestHandler()->SetPersistentOption("ExcludeTestListFile", args[i]); this->GetMemCheckHandler()->SetPersistentOption("ExcludeTestListFile", args[i]); } else if (this->CheckArgument(arg, "--rerun-failed"_s)) { this->GetTestHandler()->SetPersistentOption("RerunFailed", "true"); this->GetMemCheckHandler()->SetPersistentOption("RerunFailed", "true"); } else { return false; } return true; } #if !defined(_WIN32) bool cmCTest::ConsoleIsNotDumb() { std::string term_env_variable; if (cmSystemTools::GetEnv("TERM", term_env_variable)) { return isatty(1) && term_env_variable != "dumb"; } return false; } #endif bool cmCTest::ProgressOutputSupportedByConsole() { #if defined(_WIN32) // On Windows we need a console buffer. void* console = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO csbi; return GetConsoleScreenBufferInfo(console, &csbi); #else // On UNIX we need a non-dumb tty. return ConsoleIsNotDumb(); #endif } 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; } #if defined(_WIN32) // Not supported on Windows return false; #else // On UNIX we need a non-dumb tty. return ConsoleIsNotDumb(); #endif } // handle the -S -SR and -SP arguments bool cmCTest::HandleScriptArguments(size_t& i, std::vector& args, bool& SRArgumentSpecified) { std::string arg = args[i]; if (this->CheckArgument(arg, "-SP"_s, "--script-new-process") && i < args.size() - 1) { this->Impl->RunConfigurationScript = true; i++; cmCTestScriptHandler* ch = this->GetScriptHandler(); // -SR is an internal argument, -SP should be ignored when it is passed if (!SRArgumentSpecified) { ch->AddConfigurationScript(args[i], false); } } else if (this->CheckArgument(arg, "-SR"_s, "--script-run") && i < args.size() - 1) { SRArgumentSpecified = true; this->Impl->RunConfigurationScript = true; i++; cmCTestScriptHandler* ch = this->GetScriptHandler(); ch->AddConfigurationScript(args[i], true); } else if (this->CheckArgument(arg, "-S"_s, "--script") && i < args.size() - 1) { this->Impl->RunConfigurationScript = true; i++; cmCTestScriptHandler* ch = this->GetScriptHandler(); // -SR is an internal argument, -S should be ignored when it is passed if (!SRArgumentSpecified) { ch->AddConfigurationScript(args[i], true); } } else { return false; } return true; } bool cmCTest::AddVariableDefinition(const std::string& 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; } void cmCTest::SetPersistentOptionIfNotEmpty(const std::string& value, const std::string& optionName) { if (!value.empty()) { this->GetTestHandler()->SetPersistentOption(optionName, value); this->GetMemCheckHandler()->SetPersistentOption(optionName, value); } } void cmCTest::AddPersistentMultiOptionIfNotEmpty(const std::string& value, const std::string& optionName) { if (!value.empty()) { this->GetTestHandler()->AddPersistentMultiOption(optionName, value); this->GetMemCheckHandler()->AddPersistentMultiOption(optionName, value); } } bool cmCTest::SetArgsFromPreset(const std::string& presetName, bool listPresets) { const auto workingDirectory = cmSystemTools::GetCurrentWorkingDirectory(); cmCMakePresetsGraph settingsFile; auto result = settingsFile.ReadProjectPresets(workingDirectory); if (result != true) { cmSystemTools::Error(cmStrCat("Could not read presets from ", workingDirectory, ":", 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 (const auto& kvp : expandedPreset->OverwriteConfigurationFile) { this->AddCTestConfigurationOverwrite(kvp); } if (expandedPreset->Output) { this->Impl->TestProgressOutput = expandedPreset->Output->ShortProgress.value_or(false); if (expandedPreset->Output->Verbosity) { const auto& 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->ShowLineNumbers = 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->TestHandler.SetTestOutputSizePassed( *expandedPreset->Output->MaxPassedTestOutputSize); } if (expandedPreset->Output->MaxFailedTestOutputSize) { this->Impl->TestHandler.SetTestOutputSizeFailed( *expandedPreset->Output->MaxFailedTestOutputSize); } if (expandedPreset->Output->TestOutputTruncation) { this->Impl->TestHandler.TestOutputTruncation = *expandedPreset->Output->TestOutputTruncation; } if (expandedPreset->Output->MaxTestNameWidth) { this->Impl->MaxTestNameWidth = *expandedPreset->Output->MaxTestNameWidth; } } if (expandedPreset->Filter) { if (expandedPreset->Filter->Include) { this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Include->Name, "IncludeRegularExpression"); this->AddPersistentMultiOptionIfNotEmpty( expandedPreset->Filter->Include->Label, "LabelRegularExpression"); if (expandedPreset->Filter->Include->Index) { if (expandedPreset->Filter->Include->Index->IndexFile.empty()) { const auto& start = expandedPreset->Filter->Include->Index->Start; const auto& end = expandedPreset->Filter->Include->Index->End; const auto& 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->SetPersistentOptionIfNotEmpty(indexOptions, "TestsToRunInformation"); } else { this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Include->Index->IndexFile, "TestsToRunInformation"); } } if (expandedPreset->Filter->Include->UseUnion.value_or(false)) { this->GetTestHandler()->SetPersistentOption("UseUnion", "true"); this->GetMemCheckHandler()->SetPersistentOption("UseUnion", "true"); } } if (expandedPreset->Filter->Exclude) { this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Exclude->Name, "ExcludeRegularExpression"); this->AddPersistentMultiOptionIfNotEmpty( expandedPreset->Filter->Exclude->Label, "ExcludeLabelRegularExpression"); if (expandedPreset->Filter->Exclude->Fixtures) { this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Exclude->Fixtures->Any, "ExcludeFixtureRegularExpression"); this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Exclude->Fixtures->Setup, "ExcludeFixtureSetupRegularExpression"); this->SetPersistentOptionIfNotEmpty( expandedPreset->Filter->Exclude->Fixtures->Cleanup, "ExcludeFixtureCleanupRegularExpression"); } } } 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->SetPersistentOptionIfNotEmpty( expandedPreset->Execution->ResourceSpecFile, "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& args, std::string* output) { const char* ctestExec = "ctest"; bool cmakeAndTest = false; bool executeTests = true; bool SRArgumentSpecified = false; // 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 presetName = it->substr(9); success = this->SetArgsFromPreset(presetName, listPresets); } else if (++it != args.end()) { auto 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; } } // process the command line arguments for (size_t i = 1; i < args.size(); ++i) { // handle the simple commandline arguments std::string errormsg; bool validArg = this->HandleCommandLineArguments(i, args, errormsg); if (!validArg && !errormsg.empty()) { cmSystemTools::Error(errormsg); return 1; } std::string arg = args[i]; // handle the script arguments -S -SR -SP validArg = validArg || this->HandleScriptArguments(i, args, SRArgumentSpecified); // --dashboard: handle a request for a dashboard if (this->CheckArgument(arg, "-D"_s, "--dashboard") && i < args.size() - 1) { this->Impl->ProduceXML = true; i++; std::string targ = args[i]; // AddTestsForDashboard parses the dashboard type and converts it // into the separate stages if (!this->AddTestsForDashboardType(targ)) { if (!this->AddVariableDefinition(targ)) { this->ErrorMessageUnknownDashDValue(targ); executeTests = false; } } validArg = true; } // If it's not exactly -D, but it starts with -D, then try to parse out // a variable definition from it, same as CMake does. Unsuccessful // attempts are simply ignored since previous ctest versions ignore // this too. (As well as many other unknown command line args.) // if (arg != "-D" && cmHasLiteralPrefix(arg, "-D")) { std::string input = arg.substr(2); this->AddVariableDefinition(input); validArg = true; } // --test-action: calls SetTest(, /*report=*/ false) to enable // the corresponding stage if (!this->HandleTestActionArgument(ctestExec, i, args, validArg)) { executeTests = false; } // --test-model: what type of test model if (!this->HandleTestModelArgument(ctestExec, i, args, validArg)) { executeTests = false; } // --extra-submit if (this->CheckArgument(arg, "--extra-submit"_s) && i < args.size() - 1) { this->Impl->ProduceXML = true; this->SetTest("Submit"); i++; if (!this->SubmitExtraFiles(args[i])) { return 0; } validArg = true; } // --build-and-test options if (this->CheckArgument(arg, "--build-and-test"_s) && i < args.size() - 1) { cmakeAndTest = true; validArg = true; } // --schedule-random if (this->CheckArgument(arg, "--schedule-random"_s)) { this->Impl->ScheduleType = "Random"; validArg = true; } // pass the argument to all the handlers as well, but it may no longer be // set to what it was originally so I'm not sure this is working as // intended for (auto& handler : this->Impl->GetTestingHandlers()) { if (!handler->ProcessCommandLineArguments(arg, i, args, validArg)) { cmCTestLog( this, ERROR_MESSAGE, "Problem parsing command line arguments within a handler\n"); return 0; } } if (!validArg && cmHasLiteralPrefix(arg, "-") && !cmHasLiteralPrefix(arg, "--preset")) { cmSystemTools::Error(cmStrCat("Unknown argument: ", arg)); cmSystemTools::Error("Run 'ctest --help' for all supported options."); return 1; } } // the close of the for argument loop // 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(output); } if (executeTests) { return this->ExecuteTests(); } return 1; } bool cmCTest::HandleTestActionArgument(const char* ctestExec, size_t& i, const std::vector& args, bool& validArg) { bool success = true; std::string const& arg = args[i]; if (this->CheckArgument(arg, "-T"_s, "--test-action") && (i < args.size() - 1)) { validArg = true; this->Impl->ProduceXML = true; i++; if (!this->SetTest(args[i], false)) { success = false; cmCTestLog(this, ERROR_MESSAGE, "CTest -T called with incorrect option: " << args[i] << std::endl); cmCTestLog(this, ERROR_MESSAGE, "Available options are:" << std::endl << " " << ctestExec << " -T all" << std::endl << " " << ctestExec << " -T start" << std::endl << " " << ctestExec << " -T update" << std::endl << " " << ctestExec << " -T configure" << std::endl << " " << ctestExec << " -T build" << std::endl << " " << ctestExec << " -T test" << std::endl << " " << ctestExec << " -T coverage" << std::endl << " " << ctestExec << " -T memcheck" << std::endl << " " << ctestExec << " -T notes" << std::endl << " " << ctestExec << " -T submit" << std::endl); } } return success; } bool cmCTest::HandleTestModelArgument(const char* ctestExec, size_t& i, const std::vector& args, bool& validArg) { bool success = true; std::string const& arg = args[i]; if (this->CheckArgument(arg, "-M"_s, "--test-model") && (i < args.size() - 1)) { validArg = true; i++; std::string const& str = args[i]; if (cmSystemTools::LowerCase(str) == "nightly"_s) { this->SetTestModel(cmCTest::NIGHTLY); } else if (cmSystemTools::LowerCase(str) == "continuous"_s) { this->SetTestModel(cmCTest::CONTINUOUS); } else if (cmSystemTools::LowerCase(str) == "experimental"_s) { this->SetTestModel(cmCTest::EXPERIMENTAL); } else { success = false; cmCTestLog(this, ERROR_MESSAGE, "CTest -M called with incorrect option: " << str << std::endl); cmCTestLog(this, ERROR_MESSAGE, "Available options are:" << std::endl << " " << ctestExec << " -M Continuous" << std::endl << " " << ctestExec << " -M Experimental" << std::endl << " " << ctestExec << " -M Nightly" << std::endl); } } return success; } int cmCTest::ExecuteTests() { int res; // call process directory if (this->Impl->RunConfigurationScript) { if (this->Impl->ExtraVerbose) { cmCTestLog(this, OUTPUT, "* Extra verbosity turned on" << std::endl); } for (auto& handler : this->Impl->GetTestingHandlers()) { handler->SetVerbose(this->Impl->ExtraVerbose); handler->SetSubmitIndex(this->Impl->SubmitIndex); } this->GetScriptHandler()->SetVerbose(this->Impl->Verbose); res = this->GetScriptHandler()->ProcessHandler(); if (res != 0) { cmCTestLog(this, DEBUG, "running script failing returning: " << res << std::endl); } } else { // What is this? -V seems to be the same as -VV, // and Verbose is always on in this case this->Impl->ExtraVerbose = this->Impl->Verbose; this->Impl->Verbose = true; for (auto& handler : this->Impl->GetTestingHandlers()) { handler->SetVerbose(this->Impl->Verbose); handler->SetSubmitIndex(this->Impl->SubmitIndex); } const std::string currDir = cmSystemTools::GetCurrentWorkingDirectory(); std::string workDir = currDir; if (!this->Impl->TestDir.empty()) { workDir = cmSystemTools::CollapseFullPath(this->Impl->TestDir); } if (currDir != workDir) { if (!this->TryToChangeDirectory(workDir)) { return 1; } } if (!this->Initialize(workDir, nullptr)) { res = 12; cmCTestLog(this, ERROR_MESSAGE, "Problem initializing the dashboard." << std::endl); } else { res = this->ProcessSteps(); } this->Finalize(); if (currDir != workDir) { cmSystemTools::ChangeDirectory(currDir); } } if (res != 0) { cmCTestLog(this, DEBUG, "Running a test(s) failed returning : " << res << std::endl); } return res; } int cmCTest::RunCMakeAndTest(std::string* output) { this->Impl->Verbose = true; cmCTestBuildAndTestHandler* handler = this->GetBuildAndTestHandler(); int retv = handler->ProcessHandler(); *output = handler->GetOutput(); #ifndef CMAKE_BOOTSTRAP cmDynamicLoader::FlushCache(); #endif if (retv != 0) { cmCTestLog(this, DEBUG, "build and test failing returning: " << retv << std::endl); } return retv; } void cmCTest::SetNotesFiles(const std::string& 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 = curl_getdate(buf, ¤t_time); 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); } } std::string cmCTest::GetScheduleType() const { return this->Impl->ScheduleType; } void cmCTest::SetScheduleType(std::string const& type) { this->Impl->ScheduleType = type; } int cmCTest::ReadCustomConfigurationFileTree(const std::string& dir, cmMakefile* mf) { bool found = false; cmCTestLog(this, DEBUG, "* Read custom CTest configuration directory: " << dir << std::endl); std::string fname = cmStrCat(dir, "/CTestCustom.cmake"); cmCTestLog(this, DEBUG, "* Check for file: " << fname << std::endl); if (cmSystemTools::FileExists(fname)) { 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); } found = true; if (erroroc) { cmSystemTools::SetErrorOccurred(); } } std::string rexpr = cmStrCat(dir, "/CTestCustom.ctest"); cmCTestLog(this, DEBUG, "* Check for file: " << rexpr << std::endl); if (!found && cmSystemTools::FileExists(rexpr)) { cmsys::Glob gl; gl.RecurseOn(); gl.FindFiles(rexpr); std::vector& files = gl.GetFiles(); for (const std::string& file : files) { cmCTestLog(this, DEBUG, "* Read custom CTest configuration file: " << file << std::endl); if (!mf->ReadListFile(file) || cmSystemTools::GetErrorOccurredFlag()) { cmCTestLog(this, ERROR_MESSAGE, "Problem reading custom configuration: " << file << std::endl); } } found = true; } if (found) { for (auto& handler : this->Impl->GetNamedTestingHandlers()) { cmCTestLog(this, DEBUG, "* Read custom CTest configuration vectors for handler: " << handler.first << " (" << handler.second << ")" << std::endl); handler.second->PopulateCustomVectors(mf); } } return 1; } void cmCTest::PopulateCustomVector(cmMakefile* mf, const std::string& 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, const std::string& def, int& val) { cmValue dval = mf->GetDefinition(def); if (!dval) { return; } val = atoi(dval->c_str()); } std::string cmCTest::GetShortPathToFile(const std::string& cfname) { const std::string& sourceDir = cmSystemTools::CollapseFullPath( this->GetCTestConfiguration("SourceDirectory")); const std::string& buildDir = cmSystemTools::CollapseFullPath( 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(const std::string& 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(const char* name, const std::string& 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; } const char* cmCTest::GetSpecificGroup() { if (this->Impl->SpecificGroup.empty()) { return nullptr; } return this->Impl->SpecificGroup.c_str(); } void cmCTest::SetSpecificGroup(const char* 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; } void cmCTest::SetStreams(std::ostream* out, std::ostream* err) { this->Impl->StreamOut = out; this->Impl->StreamErr = err; } bool cmCTest::GetLabelSummary() const { return this->Impl->LabelSummary; } bool cmCTest::GetSubprojectSummary() const { return this->Impl->SubprojectSummary; } bool cmCTest::GetOutputTestOutputOnTestFailure() const { return this->Impl->OutputTestOutputOnTestFailure; } const std::map& 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(const std::string& id) { this->Impl->BuildID = id; } std::string cmCTest::GetBuildID() const { return this->Impl->BuildID; } void cmCTest::AddSubmitFile(Part part, const std::string& 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::SetSuppressUpdatingCTestConfiguration(bool val) { this->Impl->SuppressUpdatingCTestConfiguration = val; } void cmCTest::AddCTestConfigurationOverwrite(const std::string& overStr) { size_t epos = overStr.find('='); if (epos == std::string::npos) { cmCTestLog(this, ERROR_MESSAGE, "CTest configuration overwrite specified in the wrong format." << std::endl << "Valid format is: --overwrite key=value" << std::endl << "The specified was: --overwrite " << overStr << std::endl); return; } std::string key = overStr.substr(0, epos); std::string value = overStr.substr(epos + 1); this->Impl->CTestConfigurationOverwrites[key] = value; } void cmCTest::SetConfigType(const std::string& 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, const char* dconfig, const std::string& 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; } bool cmCTest::RunCommand(std::vector const& args, std::string* stdOut, std::string* stdErr, int* retVal, const char* 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) { const char* 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(const std::string& name) { if (!name.empty()) { this->Impl->OutputLogFile = cm::make_unique(name); } else { this->Impl->OutputLogFile.reset(); } } void cmCTest::SetOutputJUnitFileName(const std::string& name) { this->Impl->TestHandler.SetJUnitXMLFileName(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 const char* cmCTestStringLogType[] = { "DEBUG", "OUTPUT", "HANDLER_OUTPUT", "HANDLER_PROGRESS_OUTPUT", "HANDLER_TEST_PROGRESS_OUTPUT", "HANDLER_VERBOSE_OUTPUT", "WARNING", "ERROR_MESSAGE", nullptr }; #define cmCTestLogOutputFileLine(stream) \ do { \ if (this->Impl->ShowLineNumbers) { \ (stream) << std::endl << file << ":" << line << " "; \ } \ } while (false) void cmCTest::Log(int logType, const char* file, int line, const char* msg, bool suppress) { if (!msg || !*msg) { 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) { cmCTestLogOutputFileLine(*this->Impl->OutputLogFile); if (logType != this->Impl->OutputLogFileLastTag) { *this->Impl->OutputLogFile << "["; if (logType >= OTHER || logType < 0) { *this->Impl->OutputLogFile << "OTHER"; } else { *this->Impl->OutputLogFile << cmCTestStringLogType[logType]; } *this->Impl->OutputLogFile << "] " << std::endl; } *this->Impl->OutputLogFile << msg << std::flush; if (logType != this->Impl->OutputLogFileLastTag) { *this->Impl->OutputLogFile << std::endl; this->Impl->OutputLogFileLastTag = logType; } } } if (!this->Impl->Quiet) { std::ostream& out = *this->Impl->StreamOut; std::ostream& err = *this->Impl->StreamErr; if (logType == HANDLER_TEST_PROGRESS_OUTPUT) { if (this->Impl->TestProgressOutput) { cmCTestLogOutputFileLine(out); if (this->Impl->FlushTestProgressLine) { printf("\r"); this->Impl->FlushTestProgressLine = false; out.flush(); } std::string msg_str{ msg }; auto const lineBreakIt = msg_str.find('\n'); if (lineBreakIt != std::string::npos) { this->Impl->FlushTestProgressLine = true; msg_str.erase(std::remove(msg_str.begin(), msg_str.end(), '\n'), msg_str.end()); } out << msg_str; #ifndef _WIN32 printf("\x1B[K"); // move caret to end #endif out.flush(); return; } logType = HANDLER_OUTPUT; } switch (logType) { case DEBUG: if (this->Impl->Debug) { cmCTestLogOutputFileLine(out); out << msg; out.flush(); } break; case OUTPUT: case HANDLER_OUTPUT: if (this->Impl->Debug || this->Impl->Verbose) { cmCTestLogOutputFileLine(out); out << msg; out.flush(); } break; case HANDLER_VERBOSE_OUTPUT: if (this->Impl->Debug || this->Impl->ExtraVerbose) { cmCTestLogOutputFileLine(out); out << msg; out.flush(); } break; case WARNING: cmCTestLogOutputFileLine(err); err << msg; err.flush(); break; case ERROR_MESSAGE: cmCTestLogOutputFileLine(err); err << msg; err.flush(); cmSystemTools::SetErrorOccurred(); break; default: cmCTestLogOutputFileLine(out); out << msg; out.flush(); } } } std::string cmCTest::GetColorCode(Color color) const { if (this->Impl->OutputColorCode) { return "\033[0;" + std::to_string(static_cast(color)) + "m"; } return ""; } cmDuration cmCTest::GetRemainingTimeAllowed() { return this->GetScriptHandler()->GetRemainingTimeAllowed(); } cmDuration cmCTest::MaxDuration() { return cmDuration(1.0e7); } void cmCTest::SetRunCurrentScript(bool value) { this->GetScriptHandler()->SetRunCurrentScript(value); } void cmCTest::OutputTestErrors(std::vector const& process_output) { std::string test_outputs("\n*** Test Failed:\n"); if (!process_output.empty()) { test_outputs.append(process_output.data(), process_output.size()); } cmCTestLog(this, HANDLER_OUTPUT, test_outputs << std::endl); } 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; }