Coverage Report

Created: 2026-02-09 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmTestGenerator.cxx
Line
Count
Source
1
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2
   file LICENSE.rst or https://cmake.org/licensing for details.  */
3
#include "cmTestGenerator.h"
4
5
#include <algorithm>
6
#include <cstddef> // IWYU pragma: keep
7
#include <iterator>
8
#include <memory>
9
#include <ostream>
10
#include <string>
11
#include <utility>
12
#include <vector>
13
14
#include "cmGeneratorExpression.h"
15
#include "cmGeneratorTarget.h"
16
#include "cmList.h"
17
#include "cmListFileCache.h"
18
#include "cmLocalGenerator.h"
19
#include "cmMakefile.h"
20
#include "cmMessageType.h"
21
#include "cmOutputConverter.h"
22
#include "cmPolicies.h"
23
#include "cmPropertyMap.h"
24
#include "cmRange.h"
25
#include "cmStateTypes.h"
26
#include "cmStringAlgorithms.h"
27
#include "cmSystemTools.h"
28
#include "cmTest.h"
29
#include "cmValue.h"
30
31
namespace /* anonymous */
32
{
33
34
bool needToQuoteTestName(cmMakefile const& mf, std::string const& name)
35
0
{
36
  // Determine if policy CMP0110 is set to NEW.
37
0
  switch (mf.GetPolicyStatus(cmPolicies::CMP0110)) {
38
0
    case cmPolicies::WARN:
39
      // Only warn if a forbidden character is used in the name.
40
0
      if (name.find_first_of("$[] #;\t\n\"\\") != std::string::npos) {
41
0
        mf.IssueMessage(
42
0
          MessageType::AUTHOR_WARNING,
43
0
          cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0110),
44
0
                   "\nThe following name given to add_test() is invalid if "
45
0
                   "CMP0110 is not set or set to OLD:\n  `",
46
0
                   name, "ยด\n"));
47
0
      }
48
0
      CM_FALLTHROUGH;
49
0
    case cmPolicies::OLD:
50
      // OLD behavior is to not quote the test's name.
51
0
      return false;
52
0
    case cmPolicies::NEW:
53
0
    default:
54
      // NEW behavior is to quote the test's name.
55
0
      return true;
56
0
  }
57
0
}
58
59
std::size_t countMaxConsecutiveEqualSigns(std::string const& name)
60
0
{
61
0
  std::size_t max = 0;
62
0
  auto startIt = find(name.begin(), name.end(), '=');
63
0
  auto endIt = startIt;
64
0
  for (; startIt != name.end(); startIt = find(endIt, name.end(), '=')) {
65
0
    endIt =
66
0
      find_if_not(startIt + 1, name.end(), [](char c) { return c == '='; });
67
0
    max =
68
0
      std::max(max, static_cast<std::size_t>(std::distance(startIt, endIt)));
69
0
  }
70
0
  return max;
71
0
}
72
73
} // End: anonymous namespace
74
75
cmTestGenerator::cmTestGenerator(
76
  cmTest* test, std::vector<std::string> const& configurations)
77
0
  : cmScriptGenerator("CTEST_CONFIGURATION_TYPE", configurations)
78
0
  , Test(test)
79
0
{
80
0
  this->ActionsPerConfig = !test->GetOldStyle();
81
0
  this->TestGenerated = false;
82
0
  this->LG = nullptr;
83
0
}
84
85
0
cmTestGenerator::~cmTestGenerator() = default;
86
87
void cmTestGenerator::Compute(cmLocalGenerator* lg)
88
0
{
89
0
  this->LG = lg;
90
0
}
91
92
bool cmTestGenerator::TestsForConfig(std::string const& config)
93
0
{
94
0
  return this->GeneratesForConfig(config);
95
0
}
96
97
cmTest* cmTestGenerator::GetTest() const
98
0
{
99
0
  return this->Test;
100
0
}
101
102
void cmTestGenerator::GenerateScriptConfigs(std::ostream& os, Indent indent)
103
0
{
104
  // Create the tests.
105
0
  this->cmScriptGenerator::GenerateScriptConfigs(os, indent);
106
0
}
107
108
void cmTestGenerator::GenerateScriptActions(std::ostream& os, Indent indent)
109
0
{
110
0
  if (this->ActionsPerConfig) {
111
    // This is the per-config generation in a single-configuration
112
    // build generator case.  The superclass will call our per-config
113
    // method.
114
0
    this->cmScriptGenerator::GenerateScriptActions(os, indent);
115
0
  } else {
116
    // This is an old-style test, so there is only one config.
117
    // assert(this->Test->GetOldStyle());
118
0
    this->GenerateOldStyle(os, indent);
119
0
  }
120
0
}
121
122
void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
123
                                              std::string const& config,
124
                                              Indent indent)
125
0
{
126
0
  this->TestGenerated = true;
127
128
  // Set up generator expression evaluation context.
129
0
  cmGeneratorExpression ge(*this->Test->GetMakefile()->GetCMakeInstance(),
130
0
                           this->Test->GetBacktrace());
131
132
  // Determine if policy CMP0110 is set to NEW.
133
0
  bool const quote_test_name =
134
0
    needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
135
  // Determine the number of equal-signs needed for quoting test name with
136
  // [==[...]==] syntax.
137
0
  std::string const equalSigns(
138
0
    1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
139
140
  // Start the test command.
141
0
  if (quote_test_name) {
142
0
    os << indent << "add_test([" << equalSigns << "[" << this->Test->GetName()
143
0
       << "]" << equalSigns << "] ";
144
0
  } else {
145
0
    os << indent << "add_test(" << this->Test->GetName() << " ";
146
0
  }
147
148
  // Evaluate command line arguments
149
0
  cmList argv{
150
0
    this->EvaluateCommandLineArguments(this->Test->GetCommand(), ge, config),
151
    // Expand arguments if COMMAND_EXPAND_LISTS is set
152
0
    this->Test->GetCommandExpandLists() ? cmList::ExpandElements::Yes
153
0
                                        : cmList::ExpandElements::No,
154
0
    cmList::EmptyElements::Yes
155
0
  };
156
  // Expanding lists on an empty command may have left it empty
157
0
  if (argv.empty()) {
158
0
    argv.emplace_back();
159
0
  }
160
161
  // Check whether the command executable is a target whose name is to
162
  // be translated.
163
0
  std::string exe = argv[0];
164
0
  cmGeneratorTarget* target = this->LG->FindGeneratorTargetToUse(exe);
165
0
  if (target && target->GetType() == cmStateEnums::EXECUTABLE) {
166
    // Use the target file on disk.
167
0
    exe = target->GetFullPath(config);
168
169
0
    auto addLauncher = [this, &config, &ge, &os,
170
0
                        target](std::string const& propertyName) {
171
0
      cmValue launcher = target->GetProperty(propertyName);
172
0
      if (!cmNonempty(launcher)) {
173
0
        return;
174
0
      }
175
0
      auto const propVal = ge.Parse(*launcher)->Evaluate(this->LG, config);
176
0
      cmList launcherWithArgs(propVal, cmList::ExpandElements::Yes,
177
0
                              this->Test->GetCMP0178() == cmPolicies::NEW
178
0
                                ? cmList::EmptyElements::Yes
179
0
                                : cmList::EmptyElements::No);
180
0
      if (!launcherWithArgs.empty() && !launcherWithArgs[0].empty()) {
181
0
        if (this->Test->GetCMP0178() == cmPolicies::WARN) {
182
0
          cmList argsWithEmptyValuesPreserved(
183
0
            propVal, cmList::ExpandElements::Yes, cmList::EmptyElements::Yes);
184
0
          if (launcherWithArgs != argsWithEmptyValuesPreserved) {
185
0
            this->Test->GetMakefile()->IssueMessage(
186
0
              MessageType::AUTHOR_WARNING,
187
0
              cmStrCat("The ", propertyName, " property of target '",
188
0
                       target->GetName(),
189
0
                       "' contains empty list items. Those empty items are "
190
0
                       "being silently discarded to preserve backward "
191
0
                       "compatibility.\n",
192
0
                       cmPolicies::GetPolicyWarning(cmPolicies::CMP0178)));
193
0
          }
194
0
        }
195
0
        std::string launcherExe(launcherWithArgs[0]);
196
0
        cmSystemTools::ConvertToUnixSlashes(launcherExe);
197
0
        os << cmOutputConverter::EscapeForCMake(launcherExe) << " ";
198
0
        for (std::string const& arg :
199
0
             cmMakeRange(launcherWithArgs).advance(1)) {
200
0
          if (arg.empty()) {
201
0
            os << "\"\" ";
202
0
          } else {
203
0
            os << cmOutputConverter::EscapeForCMake(arg) << " ";
204
0
          }
205
0
        }
206
0
      }
207
0
    };
208
209
    // Prepend with the test launcher if specified.
210
0
    addLauncher("TEST_LAUNCHER");
211
212
    // Prepend with the emulator when cross compiling if required.
213
0
    if (!this->GetTest()->GetCMP0158IsNew() ||
214
0
        this->LG->GetMakefile()->IsOn("CMAKE_CROSSCOMPILING")) {
215
0
      addLauncher("CROSSCOMPILING_EMULATOR");
216
0
    }
217
0
  } else {
218
    // Use the command name given.
219
0
    cmSystemTools::ConvertToUnixSlashes(exe);
220
0
  }
221
222
  // Generate the command line with full escapes.
223
0
  os << cmOutputConverter::EscapeForCMake(exe);
224
225
0
  for (auto const& arg : cmMakeRange(argv).advance(1)) {
226
0
    os << " " << cmOutputConverter::EscapeForCMake(arg);
227
0
  }
228
229
  // Finish the test command.
230
0
  os << ")\n";
231
232
  // Output properties for the test.
233
0
  if (quote_test_name) {
234
0
    os << indent << "set_tests_properties([" << equalSigns << "["
235
0
       << this->Test->GetName() << "]" << equalSigns << "] PROPERTIES ";
236
0
  } else {
237
0
    os << indent << "set_tests_properties(" << this->Test->GetName()
238
0
       << " PROPERTIES ";
239
0
  }
240
0
  for (auto const& i : this->Test->GetProperties().GetList()) {
241
0
    os << " " << i.first << " "
242
0
       << cmOutputConverter::EscapeForCMake(
243
0
            ge.Parse(i.second)->Evaluate(this->LG, config));
244
0
  }
245
0
  this->GenerateInternalProperties(os);
246
0
  os << ")\n";
247
0
}
248
249
void cmTestGenerator::GenerateScriptNoConfig(std::ostream& os, Indent indent)
250
0
{
251
  // Determine if policy CMP0110 is set to NEW.
252
0
  bool const quote_test_name =
253
0
    needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
254
  // Determine the number of equal-signs needed for quoting test name with
255
  // [==[...]==] syntax.
256
0
  std::string const equalSigns(
257
0
    1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
258
259
0
  if (quote_test_name) {
260
0
    os << indent << "add_test([" << equalSigns << "[" << this->Test->GetName()
261
0
       << "]" << equalSigns << "] NOT_AVAILABLE)\n";
262
0
  } else {
263
0
    os << indent << "add_test(" << this->Test->GetName()
264
0
       << " NOT_AVAILABLE)\n";
265
0
  }
266
0
}
267
268
bool cmTestGenerator::NeedsScriptNoConfig() const
269
0
{
270
0
  return (this->TestGenerated &&    // test generated for at least one config
271
0
          this->ActionsPerConfig && // test is config-aware
272
0
          this->Configurations.empty() &&      // test runs in all configs
273
0
          !this->ConfigurationTypes->empty()); // config-dependent command
274
0
}
275
276
void cmTestGenerator::GenerateOldStyle(std::ostream& fout, Indent indent)
277
0
{
278
0
  this->TestGenerated = true;
279
280
  // Determine if policy CMP0110 is set to NEW.
281
0
  bool const quote_test_name =
282
0
    needToQuoteTestName(*this->Test->GetMakefile(), this->Test->GetName());
283
  // Determine the number of equal-signs needed for quoting test name with
284
  // [==[...]==] syntax.
285
0
  std::string const equalSigns(
286
0
    1 + countMaxConsecutiveEqualSigns(this->Test->GetName()), '=');
287
288
  // Get the test command line to be executed.
289
0
  std::vector<std::string> const& command = this->Test->GetCommand();
290
291
0
  std::string exe = command[0];
292
0
  cmSystemTools::ConvertToUnixSlashes(exe);
293
0
  if (quote_test_name) {
294
0
    fout << indent << "add_test([" << equalSigns << "["
295
0
         << this->Test->GetName() << "]" << equalSigns << "] \"" << exe
296
0
         << "\"";
297
0
  } else {
298
0
    fout << indent << "add_test(" << this->Test->GetName() << " \"" << exe
299
0
         << "\"";
300
0
  }
301
302
0
  for (std::string const& arg : cmMakeRange(command).advance(1)) {
303
    // Just double-quote all arguments so they are re-parsed
304
    // correctly by the test system.
305
0
    fout << " \"";
306
0
    for (char c : arg) {
307
      // Escape quotes within arguments.  We should escape
308
      // backslashes too but we cannot because it makes the result
309
      // inconsistent with previous behavior of this command.
310
0
      if (c == '"') {
311
0
        fout << '\\';
312
0
      }
313
0
      fout << c;
314
0
    }
315
0
    fout << '"';
316
0
  }
317
0
  fout << ")\n";
318
319
  // Output properties for the test.
320
0
  if (quote_test_name) {
321
0
    fout << indent << "set_tests_properties([" << equalSigns << "["
322
0
         << this->Test->GetName() << "]" << equalSigns << "] PROPERTIES ";
323
0
  } else {
324
0
    fout << indent << "set_tests_properties(" << this->Test->GetName()
325
0
         << " PROPERTIES ";
326
0
  }
327
0
  for (auto const& i : this->Test->GetProperties().GetList()) {
328
0
    fout << " " << i.first << " "
329
0
         << cmOutputConverter::EscapeForCMake(i.second);
330
0
  }
331
0
  this->GenerateInternalProperties(fout);
332
0
  fout << ")\n";
333
0
}
334
335
void cmTestGenerator::GenerateInternalProperties(std::ostream& os)
336
0
{
337
0
  cmListFileBacktrace bt = this->Test->GetBacktrace();
338
0
  if (bt.Empty()) {
339
0
    return;
340
0
  }
341
342
0
  os << " "
343
0
     << "_BACKTRACE_TRIPLES"
344
0
     << " \"";
345
346
0
  bool prependTripleSeparator = false;
347
0
  while (!bt.Empty()) {
348
0
    auto const& entry = bt.Top();
349
0
    if (prependTripleSeparator) {
350
0
      os << ";";
351
0
    }
352
0
    os << entry.FilePath << ";" << entry.Line << ";" << entry.Name;
353
0
    bt = bt.Pop();
354
0
    prependTripleSeparator = true;
355
0
  }
356
357
0
  os << '"';
358
0
}
359
360
std::vector<std::string> cmTestGenerator::EvaluateCommandLineArguments(
361
  std::vector<std::string> const& argv, cmGeneratorExpression& ge,
362
  std::string const& config) const
363
0
{
364
  // Evaluate executable name and arguments
365
0
  auto evaluatedRange =
366
0
    cmMakeRange(argv).transform([&](std::string const& arg) {
367
0
      return ge.Parse(arg)->Evaluate(this->LG, config);
368
0
    });
369
370
0
  return { evaluatedRange.begin(), evaluatedRange.end() };
371
0
}