diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 25fda89edf..a4012c98d8 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -1143,6 +1143,7 @@ add_library( CTest/cmCTestCoverageCommand.cxx CTest/cmCTestCoverageHandler.cxx CTest/cmCTestCurl.cxx + CTest/cmCTestDiscoverTests.cxx CTest/cmParseMumpsCoverage.cxx CTest/cmParseCacheCoverage.cxx CTest/cmParseGTMCoverage.cxx diff --git a/Source/CTest/cmCTestDiscoverTests.cxx b/Source/CTest/cmCTestDiscoverTests.cxx new file mode 100644 index 0000000000..95a97a130a --- /dev/null +++ b/Source/CTest/cmCTestDiscoverTests.cxx @@ -0,0 +1,163 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#include "cmCTestDiscoverTests.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "cmArgumentParserTypes.h" +#include "cmCTestTestHandler.h" +#include "cmDuration.h" +#include "cmEnvironment.h" +#include "cmExecutionStatus.h" +#include "cmList.h" +#include "cmProcessOutput.h" +#include "cmStringAlgorithms.h" +#include "cmSystemTools.h" +#include "cmTestDiscovery.h" + +namespace { + +struct DiscoveryProperties +{ + DiscoveryProperties(std::vector const& props); + + std::vector Environment; + std::vector EnvironmentModification; + cmDuration Timeout = cmDuration(5.0); + std::string WorkingDirectory; +}; + +DiscoveryProperties::DiscoveryProperties(std::vector const& props) +{ + for (std::size_t i = 0; i < props.size(); i += 2) { + auto const& key = props[i]; + auto const& val = props[i + 1]; + if (key == "ENVIRONMENT") { + cmExpandList(val, this->Environment); + } else if (key == "ENVIRONMENT_MODIFICATION") { + cmExpandList(val, this->EnvironmentModification); + } else if (key == "TIMEOUT") { + this->Timeout = cmDuration(atof(val.c_str())); + } else if (key == "WORKING_DIRECTORY") { + this->WorkingDirectory = val; + } + } +} + +std::string AddAnchors(std::string str) +{ + if (str.empty() || str.front() != '^') { + str.insert(str.begin(), '^'); + } + if (str.size() < 2 || str.back() != '$') { + str.push_back('$'); + } + return str; +} + +class RegexReplacer +{ +public: + RegexReplacer(cmsys::RegularExpression& regex) + : RegEx(regex) + { + } + + std::string operator()(std::string str) + { + for (int i = 0; i <= this->RegEx.num_groups(); ++i) { + cmsys::SystemTools::ReplaceString(str, cmStrCat('\\', i), + this->RegEx.match(i)); + } + return str; + } + +private: + cmsys::RegularExpression& RegEx; +}; + +} // namespace + +bool cmCTestDiscoverTests(cmTestDiscoveryArgs const& args, + cmCTestTestHandler* handler, + cmExecutionStatus& status) +{ + cmsys::RegularExpression re; + if (!re.compile(AddAnchors(args.DiscoveryMatch))) { + std::string e = "DISCOVERY_MATCH failed to compile regex \"" + + args.DiscoveryMatch + "\"."; + status.SetError(e); + return false; + } + + auto const props = DiscoveryProperties{ args.DiscoveryProperties }; + + auto env = cmEnvironment{ cmSystemTools::GetEnvironmentVariables() }; + env.Update(props.Environment); + if (!props.EnvironmentModification.empty()) { + auto diff = cmEnvironmentModification{}; + if (!diff.Add(props.EnvironmentModification)) { + return false; + } + diff.ApplyTo(env); + } + + auto runCommand = std::vector(args.Command); + runCommand.reserve(runCommand.size() + args.DiscoveryArgs.size()); + std::copy(args.DiscoveryArgs.begin(), args.DiscoveryArgs.end(), + std::back_inserter(runCommand)); + + std::string stdOut; + std::string stdErr; + bool const res = cmSystemTools::RunSingleCommand( + runCommand, &stdOut, &stdErr, nullptr, props.WorkingDirectory.c_str(), + cmSystemTools::OUTPUT_NONE, props.Timeout, cmProcessOutput::Auto, + env.GetVariables()); + if (!res) { + status.SetError(cmStrCat(" failed to run command: ", + cmSystemTools::PrintSingleCommand(runCommand), + "\n", stdErr)); + return false; + } + + auto replace = RegexReplacer{ re }; + + std::vector lines; + cmSystemTools::Split(stdOut, lines); + for (auto const& line : lines) { + if (!re.find(line)) { + continue; + } + + auto const testName = replace(args.TestName); + auto testArgs = std::vector{ testName }; + testArgs.reserve(1 + args.Command.size() + args.TestArgs.size()); + std::copy(args.Command.begin(), args.Command.end(), + std::back_inserter(testArgs)); + std::transform(args.TestArgs.begin(), args.TestArgs.end(), + std::back_inserter(testArgs), replace); + + if (!handler->AddTest(testArgs)) { + return false; + } + + auto testProperties = std::vector{ testName, "PROPERTIES" }; + testProperties.reserve(2 + args.TestProperties.size()); + for (std::size_t i = 0; i < args.TestProperties.size(); i += 2) { + testProperties.push_back(args.TestProperties[i]); + testProperties.push_back(replace(args.TestProperties[i + 1])); + } + if (!handler->SetTestsProperties(testProperties)) { + return false; + } + } + + return true; +} diff --git a/Source/CTest/cmCTestDiscoverTests.h b/Source/CTest/cmCTestDiscoverTests.h new file mode 100644 index 0000000000..8de7d7e85f --- /dev/null +++ b/Source/CTest/cmCTestDiscoverTests.h @@ -0,0 +1,13 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +struct cmTestDiscoveryArgs; +class cmCTestTestHandler; +class cmExecutionStatus; + +bool cmCTestDiscoverTests(cmTestDiscoveryArgs const& args, + cmCTestTestHandler* handler, + cmExecutionStatus& status); diff --git a/Source/CTest/cmCTestTestHandler.cxx b/Source/CTest/cmCTestTestHandler.cxx index f453e4f0b6..d818f4dd6b 100644 --- a/Source/CTest/cmCTestTestHandler.cxx +++ b/Source/CTest/cmCTestTestHandler.cxx @@ -15,6 +15,7 @@ #include #include #include +#include #include #ifndef _WIN32 @@ -33,7 +34,9 @@ #include "cm_utf8.h" +#include "cmArgumentParser.h" #include "cmCTest.h" +#include "cmCTestDiscoverTests.h" #include "cmCTestMultiProcessHandler.h" #include "cmCTestResourceGroupsLexerHelper.h" #include "cmCTestTestMeasurementXMLParser.h" @@ -49,6 +52,7 @@ #include "cmStateSnapshot.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" +#include "cmTestDiscovery.h" #include "cmTimestamp.h" #include "cmValue.h" #include "cmWorkingDirectory.h" @@ -184,6 +188,34 @@ bool cmCTestAddTestCommand::InitialPass(std::vector const& args, return this->TestHandler->AddTest(args); } +class cmCTestDiscoverTestsCommand : public cmCTestCommand +{ +public: + using cmCTestCommand::cmCTestCommand; + bool InitialPass(std::vector const& args, + cmExecutionStatus& status) override; +}; + +bool cmCTestDiscoverTestsCommand::InitialPass( + std::vector const& args, cmExecutionStatus& status) +{ + using Arguments = cmTestDiscoveryArgs; + static auto const parser = cmTestDiscoveryParser(); + + auto unparsed = std::vector{}; + Arguments const arguments = parser.Parse(args, &unparsed); + if (arguments.MaybeReportError(status.GetMakefile())) { + return true; + } + + if (!unparsed.empty()) { + status.SetError(" given unknown argument \"" + unparsed.front() + "\"."); + return false; + } + + return cmCTestDiscoverTests(arguments, this->TestHandler, status); +} + class cmCTestSetTestsPropertiesCommand : public cmCTestCommand { public: @@ -1772,6 +1804,10 @@ bool cmCTestTestHandler::GetListOfTests() // Add handler for ADD_TEST cm.GetState()->AddBuiltinCommand("add_test", cmCTestAddTestCommand(this)); + // Add handler for DISCOVER_TESTS + cm.GetState()->AddBuiltinCommand("discover_tests", + cmCTestDiscoverTestsCommand(this)); + // Add handler for SUBDIRS cm.GetState()->AddBuiltinCommand("subdirs", cmCTestSubdirCommand); diff --git a/Source/cmTestDiscovery.h b/Source/cmTestDiscovery.h new file mode 100644 index 0000000000..a8d899ee35 --- /dev/null +++ b/Source/cmTestDiscovery.h @@ -0,0 +1,32 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#pragma once + +#include "cmConfigure.h" // IWYU pragma: keep + +#include "cmArgumentParser.h" + +struct cmTestDiscoveryArgs : ArgumentParser::ParseResult +{ + ArgumentParser::NonEmpty> Command; + ArgumentParser::NonEmpty> DiscoveryArgs; + ArgumentParser::NonEmpty DiscoveryMatch; + ArgumentParser::MaybeEmpty> DiscoveryProperties; + ArgumentParser::NonEmpty TestName; + ArgumentParser::NonEmpty> TestArgs; + ArgumentParser::MaybeEmpty> TestProperties; +}; + +template +auto cmTestDiscoveryParser() -> cmArgumentParser +{ + static_assert(std::is_base_of::value, ""); + return cmArgumentParser{} + .Bind("COMMAND"_s, &Args::Command) + .Bind("DISCOVERY_ARGS"_s, &Args::DiscoveryArgs) + .Bind("DISCOVERY_MATCH"_s, &Args::DiscoveryMatch) + .Bind("DISCOVERY_PROPERTIES"_s, &Args::DiscoveryProperties) + .Bind("TEST_NAME"_s, &Args::TestName) + .Bind("TEST_ARGS"_s, &Args::TestArgs) + .Bind("TEST_PROPERTIES"_s, &Args::TestProperties); +}