You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cmake/Source/cmTestGenerator.cxx

337 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file Copyright.txt or https://cmake.org/licensing for details. */
#include "cmTestGenerator.h"
#include <algorithm>
#include <cstddef> // IWYU pragma: keep
#include <iterator>
#include <memory>
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "cmGeneratorExpression.h"
#include "cmGeneratorTarget.h"
#include "cmList.h"
#include "cmListFileCache.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmMessageType.h"
#include "cmOutputConverter.h"
#include "cmPolicies.h"
#include "cmPropertyMap.h"
#include "cmRange.h"
#include "cmStateTypes.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmTest.h"
#include "cmValue.h"
namespace /* anonymous */
{
bool needToQuoteTestName(const cmMakefile& mf, const std::string& 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.IssueMessage(
MessageType::AUTHOR_WARNING,
cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0110),
"\nThe 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::REQUIRED_IF_USED:
case cmPolicies::REQUIRED_ALWAYS:
case cmPolicies::NEW:
default:
// NEW behavior is to quote the test's name.
return true;
}
}
std::size_t countMaxConsecutiveEqualSigns(const std::string& name)
{
std::size_t max = 0;
auto startIt = find(name.begin(), name.end(), '=');
auto endIt = startIt;
for (; startIt != name.end(); startIt = find(endIt, name.end(), '=')) {
endIt =
find_if_not(startIt + 1, name.end(), [](char c) { return c == '='; });
max =
std::max(max, static_cast<std::size_t>(std::distance(startIt, endIt)));
}
return max;
}
} // End: anonymous namespace
cmTestGenerator::cmTestGenerator(
cmTest* test, std::vector<std::string> const& configurations)
: cmScriptGenerator("CTEST_CONFIGURATION_TYPE", configurations)
, Test(test)
{
this->ActionsPerConfig = !test->GetOldStyle();
this->TestGenerated = false;
this->LG = nullptr;
}
cmTestGenerator::~cmTestGenerator() = default;
void cmTestGenerator::Compute(cmLocalGenerator* lg)
{
this->LG = lg;
}
bool cmTestGenerator::TestsForConfig(const std::string& config)
{
return this->GeneratesForConfig(config);
}
cmTest* cmTestGenerator::GetTest() const
{
return this->Test;
}
void cmTestGenerator::GenerateScriptConfigs(std::ostream& os, Indent indent)
{
// Create the tests.
this->cmScriptGenerator::GenerateScriptConfigs(os, indent);
}
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::GenerateScriptForConfig(std::ostream& os,
const std::string& config,
Indent indent)
{
this->TestGenerated = true;
// Set up generator expression evaluation context.
cmGeneratorExpression ge(*this->Test->GetMakefile()->GetCMakeInstance(),
this->Test->GetBacktrace());
// Determine if policy CMP0110 is set to NEW.
const bool quote_test_name =
needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
// Determine the number of equal-signs needed for quoting test name with
// [==[...]==] syntax.
const std::string equalSigns(
1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
// Start the test command.
if (quote_test_name) {
os << indent << "add_test([" << equalSigns << "[" << this->Test->GetName()
<< "]" << equalSigns << "] ";
} else {
os << indent << "add_test(" << this->Test->GetName() << " ";
}
// Evaluate command line arguments
cmList argv{
this->EvaluateCommandLineArguments(this->Test->GetCommand(), ge, config),
// Expand arguments if COMMAND_EXPAND_LISTS is set
this->Test->GetCommandExpandLists() ? 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() == cmStateEnums::EXECUTABLE) {
// Use the target file on disk.
exe = target->GetFullPath(config);
// Prepend with the emulator when cross compiling if required.
cmValue emulator = target->GetProperty("CROSSCOMPILING_EMULATOR");
if (cmNonempty(emulator)) {
cmList emulatorWithArgs{ *emulator };
std::string emulatorExe(emulatorWithArgs[0]);
cmSystemTools::ConvertToUnixSlashes(emulatorExe);
os << cmOutputConverter::EscapeForCMake(emulatorExe) << " ";
for (std::string const& arg : cmMakeRange(emulatorWithArgs).advance(1)) {
os << cmOutputConverter::EscapeForCMake(arg) << " ";
}
}
} else {
// Use the command name given.
cmSystemTools::ConvertToUnixSlashes(exe);
}
// Generate the command line with full escapes.
os << cmOutputConverter::EscapeForCMake(exe);
for (auto const& arg : cmMakeRange(argv).advance(1)) {
os << " " << cmOutputConverter::EscapeForCMake(arg);
}
// Finish the test command.
os << ")\n";
// Output properties for the test.
if (quote_test_name) {
os << indent << "set_tests_properties([" << equalSigns << "["
<< this->Test->GetName() << "]" << equalSigns << "] PROPERTIES ";
} else {
os << indent << "set_tests_properties(" << this->Test->GetName()
<< " PROPERTIES ";
}
for (auto const& i : this->Test->GetProperties().GetList()) {
os << " " << i.first << " "
<< cmOutputConverter::EscapeForCMake(
ge.Parse(i.second)->Evaluate(this->LG, config));
}
this->GenerateInternalProperties(os);
os << ")\n";
}
void cmTestGenerator::GenerateScriptNoConfig(std::ostream& os, Indent indent)
{
// Determine if policy CMP0110 is set to NEW.
const bool quote_test_name =
needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
// Determine the number of equal-signs needed for quoting test name with
// [==[...]==] syntax.
const std::string equalSigns(
1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
if (quote_test_name) {
os << indent << "add_test([" << equalSigns << "[" << this->Test->GetName()
<< "]" << equalSigns << "] NOT_AVAILABLE)\n";
} else {
os << indent << "add_test(" << this->Test->GetName()
<< " 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;
// Determine if policy CMP0110 is set to NEW.
const bool quote_test_name =
needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
// Determine the number of equal-signs needed for quoting test name with
// [==[...]==] syntax.
const std::string equalSigns(
1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
// 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);
if (quote_test_name) {
fout << indent << "add_test([" << equalSigns << "["
<< this->Test->GetName() << "]" << equalSigns << "] \"" << exe
<< "\"";
} else {
fout << indent << "add_test(" << this->Test->GetName() << " \"" << 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.
if (quote_test_name) {
fout << indent << "set_tests_properties([" << equalSigns << "["
<< this->Test->GetName() << "]" << equalSigns << "] PROPERTIES ";
} else {
fout << indent << "set_tests_properties(" << this->Test->GetName()
<< " PROPERTIES ";
}
for (auto const& i : this->Test->GetProperties().GetList()) {
fout << " " << i.first << " "
<< cmOutputConverter::EscapeForCMake(i.second);
}
this->GenerateInternalProperties(fout);
fout << ")\n";
}
void cmTestGenerator::GenerateInternalProperties(std::ostream& os)
{
cmListFileBacktrace bt = this->Test->GetBacktrace();
if (bt.Empty()) {
return;
}
os << " "
<< "_BACKTRACE_TRIPLES"
<< " \"";
bool prependTripleSeparator = false;
while (!bt.Empty()) {
const auto& 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(
const std::vector<std::string>& argv, cmGeneratorExpression& ge,
const std::string& config) const
{
// Evaluate executable name and arguments
auto evaluatedRange =
cmMakeRange(argv).transform([&](const std::string& arg) {
return ge.Parse(arg)->Evaluate(this->LG, config);
});
return { evaluatedRange.begin(), evaluatedRange.end() };
}