mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
When ctest repeats tests with its --repeat option, it repeats each test on its own. A fixture's setup and cleanup tests therefore run all of their repetitions back to back, and the tests they bracket repeat inside a single setup/cleanup pair: setup -> setup -> test -> test -> cleanup -> cleanup Add a FIXTURE_REPEAT_MODE test property to select how a fixture behaves when its tests are repeated: * AROUND_ALL_REPEATS: the fixture runs once, around all repetitions of the tests requiring it. * AROUND_EACH_REPEAT: the fixture and the tests requiring it repeat together, so every repetition gets a fresh setup and its own cleanup. * EACH_TEST_SEPARATELY: every test repeats on its own, as before. The property describes the fixture rather than the test carrying it, so setting it on any one of a fixture's setup or cleanup tests is enough. In AROUND_EACH_REPEAT mode the tests of a fixture form a repeat group that ctest re-queues as a whole once every test in it has finished. The --repeat condition then applies to the group the way it applies to an individual test: until-fail repeats while the whole group passes, until-pass repeats while any of it does not, and after-timeout repeats while any of it times out. Fixtures that share a test repeat together, so a test requiring two of them still runs once per repetition. A group is recorded the way a repeating test is: only once it stops repeating, and with the results of its last repetition. A group that until-pass makes pass therefore reports a pass rather than the failure that made it repeat, a test that DEPENDS on one of the group's tests waits for the last repetition rather than the first, and `ctest -F` resumes an interrupted group by running it again from the beginning. Fixtures that repeat together have to agree on the mode: a test cannot repeat with one fixture but not with another it takes part in, and a fixture whose setup and cleanup tests disagree has no coherent behavior. Report an error and run nothing in those cases rather than pick an order in which a test repeats after a fixture it requires has been cleaned up. Add policy CMP0224 to select AROUND_EACH_REPEAT as the default for fixtures whose setup and cleanup tests choose no mode themselves. Record the mode the policy chose in the generated test file under its own _CMAKE_DEFAULT_FIXTURE_REPEAT_MODE keyword, so that ctest reads a mode rather than the policy settings behind it, and so that a mode requested on one of a fixture's tests wins over the default recorded for its siblings. Only NEW needs recording: with nothing recorded, ctest already uses the behavior of CMake 4.4 and below. Fixtures are common, and the choice of mode matters only to those who run ctest --repeat, so warn about the unset policy only when the CMAKE_POLICY_WARNING_CMP0224 variable asks for it. discover_tests() and gtest_discover_tests() create their tests while ctest runs or at build time, too late for the policy to reach them, so carry the setting in effect at their call sites through to the tests they create. Report the repetition a grouped test belongs to in the "(run N/M)" suffix of its "Start" line, as ctest already does for a test repeating on its own. Co-authored-by: Tyler Yankee <tyler.yankee@kitware.com> Fixes: #21438
446 lines
15 KiB
C++
446 lines
15 KiB
C++
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||
#include "cmTestGenerator.h"
|
||
|
||
#include <cstddef> // IWYU pragma: keep
|
||
#include <memory>
|
||
#include <ostream>
|
||
#include <set>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "cmGeneratorExpression.h"
|
||
#include "cmGeneratorTarget.h"
|
||
#include "cmGlobalGenerator.h"
|
||
#include "cmList.h"
|
||
#include "cmListFileCache.h"
|
||
#include "cmLocalGenerator.h"
|
||
#include "cmMakefile.h"
|
||
#include "cmMessageType.h"
|
||
#include "cmPolicies.h"
|
||
#include "cmPropertyMap.h"
|
||
#include "cmRange.h"
|
||
#include "cmScriptGenerator.h"
|
||
#include "cmStringAlgorithms.h"
|
||
#include "cmSystemTools.h"
|
||
#include "cmTargetTypes.h"
|
||
#include "cmTest.h"
|
||
#include "cmValue.h"
|
||
|
||
namespace /* anonymous */
|
||
{
|
||
|
||
bool needToQuoteTestName(cmMakefile const& mf, std::string const& name)
|
||
{
|
||
// Determine if policy CMP0110 is set to NEW.
|
||
switch (mf.GetPolicyStatus(cmPolicies::CMP0110)) {
|
||
case cmPolicies::WARN:
|
||
// Only warn if a forbidden character is used in the name.
|
||
if (name.find_first_of("$[] #;\t\n\"\\") != std::string::npos) {
|
||
mf.IssuePolicyWarning(
|
||
cmPolicies::CMP0110, {},
|
||
cmStrCat("The following name given to add_test() is invalid if "
|
||
"CMP0110 is not set or set to OLD:\n `",
|
||
name, "´\n"));
|
||
}
|
||
CM_FALLTHROUGH;
|
||
case cmPolicies::OLD:
|
||
// OLD behavior is to not quote the test's name.
|
||
return false;
|
||
case cmPolicies::NEW:
|
||
default:
|
||
// NEW behavior is to quote the test's name.
|
||
return true;
|
||
}
|
||
}
|
||
|
||
std::string TestName(cmTest* test)
|
||
{
|
||
std::string name = test->GetName();
|
||
if (needToQuoteTestName(*test->GetMakefile(), name)) {
|
||
name = cmScriptGenerator::Quote(name);
|
||
}
|
||
return name;
|
||
}
|
||
|
||
// Whether a path is produced by the build (a custom-command output or
|
||
// byproduct) rather than a pre-existing file. The output-to-source map
|
||
// records every generated path regardless of which target, if any, builds it.
|
||
bool fileIsGenerated(cmGlobalGenerator* gg, std::string const& file)
|
||
{
|
||
std::string const collapsed = cmSystemTools::CollapseFullPath(file);
|
||
for (auto const& lg : gg->GetLocalGenerators()) {
|
||
cmSourcesWithOutput so = lg->GetSourcesWithOutput(collapsed);
|
||
if (so.Source || so.Target) {
|
||
return true;
|
||
}
|
||
if (file != collapsed) {
|
||
so = lg->GetSourcesWithOutput(file);
|
||
if (so.Source || so.Target) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
} // End: anonymous namespace
|
||
|
||
cmTestGenerator::cmTestGenerator(
|
||
cmTest* test, std::vector<std::string> const& configurations)
|
||
: cmScriptGenerator("CTEST_CONFIGURATION_TYPE", configurations)
|
||
, Test(test)
|
||
{
|
||
this->ActionsPerConfig = test == nullptr || !test->GetOldStyle();
|
||
this->TestGenerated = false;
|
||
this->LG = nullptr;
|
||
}
|
||
|
||
cmTestGenerator::~cmTestGenerator() = default;
|
||
|
||
void cmTestGenerator::Compute(cmLocalGenerator* lg)
|
||
{
|
||
this->LG = lg;
|
||
}
|
||
|
||
bool cmTestGenerator::TestsForConfig(std::string const& config)
|
||
{
|
||
return this->Test != nullptr && this->GeneratesForConfig(config);
|
||
}
|
||
|
||
cmTest* cmTestGenerator::GetTest() const
|
||
{
|
||
return this->Test;
|
||
}
|
||
|
||
bool cmTestGenerator::GetBuildDependencies(cmLocalGenerator* lg,
|
||
std::string const& config,
|
||
BuildDependencies& info)
|
||
{
|
||
if (this->Test == nullptr ||
|
||
!cmGeneratorExpression::IsValidTargetName(this->Test->GetName()) ||
|
||
cmGlobalGenerator::IsReservedTarget(this->Test->GetName())) {
|
||
return false;
|
||
}
|
||
|
||
std::set<cmGeneratorTarget*> dependencies;
|
||
|
||
// Get dependencies from generator expressions
|
||
cmGeneratorExpression ge(*this->Test->GetMakefile()->GetCMakeInstance(),
|
||
this->Test->GetBacktrace());
|
||
for (std::string const& arg : this->Test->GetCommand()) {
|
||
auto parsed = ge.Parse(arg);
|
||
parsed->Evaluate(lg, config);
|
||
for (cmGeneratorTarget* dep : parsed->GetTargets()) {
|
||
if (dep && !dep->IsImported()) {
|
||
dependencies.insert(dep);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Add target executed by test
|
||
if (!this->Test->GetCommand().empty()) {
|
||
std::string exe = this->Test->GetCommand().front();
|
||
cmGeneratorTarget* target = lg->FindGeneratorTargetToUse(exe);
|
||
if (target && target->GetType() == cm::TargetType::EXECUTABLE &&
|
||
!target->IsImported()) {
|
||
dependencies.insert(target);
|
||
}
|
||
}
|
||
|
||
// Add dependencies from BUILD_DEPENDS keyword
|
||
for (auto const& depName : this->Test->GetDependencies()) {
|
||
if (depName.empty()) {
|
||
continue;
|
||
}
|
||
cmGeneratorTarget* depTarget = lg->FindGeneratorTargetToUse(depName);
|
||
if (!depTarget) {
|
||
cmGlobalGenerator* gg = lg->GetGlobalGenerator();
|
||
BuildDependencies::FileDependency file;
|
||
file.Path = depName;
|
||
file.Owner = gg->FindOutputOwningTarget(depName);
|
||
file.Generated = fileIsGenerated(gg, depName);
|
||
info.Files.push_back(std::move(file));
|
||
continue;
|
||
}
|
||
if (depTarget->IsImported()) {
|
||
lg->GetMakefile()->IssueMessage(
|
||
MessageType::FATAL_ERROR,
|
||
cmStrCat("Test \"", this->Test->GetName(), "\" DEPENDS target \"",
|
||
depName, "\" which is imported and cannot be built."),
|
||
this->Test->GetBacktrace());
|
||
return false;
|
||
}
|
||
dependencies.insert(depTarget);
|
||
}
|
||
|
||
for (cmGeneratorTarget* gt : dependencies) {
|
||
if (gt->IsInBuildSystem()) {
|
||
info.Targets.push_back(gt);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void cmTestGenerator::GenerateScriptActions(std::ostream& os, Indent indent)
|
||
{
|
||
if (this->ActionsPerConfig) {
|
||
// This is the per-config generation in a single-configuration
|
||
// build generator case. The superclass will call our per-config
|
||
// method.
|
||
this->cmScriptGenerator::GenerateScriptActions(os, indent);
|
||
} else {
|
||
// This is an old-style test, so there is only one config.
|
||
// assert(this->Test->GetOldStyle());
|
||
this->GenerateOldStyle(os, indent);
|
||
}
|
||
}
|
||
|
||
void cmTestGenerator::GenerateCommand(std::ostream& os,
|
||
std::vector<std::string> const& command,
|
||
std::string const& config, bool expand,
|
||
cmGeneratorExpression& ge,
|
||
cmPolicies::PolicyStatus cmp0158,
|
||
cmPolicies::PolicyStatus cmp0178)
|
||
{
|
||
// Evaluate command line arguments
|
||
cmList argv{
|
||
this->EvaluateCommandLineArguments(command, ge, config),
|
||
// Expand arguments if COMMAND_EXPAND_LISTS is set
|
||
expand ? cmList::ExpandElements::Yes : cmList::ExpandElements::No,
|
||
cmList::EmptyElements::Yes,
|
||
};
|
||
// Expanding lists on an empty command may have left it empty
|
||
if (argv.empty()) {
|
||
argv.emplace_back();
|
||
}
|
||
|
||
// Check whether the command executable is a target whose name is to
|
||
// be translated.
|
||
std::string exe = argv[0];
|
||
cmGeneratorTarget* target = this->LG->FindGeneratorTargetToUse(exe);
|
||
if (target && target->GetType() == cm::TargetType::EXECUTABLE) {
|
||
// Use the target file on disk.
|
||
exe = target->GetFullPath(config);
|
||
|
||
auto addLauncher = [&](std::string const& propertyName) {
|
||
cmValue launcher = target->GetProperty(propertyName);
|
||
if (!cmNonempty(launcher)) {
|
||
return;
|
||
}
|
||
auto const propVal = ge.Parse(*launcher)->Evaluate(this->LG, config);
|
||
cmList launcherWithArgs(propVal, cmList::ExpandElements::Yes,
|
||
cmp0178 == cmPolicies::NEW
|
||
? cmList::EmptyElements::Yes
|
||
: cmList::EmptyElements::No);
|
||
if (!launcherWithArgs.empty() && !launcherWithArgs[0].empty()) {
|
||
if (cmp0178 == cmPolicies::WARN) {
|
||
cmList argsWithEmptyValuesPreserved(
|
||
propVal, cmList::ExpandElements::Yes, cmList::EmptyElements::Yes);
|
||
if (launcherWithArgs != argsWithEmptyValuesPreserved) {
|
||
this->LG->GetMakefile()->IssuePolicyWarning(
|
||
cmPolicies::CMP0178,
|
||
cmStrCat("The ", propertyName, " property of target '",
|
||
target->GetName(),
|
||
"' contains empty list items. Those empty items are "
|
||
"being silently discarded to preserve backward "
|
||
"compatibility."));
|
||
}
|
||
}
|
||
std::string launcherExe(launcherWithArgs[0]);
|
||
cmSystemTools::ConvertToUnixSlashes(launcherExe);
|
||
os << cmScriptGenerator::Quote(launcherExe) << " ";
|
||
for (std::string const& arg :
|
||
cmMakeRange(launcherWithArgs).advance(1)) {
|
||
os << cmScriptGenerator::Quote(arg) << " ";
|
||
}
|
||
}
|
||
};
|
||
|
||
// Prepend with the test launcher if specified.
|
||
addLauncher("TEST_LAUNCHER");
|
||
|
||
// Prepend with the emulator when cross compiling if required.
|
||
if (cmp0158 != cmPolicies::NEW ||
|
||
this->LG->GetMakefile()->IsOn("CMAKE_CROSSCOMPILING")) {
|
||
addLauncher("CROSSCOMPILING_EMULATOR");
|
||
}
|
||
} else {
|
||
// Use the command name given.
|
||
cmSystemTools::ConvertToUnixSlashes(exe);
|
||
}
|
||
|
||
// Generate the command line with full escapes.
|
||
os << cmScriptGenerator::Quote(exe);
|
||
|
||
for (auto const& arg : cmMakeRange(argv).advance(1)) {
|
||
os << " " << cmScriptGenerator::Quote(arg);
|
||
}
|
||
}
|
||
|
||
void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
|
||
std::string const& config,
|
||
Indent indent)
|
||
{
|
||
this->TestGenerated = true;
|
||
|
||
// Set up generator expression evaluation context.
|
||
cmGeneratorExpression ge(*this->Test->GetMakefile()->GetCMakeInstance(),
|
||
this->Test->GetBacktrace());
|
||
|
||
auto const test_name = TestName(this->Test);
|
||
os << indent << "add_test(" << test_name << ' ';
|
||
this->GenerateCommand(
|
||
os, this->Test->GetCommand(), config, this->Test->GetCommandExpandLists(),
|
||
ge, this->GetTest()->GetCMP0158(), this->Test->GetCMP0178());
|
||
os << ")\n";
|
||
|
||
// Output properties for the test.
|
||
os << indent << "set_tests_properties(" << test_name << " PROPERTIES ";
|
||
for (auto const& i : this->Test->GetProperties().GetList()) {
|
||
os << " " << i.first << " "
|
||
<< cmScriptGenerator::Quote(
|
||
ge.Parse(i.second)->Evaluate(this->LG, config));
|
||
}
|
||
BuildDependencies deps;
|
||
if (this->GetBuildDependencies(this->LG, config, deps)) {
|
||
cmList depList;
|
||
for (std::string const& dep :
|
||
this->LG->GetGlobalGenerator()->GetTestBuildDependencyPaths(config,
|
||
deps)) {
|
||
depList.append(dep);
|
||
}
|
||
os << " _CMAKE_TEST_BUILD_DEPENDS "
|
||
<< cmScriptGenerator::Quote(depList.to_string());
|
||
}
|
||
this->GenerateDefaultFixtureRepeatMode(os);
|
||
os << ' ';
|
||
this->GenerateBacktrace(os, this->Test->GetBacktrace());
|
||
os << ")\n";
|
||
}
|
||
|
||
void cmTestGenerator::GenerateScriptNoConfig(std::ostream& os, Indent indent)
|
||
{
|
||
os << indent << "add_test(" << TestName(this->Test) << " NOT_AVAILABLE)\n";
|
||
}
|
||
|
||
bool cmTestGenerator::NeedsScriptNoConfig() const
|
||
{
|
||
return (this->TestGenerated && // test generated for at least one config
|
||
this->ActionsPerConfig && // test is config-aware
|
||
this->Configurations.empty() && // test runs in all configs
|
||
!this->ConfigurationTypes->empty()); // config-dependent command
|
||
}
|
||
|
||
void cmTestGenerator::GenerateOldStyle(std::ostream& fout, Indent indent)
|
||
{
|
||
this->TestGenerated = true;
|
||
|
||
auto const test_name = TestName(this->Test);
|
||
|
||
// Get the test command line to be executed.
|
||
std::vector<std::string> const& command = this->Test->GetCommand();
|
||
|
||
std::string exe = command[0];
|
||
cmSystemTools::ConvertToUnixSlashes(exe);
|
||
fout << indent << "add_test(" << test_name << " \"" << exe << "\"";
|
||
|
||
for (std::string const& arg : cmMakeRange(command).advance(1)) {
|
||
// Just double-quote all arguments so they are re-parsed
|
||
// correctly by the test system.
|
||
fout << " \"";
|
||
for (char c : arg) {
|
||
// Escape quotes within arguments. We should escape
|
||
// backslashes too but we cannot because it makes the result
|
||
// inconsistent with previous behavior of this command.
|
||
if (c == '"') {
|
||
fout << '\\';
|
||
}
|
||
fout << c;
|
||
}
|
||
fout << '"';
|
||
}
|
||
fout << ")\n";
|
||
|
||
// Output properties for the test.
|
||
fout << indent << "set_tests_properties(" << test_name << " PROPERTIES ";
|
||
for (auto const& i : this->Test->GetProperties().GetList()) {
|
||
fout << " " << i.first << " " << cmScriptGenerator::Quote(i.second);
|
||
}
|
||
this->GenerateDefaultFixtureRepeatMode(fout);
|
||
fout << ' ';
|
||
this->GenerateBacktrace(fout, this->Test->GetBacktrace());
|
||
fout << ")\n";
|
||
}
|
||
|
||
void cmTestGenerator::GenerateDefaultFixtureRepeatMode(std::ostream& os)
|
||
{
|
||
// Nothing to choose for a test that is not part of a fixture, or that
|
||
// names a mode itself.
|
||
if (this->Test->GetProperty("FIXTURE_REPEAT_MODE") ||
|
||
(!this->Test->GetProperty("FIXTURES_SETUP") &&
|
||
!this->Test->GetProperty("FIXTURES_CLEANUP"))) {
|
||
return;
|
||
}
|
||
|
||
// Write the mode the policy chose into the test file, so that ctest reads
|
||
// a mode rather than the policy settings behind it. Only NEW needs
|
||
// writing: with nothing written, ctest already uses the
|
||
// EACH_TEST_SEPARATELY behavior of CMake 4.4 and below.
|
||
switch (this->Test->GetCMP0224()) {
|
||
case cmPolicies::WARN:
|
||
// Warn only on request. Fixtures are common, and the choice of mode
|
||
// matters only to those who run ctest --repeat. Collect the tests
|
||
// rather than warning about each: a project that sets its fixtures up
|
||
// in an add_test() wrapper would fill the console.
|
||
if (this->Test->GetMakefile()->PolicyOptionalWarningEnabled(
|
||
"CMAKE_POLICY_WARNING_CMP0224")) {
|
||
this->LG->GetGlobalGenerator()->AddCMP0224WarnTest(
|
||
this->Test->GetName());
|
||
}
|
||
CM_FALLTHROUGH;
|
||
case cmPolicies::OLD:
|
||
break;
|
||
case cmPolicies::NEW:
|
||
os << " _CMAKE_DEFAULT_FIXTURE_REPEAT_MODE AROUND_EACH_REPEAT";
|
||
break;
|
||
}
|
||
}
|
||
|
||
void cmTestGenerator::GenerateBacktrace(std::ostream& os,
|
||
cmListFileBacktrace bt)
|
||
{
|
||
if (bt.Empty()) {
|
||
return;
|
||
}
|
||
|
||
os << "_BACKTRACE_TRIPLES \"";
|
||
|
||
bool prependTripleSeparator = false;
|
||
while (!bt.Empty()) {
|
||
auto const& entry = bt.Top();
|
||
if (prependTripleSeparator) {
|
||
os << ";";
|
||
}
|
||
os << entry.FilePath << ";" << entry.Line << ";" << entry.Name;
|
||
bt = bt.Pop();
|
||
prependTripleSeparator = true;
|
||
}
|
||
|
||
os << '"';
|
||
}
|
||
|
||
std::vector<std::string> cmTestGenerator::EvaluateCommandLineArguments(
|
||
std::vector<std::string> const& argv, cmGeneratorExpression& ge,
|
||
std::string const& config) const
|
||
{
|
||
// Evaluate executable name and arguments
|
||
auto evaluatedRange =
|
||
cmMakeRange(argv).transform([&](std::string const& arg) {
|
||
return ge.Parse(arg)->Evaluate(this->LG, config);
|
||
});
|
||
|
||
return { evaluatedRange.begin(), evaluatedRange.end() };
|
||
}
|