Files
cmake/Source/CTest/cmCTestTestHandler.h
Taylor Braun-JonesandTyler Yankee 5cced02b9d CTest: Repeat test fixtures with the tests that require them
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
2026-09-18 08:29:28 -04:00

454 lines
14 KiB
C++

/* 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 <chrono>
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <cm/optional>
#include <cm/string_view>
#include "cmsys/RegularExpression.hxx"
#include "cmCMakePresetsGraph.h"
#include "cmCTest.h"
#include "cmCTestGenericHandler.h"
#include "cmCTestTypes.h" // IWYU pragma: keep
#include "cmDuration.h"
#include "cmListFileCache.h"
class cmXMLWriter;
struct cmCTestTestOptions
{
bool RerunFailed = false;
bool OutOfDateOnly = false;
bool ScheduleRandom = false;
bool StopOnFailure = false;
bool UseUnion = false;
cm::optional<unsigned int> ScheduleRandomSeed;
int OutputSizePassed = 1 * 1024;
int OutputSizeFailed = 300 * 1024;
cmCTestTypes::TruncationMode OutputTruncation =
cmCTestTypes::TruncationMode::Tail;
std::string TestsToRunInformation;
std::string IncludeRegularExpression;
std::string ExcludeRegularExpression;
std::vector<std::string> LabelRegularExpression;
std::vector<std::string> ExcludeLabelRegularExpression;
std::string ExcludeFixtureRegularExpression;
std::string ExcludeFixtureSetupRegularExpression;
std::string ExcludeFixtureCleanupRegularExpression;
std::string TestListFile;
std::string ExcludeTestListFile;
std::string ResourceSpecFile;
std::string JUnitXMLFileName;
std::string CoverageTool;
std::vector<std::string> TestPassthroughArguments;
};
/** Apply a resolved TestPreset's fields to \a opts.
*
* Both the \c ctest \c --preset CLI path and the \c ctest_test(PRESET)
* script-command path call this to keep the mapping in one place and avoid
* drift when new preset fields are added.
*
* Fields that do not live in cmCTestTestOptions (e.g. ParallelLevel, Repeat,
* Timeout, NoTestsAction) are handled separately at each call site.
*/
void cmCTestApplyTestPresetToOptions(
cmCTestTestOptions& opts, cmCMakePresetsGraph::TestPreset const& preset);
/** \class cmCTestTestHandler
* \brief A class that handles ctest -S invocations
*
*/
class cmCTestTestHandler : public cmCTestGenericHandler
{
friend class cmCTest;
friend class cmCTestRunTest;
friend class cmCTestMultiProcessHandler;
public:
using Superclass = cmCTestGenericHandler;
/**
* The main entry point for this class
*/
int ProcessHandler() override;
/**
* This method is called when reading CTest custom file
*/
void PopulateCustomVectors(cmMakefile* mf) override;
//! Control the use of the regular expressions, call these methods to turn
/// them on
void UseIncludeRegExp();
void UseExcludeRegExp();
void SetMaxIndex(int n) { this->MaxIndex = n; }
int GetMaxIndex() { return this->MaxIndex; }
//! pass the -I argument down
void SetTestsToRunInformation(std::string const& in);
cmCTestTestHandler(cmCTest* ctest);
/*
* Add the test to the list of tests to be executed
*/
bool AddTest(std::vector<std::string> const& args);
/*
* Set tests properties
*/
bool SetTestsProperties(std::vector<std::string> const& args);
/**
* Set directory properties
*/
bool SetDirectoryProperties(std::vector<std::string> const& args);
struct cmCTestTestResourceRequirement
{
std::string ResourceType;
int SlotsNeeded;
int UnitsNeeded;
bool operator==(cmCTestTestResourceRequirement const& other) const;
bool operator!=(cmCTestTestResourceRequirement const& other) const;
};
struct Signal
{
int Number = 0;
std::string Name;
};
// How ctest --repeat treats a fixture. See the FIXTURE_REPEAT_MODE test
// property and policy CMP0224.
enum class FixtureRepeatMode
{
AroundAllRepeats, // the fixture runs once, around all repetitions
AroundEachRepeat, // the fixture repeats with the tests requiring it
EachTestSeparately // every test repeats on its own
};
struct cmCTestTestProperties
{
void AppendError(cm::string_view err);
std::string GetStampFile();
cm::optional<std::string> Error;
std::string Name;
// working directory for test, overridden by WORKING_DIRECTORY property
std::string Directory;
std::string DirectoryRaw;
// Original directory of test creation
std::string CTestDirectory;
std::vector<std::string> Args;
std::vector<std::string> RequiredFiles;
std::string RequiredFilesRaw;
std::vector<std::string> Depends;
std::string DependsRaw;
std::vector<std::string> AttachedFiles;
std::string AttachedFilesRaw;
std::vector<std::string> AttachOnFail;
std::string AttachOnFailRaw;
std::vector<std::pair<cmsys::RegularExpression, std::string>>
ErrorRegularExpressions;
std::string ErrorRegularExpressionsRaw;
std::vector<std::pair<cmsys::RegularExpression, std::string>>
RequiredRegularExpressions;
std::string RequiredRegularExpressionsRaw;
std::vector<std::pair<cmsys::RegularExpression, std::string>>
SkipRegularExpressions;
std::string SkipRegularExpressionsRaw;
std::vector<std::pair<cmsys::RegularExpression, std::string>>
TimeoutRegularExpressions;
std::string TimeoutRegularExpressionsRaw;
std::map<std::string, std::string> Measurements;
std::string MeasurementsRaw;
std::map<std::string, std::string> CustomProperties;
std::unordered_map<std::string, std::string> RawProperties;
bool IsInBasedOnREOptions = true;
bool WillFail = false;
std::string WillFailRaw;
bool Disabled = false;
std::string DisabledRaw;
float Cost = 0;
std::string CostRaw;
int PreviousRuns = 0;
bool RunSerial = false;
std::string RunSerialRaw;
cm::optional<cmDuration> Timeout;
std::string TimeoutRaw;
cm::optional<Signal> TimeoutSignal;
std::string TimeoutSignalRaw;
cm::optional<cmDuration> TimeoutGracePeriod;
std::string TimeoutGracePeriodRaw;
cmDuration AlternateTimeout;
int Index = 0;
// Requested number of process slots
int Processors = 1;
std::string ProcessorsRaw;
bool WantAffinity = false;
std::string WantAffinityRaw;
std::vector<size_t> Affinity;
// return code of test which will mark test as "not run"
int SkipReturnCode = -1;
std::string SkipReturnCodeRaw;
std::vector<std::string> Environment;
std::string EnvironmentRaw;
std::vector<std::string> EnvironmentModification;
std::string EnvironmentModificationRaw;
std::vector<std::string> Labels;
std::string LabelsRaw;
std::set<std::string> ProjectResources; // RESOURCE_LOCK
std::string ProjectResourcesRaw;
std::set<std::string> FixturesSetup;
std::string FixturesSetupRaw;
std::set<std::string> FixturesCleanup;
std::string FixturesCleanupRaw;
std::set<std::string> FixturesRequired;
std::string FixturesRequiredRaw;
std::set<std::string> RequireSuccessDepends;
// For the fixtures this test sets up or cleans up. A mode requested by
// any of a fixture's own tests wins over the default recorded for them.
cm::optional<FixtureRepeatMode> RequestedFixtureRepeatMode;
cm::optional<FixtureRepeatMode> DefaultFixtureRepeatMode;
std::vector<std::vector<cmCTestTestResourceRequirement>> ResourceGroups;
std::string ResourceGroupsRaw;
std::string GeneratedResourceSpecFile;
std::string GeneratedResourceSpecFileRaw;
std::string BuildDepends;
// Private test generator properties used to track backtraces
cmListFileBacktrace Backtrace;
};
struct cmCTestTestResult
{
std::string Name;
std::string Path;
std::string Reason;
std::string FullCommandLine;
std::string Environment;
cmDuration ExecutionTime = cmDuration::zero();
std::int64_t ReturnValue = 0;
int Status = NOT_RUN;
std::string ExceptionStatus;
bool CompressOutput;
std::string CompletionStatus;
std::string CustomCompletionStatus;
std::string Output;
std::string TestMeasurementsOutput;
std::string InstrumentationFile;
int TestCount = 0;
cm::optional<cmDuration> StartTestTime;
cmCTestTestProperties* Properties = nullptr;
};
struct cmCTestTestResultLess
{
bool operator()(cmCTestTestResult const& lhs,
cmCTestTestResult const& rhs) const
{
return lhs.TestCount < rhs.TestCount;
}
};
// add configurations to a search path for an executable
static void AddConfigurations(cmCTest* ctest,
std::vector<std::string>& attempted,
std::vector<std::string>& attemptedConfigs,
std::string filepath, std::string& filename);
// full signature static method to find an executable
static std::string FindExecutable(cmCTest* ctest,
std::string const& testCommand,
std::string& resultingConfig,
std::vector<std::string>& extraPaths,
std::vector<std::string>& failed);
static bool ParseResourceGroupsProperty(
std::string const& val,
std::vector<std::vector<cmCTestTestResourceRequirement>>& resourceGroups);
using ListOfTests = std::vector<cmCTestTestProperties>;
// Support for writing test results in JUnit XML format.
void SetJUnitXMLFileName(std::string const& id);
protected:
using SetOfTests =
std::set<cmCTestTestHandler::cmCTestTestResult, cmCTestTestResultLess>;
// compute a final test list
virtual int PreProcessHandler();
virtual int PostProcessHandler();
virtual void GenerateTestCommand(std::vector<std::string>& args, int test);
int ExecuteCommands(std::vector<std::string>& vec);
bool ProcessOptions();
void LogTestSummary(std::vector<std::string> const& passed,
std::vector<std::string> const& failed,
cmDuration durationInSecs);
void LogDisabledTests(std::vector<cmCTestTestResult> const& disabledTests);
void LogFailedTests(std::vector<std::string> const& failed,
SetOfTests const& resultsSet);
bool GenerateXML();
void WriteTestResultHeader(cmXMLWriter& xml,
cmCTestTestResult const& result);
void WriteTestResultFooter(cmXMLWriter& xml,
cmCTestTestResult const& result);
// Write attached test files into the xml
void AttachFiles(cmXMLWriter& xml, cmCTestTestResult& result);
void AttachFile(cmXMLWriter& xml, std::string const& file,
std::string const& name);
//! Clean test output to specified length and truncation mode
void CleanTestOutput(std::string& output, size_t length,
cmCTestTypes::TruncationMode truncate);
cmCTestTestOptions TestOptions;
cmDuration ElapsedTestingTime;
using TestResultsVector = std::vector<cmCTestTestResult>;
TestResultsVector TestResults;
std::vector<std::string> CustomTestsIgnore;
std::string StartTest;
std::string EndTest;
std::chrono::system_clock::time_point StartTestTime;
std::chrono::system_clock::time_point EndTestTime;
bool MemCheck = false;
int MaxIndex;
public:
enum
{ // Program statuses
NOT_RUN = 0,
TIMEOUT,
SEGFAULT,
ILLEGAL,
INTERRUPT,
NUMERICAL,
OTHER_FAULT,
FAILED,
BAD_COMMAND,
COMPLETED
};
private:
/**
* Write test results in CTest's Test.xml format
*/
virtual void GenerateCTestXML(cmXMLWriter& xml);
/**
* Write test results in JUnit XML format
*/
bool WriteJUnitXML();
void PrintLabelOrSubprojectSummary(bool isSubProject);
/**
* Run the tests for a directory and any subdirectories
*/
bool ProcessDirectory(std::vector<std::string>& passed,
std::vector<std::string>& failed);
/**
* Get the list of tests in directory and subdirectories.
*/
bool GetListOfTests();
// compute the lists of tests that will actually run
// based on union regex and -I stuff
bool ComputeTestList();
// compute the lists of tests that will actually run
// based on LastTestFailed.log
bool ComputeTestListForRerunFailed();
void ComputeOutOfDateTests();
// add required setup/cleanup tests not already in the
// list of tests to be run and update dependencies between
// tests to account for fixture setup/cleanup
void UpdateForFixtures(ListOfTests& tests) const;
void UpdateMaxTestNameWidth();
bool GetValue(char const* tag, std::string& value, std::istream& fin);
bool GetValue(char const* tag, int& value, std::istream& fin);
bool GetValue(char const* tag, size_t& value, std::istream& fin);
bool GetValue(char const* tag, bool& value, std::istream& fin);
bool GetValue(char const* tag, double& value, std::istream& fin);
/**
* Find the executable for a test
*/
std::string FindTheExecutable(std::string const& exe);
std::string GetTestStatus(cmCTestTestResult const&);
void ExpandTestsToRunInformation(size_t numPossibleTests);
void ExpandTestsToRunInformationForRerunFailed();
cm::optional<std::set<std::string>> ReadTestListFile(
std::string const& testListFileName) const;
std::vector<std::string> CustomPreTest;
std::vector<std::string> CustomPostTest;
std::vector<int> TestsToRun;
bool UseIncludeRegExpFlag = false;
bool UseExcludeRegExpFlag = false;
bool UseExcludeRegExpFirst = false;
std::vector<cmsys::RegularExpression> IncludeLabelRegularExpressions;
std::vector<cmsys::RegularExpression> ExcludeLabelRegularExpressions;
cmsys::RegularExpression IncludeTestsRegularExpression;
cmsys::RegularExpression ExcludeTestsRegularExpression;
cm::optional<std::set<std::string>> TestsToRunByName;
cm::optional<std::set<std::string>> TestsToExcludeByName;
cm::optional<std::string> ParallelLevel;
cm::optional<std::string> Repeat;
void RecordCustomTestMeasurements(cmXMLWriter& xml, std::string content);
void CheckLabelFilter(cmCTestTestProperties& it);
void CheckLabelFilterExclude(cmCTestTestProperties& it);
void CheckLabelFilterInclude(cmCTestTestProperties& it);
std::string TestsToRunString;
ListOfTests TestList;
size_t TotalNumberOfTests;
cmsys::RegularExpression AllTestMeasurementsRegex;
cmsys::RegularExpression SingleTestMeasurementRegex;
cmsys::RegularExpression CustomCompletionStatusRegex;
cmsys::RegularExpression CustomLabelRegex;
std::ostream* LogFile = nullptr;
cmCTest::Repeat RepeatMode = cmCTest::Repeat::Never;
int RepeatCount = 1;
friend class cmCTestTestCommand;
};