CTest: Add an option to set the output directory

CTest writes the files it produces during a run -- the TAG file, the
logs under Temporary, and the XML files staged for submission -- into a
single directory, which was always "Testing" under the build tree.  Two
ctest processes sharing a build tree therefore overwrite each other's
output.

Add a --output-dir command-line option, and a CTEST_OUTPUT_DIRECTORY
variable for dashboard scripts, to select that directory.  It defaults
to the previous location, may be outside the build tree, and resolves a
relative path against the binary directory.

Issue: #27518
This commit is contained in:
Tom Osika committed 2026-10-22 18:17:33 +00:00
1 parent 16700f729b
commit ad6be5a4dd
35 files changed
+417 -56

No files matched your search

+4
View File
@@ -58,6 +58,10 @@ The parameters are as follows:
in the first ``ctest_start()`` command, a warning will be issued, and the
new model and group will be used.
.. versionadded:: 4.6
The ``TAG`` file is read from, and written to, the output directory
selected by :variable:`CTEST_OUTPUT_DIRECTORY`.
``QUIET``
.. versionadded:: 3.3
+1
View File
@@ -799,6 +799,7 @@ Variables for CTest
/variable/CTEST_MEMORYCHECK_TYPE
/variable/CTEST_NIGHTLY_START_TIME
/variable/CTEST_NOTES_FILES
/variable/CTEST_OUTPUT_DIRECTORY
/variable/CTEST_P4_CLIENT
/variable/CTEST_P4_COMMAND
/variable/CTEST_P4_OPTIONS
+21
View File
@@ -472,6 +472,27 @@ The options for running tests are:
build directory. If not specified, the current directory is used.
See also :ctest-dashboard-option:`--build-dir`.
.. option:: --output-dir <path>
.. versionadded:: 4.6
Specify the directory in which to write output files.
CTest writes the files it produces during a run, such as the ``TAG`` file,
the logs under ``Temporary``, and the XML files submitted to a dashboard,
into a single output directory. By default this is the ``Testing``
directory under the build tree. This option writes them elsewhere; the
path may be outside the build tree, and a relative path is interpreted
with respect to the build directory.
This makes it possible to run more than one :program:`ctest` process
against the same build tree, for example to run different subsets of the
tests with :ctest-option:`--tests-from-file`, without the processes
overwriting each other's output.
When running a dashboard script, use the :variable:`CTEST_OUTPUT_DIRECTORY`
variable instead.
.. option:: --test-output-size-passed <size>
.. versionadded:: 3.4
+9
View File
@@ -0,0 +1,9 @@
ctest-output-dir
----------------
* :manual:`ctest(1)` gained an :option:`--output-dir <ctest --output-dir>`
option, and the corresponding :variable:`CTEST_OUTPUT_DIRECTORY` variable
for :ref:`Dashboard Client` scripts, to specify the directory in which
CTest writes its output files. Previously these were always written to
the ``Testing`` directory under the build tree, which prevented running
more than one :program:`ctest` process against the same build tree.
+22
View File
@@ -0,0 +1,22 @@
CTEST_OUTPUT_DIRECTORY
----------------------
.. versionadded:: 4.6
Specify the directory in which :manual:`ctest(1)` writes its output files
in a :ref:`Dashboard Client` script.
CTest writes the files it produces during a run, such as the ``TAG`` file,
the logs under ``Temporary``, and the XML files submitted to a dashboard,
into a single output directory. By default this is the ``Testing``
directory under :variable:`CTEST_BINARY_DIRECTORY`. Set this variable
before calling :command:`ctest_start` to write them elsewhere; the path may
be outside the build tree, and a relative path is interpreted with respect
to :variable:`CTEST_BINARY_DIRECTORY`.
This makes it possible to run more than one :program:`ctest` process
against the same build tree, for example to run different subsets of the
tests with the ``INCLUDE_FROM_FILE`` option of :command:`ctest_test`,
without the processes overwriting each other's output.
Outside of a dashboard script, use the :option:`ctest --output-dir` option.
+3
View File
@@ -237,6 +237,9 @@ function(ctest_coverage_collect_gcov)
return()
endif()
# setup the dir for the coverage files
# FIXME(#27518): This staging directory ignores CTEST_OUTPUT_DIRECTORY.
# Moving it is not a simple substitution: the .gcov files are
# globbed relative to binary_dir and tar runs from there.
set(coverage_dir "${binary_dir}/Testing/CoverageInfo")
file(MAKE_DIRECTORY "${coverage_dir}")
# run gcov, this will produce the .gcov files in the current
+3 -1
View File
@@ -10,7 +10,9 @@ if(NOT PROJECT_BINARY_DIR)
message(FATAL_ERROR "Do not include(CTest) before calling project().")
endif()
# make directories in the binary tree
# Make directories in the binary tree. This is only the default output
# directory: CTEST_OUTPUT_DIRECTORY and --output-dir are chosen per ctest
# invocation and are not known here, so ctest creates those itself at run time.
file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/Testing/Temporary)
get_filename_component(CMAKE_HOST_PATH ${CMAKE_COMMAND} PATH)
set(CMAKE_TARGET_PATH ${EXECUTABLE_OUTPUT_PATH})
+1 -2
View File
@@ -745,8 +745,7 @@ cmCTestBuildHandler::LaunchHelper::LaunchHelper(cmCTestBuildHandler* handler)
} else {
// Compute a directory in which to store launcher fragments.
std::string& launchDir = this->Handler->CTestLaunchDir;
launchDir =
cmStrCat(this->CTest->GetBinaryDir(), "/Testing/", tag, "/Build");
launchDir = cmStrCat(this->CTest->GetOutputDir(), '/', tag, "/Build");
// Clean out any existing launcher fragments.
cmSystemTools::RemoveADirectory(launchDir);
+8 -11
View File
@@ -36,13 +36,11 @@ using ConfigurePreset = cmCMakePresetsGraph::ConfigurePreset;
namespace {
bool ConstructConfigureCommand(cmExecutionStatus& status, cmMakefile& mf,
std::string const sourceDirectory,
std::string const buildDirectory,
std::string const options,
std::string const presetName,
std::string const presetsFile,
std::string& configureCommand)
bool ConstructConfigureCommand(
cmExecutionStatus& status, cmMakefile& mf, std::string const sourceDirectory,
std::string const buildDirectory, std::string const outputDirectory,
std::string const options, std::string const presetName,
std::string const presetsFile, std::string& configureCommand)
{
configureCommand =
cmStrCat('"', cmSystemTools::GetCMakeCommand(), "\" \"-S",
@@ -130,8 +128,7 @@ bool ConstructConfigureCommand(cmExecutionStatus& status, cmMakefile& mf,
cmValue buildName = mf.GetDefinition("CTEST_BUILD_NAME");
if (cmNonempty(site) || cmNonempty(buildName)) {
std::string const initialCacheFile =
cmStrCat(cmSystemTools::CollapseFullPath(buildDirectory),
"/Testing/Temporary/CTestConfigureInitialCache.cmake");
cmStrCat(outputDirectory, "/Temporary/CTestConfigureInitialCache.cmake");
cmGeneratedFileStream initialCache(initialCacheFile);
if (cmNonempty(site)) {
initialCache << "set(SITE " << cmOutputConverter::EscapeForCMake(*site)
@@ -244,8 +241,8 @@ bool cmCTestConfigureCommand::ExecuteConfigure(ConfigureArguments const& args,
}
if (configureCommand.empty() &&
!ConstructConfigureCommand(status, mf, sourceDirectory, buildDirectory,
args.Options, presetName, presetsFile,
configureCommand)) {
this->CTest->GetOutputDir(), args.Options,
presetName, presetsFile, configureCommand)) {
return false;
}
+11 -6
View File
@@ -43,8 +43,8 @@ cmCTestCoverageHandler::cmCTestCoverageHandler(cmCTest* ctest)
void cmCTestCoverageHandler::CleanCoverageLogFiles(std::ostream& log)
{
std::string logGlob =
cmStrCat(this->CTest->GetCTestConfiguration("BuildDirectory"), "/Testing/",
this->CTest->GetCurrentTag(), "/CoverageLog*");
cmStrCat(this->CTest->GetOutputDir(), '/', this->CTest->GetCurrentTag(),
"/CoverageLog*");
cmsys::Glob gl;
gl.FindFiles(logGlob);
std::vector<std::string> const& files = gl.GetFiles();
@@ -193,6 +193,7 @@ int cmCTestCoverageHandler::ProcessHandler()
std::string sourceDir =
this->CTest->GetCTestConfiguration("SourceDirectory");
std::string binaryDir = this->CTest->GetCTestConfiguration("BuildDirectory");
std::string outputDir = this->CTest->GetOutputDir();
if (binaryDir.empty()) {
cmCTestLog(this->CTest, ERROR_MESSAGE,
@@ -215,6 +216,7 @@ int cmCTestCoverageHandler::ProcessHandler()
cmSystemTools::ConvertToUnixSlashes(sourceDir);
cmSystemTools::ConvertToUnixSlashes(binaryDir);
cmSystemTools::ConvertToUnixSlashes(outputDir);
cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, this->Quiet,
"Performing coverage\n");
@@ -223,6 +225,7 @@ int cmCTestCoverageHandler::ProcessHandler()
cont.Error = error;
cont.SourceDir = sourceDir;
cont.BinaryDir = binaryDir;
cont.OutputDir = outputDir;
cont.OFS = &ofs;
cont.Quiet = this->Quiet;
@@ -876,7 +879,8 @@ int cmCTestCoverageHandler::HandleGCovCoverage(
return 0;
}
std::string testingDir = this->CTest->GetBinaryDir() + "/Testing";
// TODO(#27518): no test asserts this lands in the output directory.
std::string testingDir = this->CTest->GetOutputDir();
std::string tempDir = testingDir + "/CoverageInfo";
if (!cmSystemTools::MakeDirectory(tempDir)) {
cmCTestLog(this->CTest, ERROR_MESSAGE,
@@ -1403,7 +1407,6 @@ int cmCTestCoverageHandler::HandleLCovCoverage(
// No coverage files is a valid thing, so the exit code is 0
return 0;
}
std::string testingDir = this->CTest->GetBinaryDir();
std::set<std::string> missingFiles;
@@ -1683,7 +1686,8 @@ int cmCTestCoverageHandler::HandleTracePyCoverage(
return 0;
}
std::string testingDir = this->CTest->GetBinaryDir() + "/Testing";
// TODO(#27518): This directory is never used.
std::string testingDir = this->CTest->GetOutputDir();
std::string tempDir = testingDir + "/CoverageInfo";
cmSystemTools::MakeDirectory(tempDir);
@@ -1949,8 +1953,9 @@ int cmCTestCoverageHandler::RunBullseyeCommand(
}
// create a process object and start it
cmUVProcessChainBuilder builder;
// TODO(#27518): no test asserts this lands in the output directory.
std::string stdoutFile =
cmStrCat(cont->BinaryDir, "/Testing/Temporary/",
cmStrCat(cont->OutputDir, "/Temporary/",
this->GetCTestInstance()->GetCurrentTag(), '-', cmd);
std::string stderrFile = stdoutFile;
stdoutFile += ".stdout";
+1
View File
@@ -25,6 +25,7 @@ public:
int Error;
std::string SourceDir;
std::string BinaryDir;
std::string OutputDir;
using SingleFileCoverageVector = std::vector<int>;
using SingleFileBranchCoverageVector = std::vector<std::array<int, 2>>;
using TotalCoverageMap = std::map<std::string, SingleFileCoverageVector>;
+4 -4
View File
@@ -550,7 +550,7 @@ bool cmCTestMemCheckHandler::InitializeMemoryChecking()
cmSystemTools::ParseArguments(memoryTesterOptions);
this->MemoryTesterOutputFile =
this->CTest->GetBinaryDir() + "/Testing/Temporary/MemoryChecker.??.log";
this->CTest->GetOutputDir() + "/Temporary/MemoryChecker.??.log";
switch (this->MemoryTesterStyle) {
case cmCTestMemCheckHandler::VALGRIND: {
@@ -582,7 +582,7 @@ bool cmCTestMemCheckHandler::InitializeMemoryChecking()
}
case cmCTestMemCheckHandler::DRMEMORY: {
std::string tempDrMemoryDir =
this->CTest->GetBinaryDir() + "/Testing/Temporary/DrMemory";
this->CTest->GetOutputDir() + "/Temporary/DrMemory";
if (!cm::contains(this->MemoryTesterOptions, "-quiet")) {
this->MemoryTesterOptions.emplace_back("-quiet");
@@ -666,8 +666,8 @@ bool cmCTestMemCheckHandler::InitializeMemoryChecking()
}
case cmCTestMemCheckHandler::BOUNDS_CHECKER: {
this->BoundsCheckerXMLFile = this->MemoryTesterOutputFile;
std::string dpbdFile = this->CTest->GetBinaryDir() +
"/Testing/Temporary/MemoryChecker.??.DPbd";
std::string dpbdFile =
this->CTest->GetOutputDir() + "/Temporary/MemoryChecker.??.DPbd";
this->BoundsCheckerDPBDFile = dpbdFile;
this->MemoryTesterDynamicOptions.emplace_back("/B");
this->MemoryTesterDynamicOptions.push_back(std::move(dpbdFile));
+3 -3
View File
@@ -1377,7 +1377,7 @@ void cmCTestMultiProcessHandler::CreateSerialTestCostList()
void cmCTestMultiProcessHandler::WriteCheckpoint(int index)
{
std::string fname =
this->CTest->GetBinaryDir() + "/Testing/Temporary/CTestCheckpoint.txt";
this->CTest->GetOutputDir() + "/Temporary/CTestCheckpoint.txt";
cmsys::ofstream fout;
fout.open(fname.c_str(), std::ios::app);
fout << index << "\n";
@@ -1387,7 +1387,7 @@ void cmCTestMultiProcessHandler::WriteCheckpoint(int index)
void cmCTestMultiProcessHandler::MarkFinished()
{
std::string fname =
this->CTest->GetBinaryDir() + "/Testing/Temporary/CTestCheckpoint.txt";
this->CTest->GetOutputDir() + "/Temporary/CTestCheckpoint.txt";
cmSystemTools::RemoveFile(fname);
}
@@ -1925,7 +1925,7 @@ void cmCTestMultiProcessHandler::PrintLabels()
void cmCTestMultiProcessHandler::CheckResume()
{
std::string fname =
this->CTest->GetBinaryDir() + "/Testing/Temporary/CTestCheckpoint.txt";
this->CTest->GetOutputDir() + "/Temporary/CTestCheckpoint.txt";
if (this->CTest->GetFailover()) {
if (cmSystemTools::FileExists(fname, true)) {
*this->TestHandler->LogFile
+6 -2
View File
@@ -94,6 +94,10 @@ bool cmCTestStartCommand::InitialPass(std::vector<std::string> const& args,
this->CTest->SetCTestConfiguration("SourceDirectory", sourceDir, quiet);
this->CTest->SetCTestConfiguration("BuildDirectory", binaryDir, quiet);
cmValue out_dir = mf.GetDefinition("CTEST_OUTPUT_DIRECTORY");
std::string outputDir =
cmCTest::ComputeOutputDir(binaryDir, out_dir ? *out_dir : std::string());
if (smodel) {
cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, quiet,
"Run dashboard with model ", smodel,
@@ -120,7 +124,7 @@ bool cmCTestStartCommand::InitialPass(std::vector<std::string> const& args,
}
// Log startup actions.
std::string startLogFile = binaryDir + "/Testing/Temporary/LastStart.log";
std::string startLogFile = outputDir + "/Temporary/LastStart.log";
cmGeneratedFileStream ofs(startLogFile);
if (!ofs) {
cmCTestLog(this->CTest, ERROR_MESSAGE,
@@ -184,7 +188,7 @@ bool cmCTestStartCommand::InitialPass(std::vector<std::string> const& args,
this->CTest->SetCTestConfigurationFromCMakeVariable(
&mf, "BuildName", "CTEST_BUILD_NAME", quiet);
this->CTest->Initialize(binaryDir);
this->CTest->Initialize(binaryDir, outputDir);
cmCTestOptionalLog(
this->CTest, OUTPUT, quiet,
+1 -1
View File
@@ -14,7 +14,7 @@ class cmExecutionStatus;
/** \class cmCTestStart
* \brief Run a ctest script
*
* cmCTestStartCommand defineds the command to start the nightly testing.
* cmCTestStartCommand defines the command to start the nightly testing.
*/
class cmCTestStartCommand : public cmCTestCommand
{
+4 -11
View File
@@ -670,14 +670,7 @@ int cmCTestSubmitHandler::ProcessHandler()
this->CDashUploadType);
}
std::string const& buildDirectory =
this->CTest->GetCTestConfiguration("BuildDirectory");
if (buildDirectory.empty()) {
cmCTestLog(
this->CTest, ERROR_MESSAGE,
"Cannot find BuildDirectory key in the DartConfiguration.tcl\n");
return -1;
}
std::string const outputDirectory = this->CTest->GetOutputDir();
cmGeneratedFileStream ofs;
this->StartLogFile("Submit", ofs);
@@ -735,7 +728,7 @@ int cmCTestSubmitHandler::ProcessHandler()
if (this->CTest->AddIfExists(cmCTest::PartCoverage, "Coverage.xml")) {
std::vector<std::string> gfiles;
std::string gpath =
cmStrCat(buildDirectory, "/Testing/", this->CTest->GetCurrentTag());
cmStrCat(outputDirectory, '/', this->CTest->GetCurrentTag());
std::string::size_type glen = gpath.size() + 1;
gpath += "/CoverageLog*";
cmCTestOptionalLog(this->CTest, DEBUG, this->Quiet,
@@ -804,8 +797,8 @@ int cmCTestSubmitHandler::ProcessHandler()
cmCTestOptionalLog(this->CTest, HANDLER_OUTPUT, this->Quiet,
" SubmitURL: ", url, '\n');
if (!this->SubmitUsingHTTP(
cmStrCat(buildDirectory, "/Testing/", this->CTest->GetCurrentTag()),
files, prefix, url)) {
cmStrCat(outputDirectory, '/', this->CTest->GetCurrentTag()), files,
prefix, url)) {
cmCTestLog(this->CTest, ERROR_MESSAGE,
" Problems when submitting via HTTP\n");
ofs << " Problems when submitting via HTTP\n";
+2 -2
View File
@@ -2077,7 +2077,7 @@ void cmCTestTestHandler::ExpandTestsToRunInformation(size_t numTests)
void cmCTestTestHandler::ExpandTestsToRunInformationForRerunFailed()
{
std::string dirName = this->CTest->GetBinaryDir() + "/Testing/Temporary";
std::string dirName = this->CTest->GetOutputDir() + "/Temporary";
cmsys::Directory directory;
if (!directory.Load(dirName)) {
@@ -2109,7 +2109,7 @@ void cmCTestTestHandler::ExpandTestsToRunInformationForRerunFailed()
}
std::string lastTestsFailedLog =
cmStrCat(this->CTest->GetBinaryDir(), "/Testing/Temporary/", logName);
cmStrCat(this->CTest->GetOutputDir(), "/Temporary/", logName);
if (!cmSystemTools::FileExists(lastTestsFailedLog)) {
if (!this->CTest->GetShowOnly() && !this->CTest->ShouldPrintLabels()) {
+65 -9
View File
@@ -182,6 +182,12 @@ struct cmCTest::Private
std::string NotesFiles;
// Output directory as requested on the command line or by a script. It
// may be empty or relative; OutputDir holds the resolved absolute path.
std::string OutputDirArg;
std::string OutputDir;
// Default for the current mode; an explicit user choice takes precedence.
bool InteractiveDebugMode = true;
cm::optional<bool> InteractiveDebugModeCli;
@@ -303,7 +309,7 @@ std::string cmCTest::GetCostDataFile()
{
std::string fname = this->GetCTestConfiguration("CostDataFile");
if (fname.empty()) {
fname = this->GetBinaryDir() + "/Testing/Temporary/CTestCostData.txt";
fname = this->GetOutputDir() + "/Temporary/CTestCostData.txt";
}
return fname;
}
@@ -441,7 +447,8 @@ cmCTest::Part cmCTest::GetPartFromName(std::string const& name)
return PartCount;
}
void cmCTest::Initialize(std::string const& binary_dir)
void cmCTest::Initialize(std::string const& binary_dir,
std::string const& output_dir)
{
this->Impl->BuildID = "";
for (Part p = PartStart; p != PartCount; p = static_cast<Part>(p + 1)) {
@@ -456,11 +463,16 @@ void cmCTest::Initialize(std::string const& binary_dir)
this->Impl->BinaryDir = binary_dir;
cmSystemTools::ConvertToUnixSlashes(this->Impl->BinaryDir);
if (!output_dir.empty()) {
this->Impl->OutputDirArg = output_dir;
}
this->UpdateOutputDir();
}
bool cmCTest::CreateNewTag(bool quiet)
{
std::string const testingDir = this->Impl->BinaryDir + "/Testing";
std::string const testingDir = this->Impl->OutputDir;
std::string const tagfile = testingDir + "/TAG";
auto const result = cmSystemTools::MakeDirectory(testingDir);
@@ -504,7 +516,7 @@ bool cmCTest::CreateNewTag(bool quiet)
bool cmCTest::ReadExistingTag(bool quiet)
{
std::string const testingDir = this->Impl->BinaryDir + "/Testing";
std::string const testingDir = this->Impl->OutputDir;
std::string const tagfile = testingDir + "/TAG";
std::string tag;
@@ -624,6 +636,9 @@ bool cmCTest::UpdateCTestConfiguration()
cmSystemTools::SetLogicalWorkingDirectory(this->Impl->BinaryDir);
}
}
// The binary directory may have just changed, so resolve the output
// directory against its final value.
this->UpdateOutputDir();
this->Impl->TimeOut =
std::chrono::seconds(atoi(this->GetCTestConfiguration("TimeOut").c_str()));
std::string const& testLoad = this->GetCTestConfiguration("TestLoad");
@@ -690,7 +705,7 @@ bool cmCTest::SetTest(std::string const& ttype, bool report)
bool cmCTest::OpenOutputFile(std::string const& path, std::string const& name,
cmGeneratedFileStream& stream, bool compress)
{
std::string testingDir = this->Impl->BinaryDir + "/Testing";
std::string testingDir = this->Impl->OutputDir;
if (!path.empty()) {
testingDir = cmStrCat(std::move(testingDir), '/', path);
}
@@ -738,7 +753,7 @@ bool cmCTest::AddIfExists(Part part, std::string const& file)
bool cmCTest::CTestFileExists(std::string const& filename)
{
std::string testingDir = cmStrCat(this->Impl->BinaryDir, "/Testing/",
std::string testingDir = cmStrCat(this->Impl->OutputDir, '/',
this->Impl->CurrentTag, '/', filename);
return cmSystemTools::FileExists(testingDir);
}
@@ -827,6 +842,9 @@ int cmCTest::ProcessSteps()
std::string const binaryDir = resolveResult.Preset->BinaryDir;
this->SetCTestConfiguration("BuildDirectory", binaryDir);
this->Impl->BinaryDir = binaryDir;
// Resolve the output directory against the new binary directory.
// UpdateCTestConfiguration may return early without doing so.
this->UpdateOutputDir();
cmSystemTools::SetLogicalWorkingDirectory(binaryDir);
mf.AddDefinition("CTEST_BINARY_DIRECTORY", binaryDir);
// Re-parse DartConfiguration.tcl since we changed BinaryDir.
@@ -927,7 +945,7 @@ int cmCTest::ProcessSteps()
res |= cmCTest::MEMORY_ERRORS;
}
}
std::string notes_dir = this->Impl->BinaryDir + "/Testing/Notes";
std::string notes_dir = this->Impl->OutputDir + "/Notes";
if (cmSystemTools::FileIsDirectory(notes_dir)) {
cmsys::Directory d;
d.Load(notes_dir);
@@ -2358,6 +2376,11 @@ int cmCTest::Run(std::vector<std::string> const& args)
this->Impl->OutputTestOutputOnTestFailure = true;
return true;
} },
CommandArgument{ "--output-dir", CommandArgument::Values::One,
[this](std::string const& od) -> bool {
this->Impl->OutputDirArg = od;
return true;
} },
CommandArgument{
"--test-output-size-passed", CommandArgument::Values::One,
[this](std::string const& sz) -> bool {
@@ -2729,6 +2752,7 @@ int cmCTest::Run(std::vector<std::string> const& args)
return 1;
}
this->Impl->BinaryDir = workDir;
this->UpdateOutputDir();
// -D, -T, and/or -M was specified
if (processSteps) {
@@ -2832,7 +2856,7 @@ int cmCTest::ExecuteTests(std::vector<std::string> const& args)
cmCTestLog(this, ERROR_MESSAGE, "Errors while running CTest\n");
if (!this->Impl->OutputTestOutputOnTestFailure) {
std::string const lastTestLog =
this->GetBinaryDir() + "/Testing/Temporary/LastTest.log";
this->GetOutputDir() + "/Temporary/LastTest.log";
cmCTestLog(this, ERROR_MESSAGE,
"Output from these tests are in: ", lastTestLog, '\n');
cmCTestLog(this, ERROR_MESSAGE,
@@ -3096,7 +3120,39 @@ std::string cmCTest::GetBinaryDir()
std::string cmCTest::GetStampDir()
{
return this->Impl->BinaryDir + "/Testing/Temporary/LastTestRun";
return this->Impl->OutputDir + "/Temporary/LastTestRun";
}
std::string cmCTest::ComputeOutputDir(std::string const& binary_dir,
std::string const& output_dir)
{
if (output_dir.empty()) {
return cmStrCat(binary_dir, "/Testing");
}
// Expand a leading '~', normalize separators, collapse repeated slashes,
// and drop any trailing slash.
std::string dir = output_dir;
cmSystemTools::ConvertToUnixSlashes(dir);
// Resolve a relative path against the binary directory so that it means the
// same thing as the default value, "<binary-dir>/Testing".
return cmSystemTools::CollapseFullPath(dir, binary_dir);
}
void cmCTest::UpdateOutputDir()
{
this->Impl->OutputDir =
cmCTest::ComputeOutputDir(this->Impl->BinaryDir, this->Impl->OutputDirArg);
}
std::string cmCTest::GetOutputDir()
{
return this->Impl->OutputDir;
}
void cmCTest::SetOutputDir(std::string const& outdir)
{
this->Impl->OutputDirArg = outdir;
this->UpdateOutputDir();
}
std::string const& cmCTest::GetConfigType()
+28 -2
View File
@@ -66,8 +66,10 @@ public:
/** Process Command line arguments */
int Run(std::vector<std::string> const& args);
/** Initialize a dashboard run in the given build tree. */
void Initialize(std::string const& binary_dir);
/** Initialize a dashboard run in the given build tree, writing output
files to the given output directory. */
void Initialize(std::string const& binary_dir,
std::string const& output_dir);
bool CreateNewTag(bool quiet);
bool ReadExistingTag(bool quiet);
@@ -270,6 +272,26 @@ public:
/** Get the path to the test stamp file tree */
std::string GetStampDir();
/**
* Get / set the path to the output directory.
* By default, this is the binary directory + "/Testing".
* The stored path is always absolute, uses forward slashes, and has no
* trailing slash, so callers must supply their own separator when
* appending to it.
* */
std::string GetOutputDir();
void SetOutputDir(std::string const& outdir);
/**
* Compute the output directory of a dashboard run. An empty output_dir
* yields the default, binary_dir + "/Testing". Otherwise a leading '~' is
* expanded, separators are normalized, any trailing slash is removed, and a
* relative path is resolved against binary_dir so that it means the same
* thing as the default.
*/
static std::string ComputeOutputDir(std::string const& binary_dir,
std::string const& output_dir);
/**
* Get the short path to the file.
*
@@ -464,6 +486,10 @@ private:
/** apply CTEST_* variable definitions to the CTest configuration map */
void ApplyDefinitionsToCTestConfig();
/** recompute the output directory from the requested value and the
binary directory, which may not be known until late in the run */
void UpdateOutputDir();
/** set command line arguments read from a test preset */
bool SetArgsFromPreset(cmCMakePresetsArgs const& args);
+2
View File
@@ -166,6 +166,8 @@ cmDocumentationEntry const cmDocumentationOptions[] = {
"CPU load threshold for starting new parallel tests." },
{ "--tomorrow-tag", "Nightly or experimental starts with next day tag." },
{ "--overwrite <option-name>", "Overwrite CTest configuration option." },
{ "--output-dir <path>",
"Specify the directory in which to write output files." },
{ "--extra-submit <file>[;<file>]", "Submit extra files to the dashboard." },
{ "--http-header <header>", "Append HTTP header when submitting" },
{ "--schedule-random", "Use a random order for scheduling tests" },
+8 -2
View File
@@ -8,6 +8,12 @@ cmake_minimum_required(VERSION @CMAKE_VERSION@)
set(CTEST_SOURCE_DIRECTORY "@PROJECT_SOURCE_DIR@")
set(CTEST_BINARY_DIRECTORY "@PROJECT_BINARY_DIR@")
# Where CTest writes its output files. May be set on the command line to
# keep separate ctest runs over one build tree from overwriting each other.
if(NOT DEFINED CTEST_OUTPUT_DIRECTORY)
set(CTEST_OUTPUT_DIRECTORY "${CTEST_BINARY_DIRECTORY}/Testing")
endif()
# CTest Update Step
set(CTEST_UPDATE_COMMAND "@UPDATE_COMMAND@")
set(CTEST_UPDATE_OPTIONS "@UPDATE_OPTIONS@")
@@ -92,7 +98,7 @@ endif()
set(_exit_code 0)
if("Start" IN_LIST ACTIONS OR NOT EXISTS "${CTEST_BINARY_DIRECTORY}/Testing/TAG")
if("Start" IN_LIST ACTIONS OR NOT EXISTS "${CTEST_OUTPUT_DIRECTORY}/TAG")
ctest_start("${MODEL}")
else()
ctest_start("${MODEL}" APPEND)
@@ -148,7 +154,7 @@ if("MemCheck" IN_LIST ACTIONS)
endif()
file(GLOB notes_files LIST_DIRECTORIES OFF
"${CTEST_BINARY_DIRECTORY}/Testing/Notes/*")
"${CTEST_OUTPUT_DIRECTORY}/Notes/*")
if(notes_files)
list(APPEND CTEST_NOTES_FILES "${notes_files}")
endif()
@@ -1379,3 +1379,26 @@ run_TestMaxWidthShort(MaxWidthShort0 "--max-width 0")
run_TestMaxWidthLong(MaxWidthLongUnset "")
run_TestMaxWidthLong(MaxWidthLong70 "-W 70")
run_TestMaxWidthLong(MaxWidthLong30 "-W 30")
# Test --output-dir. This project only calls enable_testing(), so nothing
# creates the default "Testing" directory under the build tree.
block()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/SetOutputDir-build)
set(OUTPUT_DIR "${RunCMake_TEST_BINARY_DIR}/CLIOutputDir")
run_cmake(SetOutputDir-cmake)
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(SetOutputDir-ctest
${CMAKE_CTEST_COMMAND} -C Debug --output-dir ${OUTPUT_DIR})
endblock()
# Test --output-dir. This project calls include(CTest), which creates the
# default "Testing" directory at configure time, so check that it is left
# unused rather than absent.
block()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/SetOutputDirConfigured-build)
set(OUTPUT_DIR "${RunCMake_TEST_BINARY_DIR}/CLIOutputDir")
run_cmake(SetOutputDirConfigured-cmake)
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(SetOutputDirConfigured-ctest
${CMAKE_CTEST_COMMAND} -C Debug --output-dir ${OUTPUT_DIR})
endblock()
@@ -0,0 +1,2 @@
enable_testing()
add_test(NAME test1 COMMAND "${CMAKE_COMMAND}" -E true)
@@ -0,0 +1,14 @@
# This project only calls enable_testing(), so the default output
# directory should never be created at all.
set(DEFAULT_OUTPUT_DIR "${RunCMake_TEST_BINARY_DIR}/Testing")
if(EXISTS "${DEFAULT_OUTPUT_DIR}")
string(APPEND RunCMake_TEST_FAILED
"Default output directory should not exist:\n ${DEFAULT_OUTPUT_DIR}\n")
elseif(NOT EXISTS "${OUTPUT_DIR}")
set(RunCMake_TEST_FAILED
"Expected output directory ${OUTPUT_DIR} does not exist.")
elseif(NOT EXISTS "${OUTPUT_DIR}/Temporary/LastTest.log")
set(RunCMake_TEST_FAILED
"Expected ${OUTPUT_DIR}/Temporary/LastTest.log to exist.")
endif()
@@ -0,0 +1,2 @@
include(CTest)
add_test(NAME test1 COMMAND "${CMAKE_COMMAND}" -E true)
@@ -0,0 +1,27 @@
# The project calls include(CTest), which creates the default output
# directory at configure time. It must still be left unused.
set(DEFAULT_OUTPUT_DIR "${RunCMake_TEST_BINARY_DIR}/Testing")
if(NOT EXISTS "${DEFAULT_OUTPUT_DIR}")
set(RunCMake_TEST_FAILED
"Expected include(CTest) to create ${DEFAULT_OUTPUT_DIR}.")
return()
endif()
file(GLOB_RECURSE DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_DIR}/*")
if(DEFAULT_OUTPUT_FILES)
string(REPLACE ";" "\n " DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_FILES}")
string(APPEND RunCMake_TEST_FAILED
"Default output directory should not have been used:\n"
" ${DEFAULT_OUTPUT_DIR}\n"
"It contains:\n ${DEFAULT_OUTPUT_FILES}\n")
return()
endif()
if(NOT EXISTS "${OUTPUT_DIR}")
set(RunCMake_TEST_FAILED
"Expected output directory ${OUTPUT_DIR} does not exist.")
elseif(NOT EXISTS "${OUTPUT_DIR}/Temporary/LastTest.log")
set(RunCMake_TEST_FAILED
"Expected ${OUTPUT_DIR}/Temporary/LastTest.log to exist.")
endif()
@@ -215,3 +215,10 @@ if(RunCMake_GENERATOR MATCHES "Ninja")
run_NinjaLauncherSignalBuildFailure()
endif()
endif()
block()
set(CASE SetOutputDir)
set(DEFAULT_OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/Testing")
set(OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/CTestBuildOutputDir")
run_ctest_build(${CASE})
endblock()
@@ -0,0 +1,39 @@
# ctest_configure() runs include(CTest), which creates the default output
# directory at configure time. Check that nothing was written into it.
file(GLOB_RECURSE DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_DIR}/*")
if(DEFAULT_OUTPUT_FILES)
string(REPLACE ";" "\n " DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_FILES}")
string(APPEND RunCMake_TEST_FAILED
"Default output directory should not have been used:\n"
" ${DEFAULT_OUTPUT_DIR}\n"
"It contains:\n ${DEFAULT_OUTPUT_FILES}\n")
return()
endif()
set(TAG_PATH "${OUTPUT_DIR}/TAG")
if(NOT EXISTS "${TAG_PATH}")
set(RunCMake_TEST_FAILED "Expected ${TAG_PATH} file to exist.")
return()
endif()
file(STRINGS "${TAG_PATH}" TAG_CONTENTS)
if(TAG_CONTENTS STREQUAL "")
set(RunCMake_TEST_FAILED "${TAG_PATH} is empty.")
return()
endif()
list(GET TAG_CONTENTS 0 TAG)
set(EXPECTED_FILES
"${OUTPUT_DIR}/Temporary/LastStart.log"
"${OUTPUT_DIR}/Temporary/LastConfigure_${TAG}.log"
"${OUTPUT_DIR}/Temporary/LastBuild_${TAG}.log"
"${OUTPUT_DIR}/${TAG}/Configure.xml"
"${OUTPUT_DIR}/${TAG}/Build.xml"
)
foreach(EXP_FILE IN LISTS EXPECTED_FILES)
if(NOT EXISTS "${EXP_FILE}")
set(RunCMake_TEST_FAILED "Expected ${EXP_FILE} file to exist.")
return()
endif()
endforeach()
+3
View File
@@ -13,6 +13,9 @@ set(CTEST_USE_LAUNCHERS "@RunCMake_USE_LAUNCHERS@")
if (@RunCMake_USE_CUSTOM_BUILD_COMMAND@)
set(CTEST_BUILD_COMMAND "\"@RunCMake_BUILD_COMMAND@\"")
endif()
if("@CASE_NAME@" MATCHES "^SetOutputDir$")
set(CTEST_OUTPUT_DIRECTORY "@OUTPUT_DIR@")
endif()
set(ctest_build_args "@CASE_CTEST_BUILD_ARGS@")
ctest_start(Experimental)
@@ -8,3 +8,10 @@ function(run_ctest_coverage CASE_NAME)
endfunction()
run_ctest_coverage(CoverageQuiet QUIET)
block()
set(CASE SetOutputDir)
set(DEFAULT_OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/Testing")
set(OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/CTestCoverageOutputDir")
run_ctest_coverage(${CASE} QUIET)
endblock()
@@ -0,0 +1,42 @@
# ctest_configure() runs include(CTest), which creates the default output
# directory at configure time. Check that nothing was written into it.
file(GLOB_RECURSE DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_DIR}/*")
if(DEFAULT_OUTPUT_FILES)
string(REPLACE ";" "\n " DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_FILES}")
string(APPEND RunCMake_TEST_FAILED
"Default output directory should not have been used:\n"
" ${DEFAULT_OUTPUT_DIR}\n"
"It contains:\n ${DEFAULT_OUTPUT_FILES}\n")
return()
endif()
set(TAG_PATH "${OUTPUT_DIR}/TAG")
if(NOT EXISTS "${TAG_PATH}")
set(RunCMake_TEST_FAILED "Expected ${TAG_PATH} file to exist.")
return()
endif()
file(STRINGS "${TAG_PATH}" TAG_CONTENTS)
if(TAG_CONTENTS STREQUAL "")
set(RunCMake_TEST_FAILED "${TAG_PATH} is empty.")
return()
endif()
list(GET TAG_CONTENTS 0 TAG)
set(EXPECTED_FILES
"${OUTPUT_DIR}/Temporary/LastStart.log"
"${OUTPUT_DIR}/Temporary/LastConfigure_${TAG}.log"
"${OUTPUT_DIR}/Temporary/LastBuild_${TAG}.log"
"${OUTPUT_DIR}/${TAG}/Configure.xml"
"${OUTPUT_DIR}/${TAG}/Build.xml"
# Produced from the coverage data the test script leaves in the build tree.
"${OUTPUT_DIR}/${TAG}/Coverage.xml"
"${OUTPUT_DIR}/${TAG}/CoverageLog-0.xml"
)
foreach(EXP_FILE IN LISTS EXPECTED_FILES)
if(NOT EXISTS "${EXP_FILE}")
set(RunCMake_TEST_FAILED "Expected ${EXP_FILE} file to exist.")
return()
endif()
endforeach()
@@ -9,10 +9,28 @@ set(CTEST_CMAKE_GENERATOR_PLATFORM "@RunCMake_GENERATOR_PLATFORM@")
set(CTEST_CMAKE_GENERATOR_TOOLSET "@RunCMake_GENERATOR_TOOLSET@")
set(CTEST_BUILD_CONFIGURATION "$ENV{CMAKE_CONFIG_TYPE}")
set(CTEST_COVERAGE_COMMAND "@COVERAGE_COMMAND@")
if("@CASE_NAME@" MATCHES "^SetOutputDir$")
set(CTEST_OUTPUT_DIRECTORY "@OUTPUT_DIR@")
endif()
set(ctest_coverage_args "@CASE_CTEST_COVERAGE_ARGS@")
ctest_start(Experimental)
ctest_configure()
ctest_build()
ctest_test()
if("@CASE_NAME@" MATCHES "^SetOutputDir$")
# Leave Cobertura-style coverage data in the build tree so that
# ctest_coverage() produces real output files to check. No coverage
# tool runs; the handler just parses what it finds.
file(WRITE "${CTEST_BINARY_DIRECTORY}/covsrc/main.cpp" "int main(){return 0;}\n")
file(WRITE "${CTEST_BINARY_DIRECTORY}/coverage.xml"
"<?xml version=\"1.0\"?>
<coverage>
<sources><source>${CTEST_BINARY_DIRECTORY}/covsrc</source></sources>
<packages><package><classes>
<class filename=\"main.cpp\"><lines><line number=\"1\" hits=\"1\"/></lines></class>
</classes></package></packages>
</coverage>
")
endif()
ctest_coverage(${ctest_coverage_args})
@@ -51,6 +51,13 @@ run_ctest_start(TooManyArgs Experimental
run_ctest_start(NoStartTimeExperimental Experimental)
run_ctest_start(NoStartTimeNightly Nightly)
block()
set(CASE SetOutputDir)
set(DEFAULT_OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/Testing")
set(OUTPUT_DIR "${RunCMake_BINARY_DIR}/${CASE}-build/CTestStartOutputDir")
run_ctest_start(${CASE} Experimental)
endblock()
function(run_ConfigInBuild)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ConfigInBuild-build)
set(RunCMake_TEST_NO_CLEAN 1)
@@ -0,0 +1,16 @@
# This case never configures the project, so nothing creates the default
# output directory. Check that the run did not create it either.
file(GLOB_RECURSE DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_DIR}/*")
if(DEFAULT_OUTPUT_FILES)
string(REPLACE ";" "\n " DEFAULT_OUTPUT_FILES "${DEFAULT_OUTPUT_FILES}")
string(APPEND RunCMake_TEST_FAILED
"Default output directory should not have been used:\n"
" ${DEFAULT_OUTPUT_DIR}\n"
"It contains:\n ${DEFAULT_OUTPUT_FILES}\n")
elseif(NOT EXISTS "${OUTPUT_DIR}/TAG")
set(RunCMake_TEST_FAILED
"Expected ${OUTPUT_DIR}/TAG file to exist.")
elseif(NOT EXISTS "${OUTPUT_DIR}/Temporary/LastStart.log")
set(RunCMake_TEST_FAILED
"Expected ${OUTPUT_DIR}/Temporary/LastStart.log file to exist.")
endif()
+3
View File
@@ -11,6 +11,9 @@ set(CTEST_BUILD_CONFIGURATION "$ENV{CMAKE_CONFIG_TYPE}")
if(NOT "@CASE_NAME@" MATCHES "^NoStartTime")
set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC")
endif()
if("@CASE_NAME@" MATCHES "^SetOutputDir$")
set(CTEST_OUTPUT_DIRECTORY "@OUTPUT_DIR@")
endif()
function(setup_tests)
ctest_start(${ctest_start_args})