ctest: Respect test envvars set by CTEST_PRESET

Note the special attention given to support CTest environment variables
from inside a test preset's `environment` field.

Fixes: #28079
This commit is contained in:
Tyler Yankee
2026-09-16 09:34:43 -04:00
parent 277236a99a
commit f745dd11ab
10 changed files with 219 additions and 2 deletions
+40 -2
View File
@@ -2,8 +2,10 @@
file LICENSE.rst or https://cmake.org/licensing for details. */ file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmCTestTestCommand.h" #include "cmCTestTestCommand.h"
#include <cassert>
#include <chrono> #include <chrono>
#include <cstdlib> #include <cstdlib>
#include <map>
#include <ratio> #include <ratio>
#include <sstream> #include <sstream>
#include <string> #include <string>
@@ -105,13 +107,49 @@ cmCTestTestCommand::ResolveTestPreset(cmMakefile& mf,
return cm::optional<ResolvedTestPreset>(std::move(resolved)); return cm::optional<ResolvedTestPreset>(std::move(resolved));
} }
bool cmCTestTestCommand::ExecuteHandlerCommand(TestArguments& args,
cmExecutionStatus& status) const
{
cmMakefile& mf = status.GetMakefile();
// Resolve the preset once here (rather than letting InitializeHandler()
// resolve it again) so its Environment can be applied for the duration of
// the whole test run, without parsing the presets file (and emitting errors,
// etc.) more than once.
this->CachedPresetResolution.emplace(
ResolveTestPreset(mf, args.Preset, args.PresetsFile, status));
cmSystemTools::SaveRestoreEnvironment restoreEnv;
if (*this->CachedPresetResolution &&
(*this->CachedPresetResolution)->ExpandedPreset) {
for (auto const& var :
(*this->CachedPresetResolution)->ExpandedPreset->Environment) {
if (var.second) {
cmSystemTools::PutEnv(cmStrCat(var.first, '=', *var.second));
}
}
}
// Special case for CTest environment variables specified in a preset, which
// are handled by cmCTest much earlier than here, and would otherwise be
// ignored.
if (!this->CTest->UpdateStateFromEnvironment()) {
return false;
}
return cmCTestHandlerCommand::ExecuteHandlerCommand(args, status);
}
std::unique_ptr<cmCTestGenericHandler> cmCTestTestCommand::InitializeHandler( std::unique_ptr<cmCTestGenericHandler> cmCTestTestCommand::InitializeHandler(
HandlerArguments& arguments, cmExecutionStatus& status) const HandlerArguments& arguments, cmExecutionStatus& status) const
{ {
cmMakefile& mf = status.GetMakefile(); cmMakefile& mf = status.GetMakefile();
auto& args = static_cast<TestArguments&>(arguments); auto& args = static_cast<TestArguments&>(arguments);
auto resolvedPreset =
ResolveTestPreset(mf, args.Preset, args.PresetsFile, status); assert(this->CachedPresetResolution);
cm::optional<ResolvedTestPreset> resolvedPreset =
std::move(*this->CachedPresetResolution);
this->CachedPresetResolution.reset();
if (!resolvedPreset) { if (!resolvedPreset) {
return nullptr; return nullptr;
} }
+20
View File
@@ -25,6 +25,17 @@ class cmCTestTestCommand : public cmCTestHandlerCommand
public: public:
using cmCTestHandlerCommand::cmCTestHandlerCommand; using cmCTestHandlerCommand::cmCTestHandlerCommand;
cmCTestTestCommand(cmCTestTestCommand const& other)
: cmCTestHandlerCommand(other)
{
}
cmCTestTestCommand& operator=(cmCTestTestCommand const& other)
{
cmCTestHandlerCommand::operator=(other);
return *this;
}
protected: protected:
struct TestArguments : HandlerArguments struct TestArguments : HandlerArguments
{ {
@@ -54,6 +65,9 @@ protected:
std::string PresetsFile; std::string PresetsFile;
}; };
bool ExecuteHandlerCommand(TestArguments& args,
cmExecutionStatus& status) const;
template <typename Args> template <typename Args>
static auto MakeTestParser() -> cmArgumentParser<Args> static auto MakeTestParser() -> cmArgumentParser<Args>
{ {
@@ -101,6 +115,12 @@ private:
cmMakefile& mf, std::string const& presetArg, cmMakefile& mf, std::string const& presetArg,
std::string const& presetsFileArg, cmExecutionStatus& status) const; std::string const& presetsFileArg, cmExecutionStatus& status) const;
// Set by ExecuteHandlerCommand() (resolves the preset once, up
// front) and consumed by InitializeHandler() to avoid parsing the presets
// file (and emitting error messages, etc.) more than once.
mutable cm::optional<cm::optional<ResolvedTestPreset>>
CachedPresetResolution;
virtual std::unique_ptr<cmCTestTestHandler> InitializeActualHandler( virtual std::unique_ptr<cmCTestTestHandler> InitializeActualHandler(
HandlerArguments& arguments, cmExecutionStatus& status) const; HandlerArguments& arguments, cmExecutionStatus& status) const;
@@ -869,6 +869,84 @@ block()
-V) -V)
endblock() endblock()
# Environment variables from a test preset selected by -D CTEST_PRESET are
# applied when dashboard mode runs the Test step.
block()
set(src "${RunCMake_BINARY_DIR}/TestPresetCLIVarEnvironment")
set(bin "${RunCMake_BINARY_DIR}/TestPresetCLIVarEnvironment-build")
file(REMOVE_RECURSE "${src}" "${bin}")
file(MAKE_DIRECTORY "${src}" "${bin}")
configure_file("${RunCMake_SOURCE_DIR}/TestPresetCLIVarEnvironment-CMakePresets.json.in"
"${src}/CMakePresets.json" @ONLY)
file(WRITE "${bin}/DartConfiguration.tcl"
"BuildDirectory: ${bin}\n"
"SourceDirectory: ${src}\n")
file(WRITE "${bin}/CTestTestfile.cmake" "
add_test(print-env \"${CMAKE_COMMAND}\" -E environment)
set_tests_properties(print-env PROPERTIES PASS_REGULAR_EXPRESSION \"DASHBOARD_PRESET_ENV=from-preset\")
")
set(RunCMake_TEST_SOURCE_DIR "${src}")
set(RunCMake_TEST_BINARY_DIR "${bin}")
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(TestPresetCLIVarEnvironment
${CMAKE_CTEST_COMMAND}
-M Experimental
-D "CTEST_PRESET=my-test-preset"
-T Test
-V)
endblock()
# CTEST_PARALLEL_LEVEL set by a test preset's environment is respected.
block()
set(src "${RunCMake_BINARY_DIR}/TestPresetParallelLevelEnvironment")
set(bin "${RunCMake_BINARY_DIR}/TestPresetParallelLevelEnvironment-build")
file(REMOVE_RECURSE "${src}" "${bin}")
file(MAKE_DIRECTORY "${src}" "${bin}")
configure_file("${RunCMake_SOURCE_DIR}/TestPresetParallelLevelEnvironment-CMakePresets.json.in"
"${src}/CMakePresets.json" @ONLY)
file(WRITE "${bin}/DartConfiguration.tcl"
"BuildDirectory: ${bin}\n"
"SourceDirectory: ${src}\n")
file(WRITE "${bin}/CTestTestfile.cmake" "
foreach(i RANGE 1 6)
add_test(test\${i} \"${CMAKE_COMMAND}\" -E true)
endforeach()
")
set(RunCMake_TEST_SOURCE_DIR "${src}")
set(RunCMake_TEST_BINARY_DIR "${bin}")
set(RunCMake_TEST_NO_CLEAN 1)
# Spoof a number of processors to make these tests predictable.
set(ENV{__CTEST_FAKE_PROCESSOR_COUNT_FOR_TESTING} 1)
run_cmake_command(TestPresetParallelLevelEnvironment
${CMAKE_CTEST_COMMAND}
-M Experimental
-D "CTEST_PRESET=my-test-preset"
-T Test)
unset(ENV{__CTEST_FAKE_PROCESSOR_COUNT_FOR_TESTING})
endblock()
# CTEST_NO_TESTS_ACTION set by a test preset's environment is respected.
block()
set(src "${RunCMake_BINARY_DIR}/TestPresetNoTestsActionEnvironment")
set(bin "${RunCMake_BINARY_DIR}/TestPresetNoTestsActionEnvironment-build")
file(REMOVE_RECURSE "${src}" "${bin}")
file(MAKE_DIRECTORY "${src}" "${bin}")
configure_file("${RunCMake_SOURCE_DIR}/TestPresetNoTestsActionEnvironment-CMakePresets.json.in"
"${src}/CMakePresets.json" @ONLY)
file(WRITE "${bin}/DartConfiguration.tcl"
"BuildDirectory: ${bin}\n"
"SourceDirectory: ${src}\n")
file(WRITE "${bin}/CTestTestfile.cmake" "")
set(RunCMake_TEST_SOURCE_DIR "${src}")
set(RunCMake_TEST_BINARY_DIR "${bin}")
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(TestPresetNoTestsActionEnvironment
${CMAKE_CTEST_COMMAND}
-M Experimental
-D "CTEST_PRESET=my-test-preset"
-T Test)
endblock()
# Test --output-junit # Test --output-junit
function(run_output_junit) function(run_output_junit)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/output-junit) set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/output-junit)
@@ -0,0 +1,24 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [
{
"name": "my-configure-preset",
"generator": "@RunCMake_GENERATOR@",
"binaryDir": "${sourceDir}/build"
}
],
"testPresets": [
{
"name": "my-test-preset",
"configurePreset": "my-configure-preset",
"environment": {
"DASHBOARD_PRESET_ENV": "from-preset"
}
}
]
}
@@ -0,0 +1 @@
DASHBOARD_PRESET_ENV=from-preset
@@ -0,0 +1,24 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [
{
"name": "my-configure-preset",
"generator": "@RunCMake_GENERATOR@",
"binaryDir": "${sourceDir}/build"
}
],
"testPresets": [
{
"name": "my-test-preset",
"configurePreset": "my-configure-preset",
"environment": {
"CTEST_NO_TESTS_ACTION": "error"
}
}
]
}
@@ -0,0 +1 @@
No tests were found!!!
@@ -0,0 +1,24 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 21,
"patch": 0
},
"configurePresets": [
{
"name": "my-configure-preset",
"generator": "@RunCMake_GENERATOR@",
"binaryDir": "${sourceDir}/build"
}
],
"testPresets": [
{
"name": "my-test-preset",
"configurePreset": "my-configure-preset",
"environment": {
"CTEST_PARALLEL_LEVEL": "3"
}
}
]
}
@@ -0,0 +1,6 @@
Test project [^
]*/Tests/RunCMake/CTestCommandLine/TestPresetParallelLevelEnvironment-build
Start [0-9]+: test[0-9]+
Start [0-9]+: test[0-9]+
Start [0-9]+: test[0-9]+
1/6 Test #[0-9]+: test[0-9]+ \.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\.\. Passed +[0-9\.]+ sec