ctest: Add support for forwarding arguments to tests via --

Add support for `ctest [options] -- <args>` to forward extra arguments
to every executed test's command line. Arguments after `--` are appended
after each test's own arguments in the order given.

This enables passing options to test frameworks like Google Test or
doctest without needing per-test ARGS properties:

    ctest -R my_test -- --gtest_filter=SomeTest.Case
    ctest --verbose -- --success

An error is raised if `--` is combined with `--build-and-test`, `-S`,
or `-D`/`-T`/`-M` modes, which have their own argument forwarding.
A bare `--` with no following arguments is silently ignored.

Fixes: #20470
This commit is contained in:
Mickaël Germain committed 2026-03-13 16:21:02 -07:00
1 parent 116272e296
commit b3980bf703
15 files changed
+141 -2

No files matched your search

+23 -2
View File
@@ -11,7 +11,7 @@ Synopsis
.. parsed-literal::
`Run Tests`_
ctest [<options>...] [--test-dir <path-to-build>]
ctest [<options>...] [--test-dir <path-to-build>] [-- <test-args>...]
`Build and Test Mode`_
ctest --build-and-test <path-to-source> <path-to-build>
@@ -47,7 +47,7 @@ typically in a build tree generated by :manual:`cmake(1)`:
.. code-block:: shell
ctest [<options>...] [--test-dir <path-to-build>]
ctest [<options>...] [--test-dir <path-to-build>] [-- <test-args>...]
If all tests pass, the exit code is ``0``. The exit code is non-zero
in the following cases:
@@ -536,6 +536,27 @@ The options for running tests are:
the :ref:`cmake-instrumentation Indexing` section of CMake instrumentation for
more details.
.. option:: -- <test-args>...
.. versionadded:: 4.4
Forward extra arguments to test executables.
All arguments after ``--`` are appended to the end of every executed
test's command line. This is useful for passing options to test
frameworks like Google Test or doctest. For example:
.. code-block:: shell
ctest -R my_test -- --gtest_filter=SomeTest.Case
ctest --verbose -- --success
All selected tests receive the same extra arguments.
Use :option:`-R <ctest -R>` or :option:`-L <ctest -L>` to limit
which tests are executed.
A bare ``--`` with no following arguments is silently ignored.
View Help
=========
@@ -0,0 +1,5 @@
ctest-passthrough-args
----------------------
* :manual:`ctest(1)` gained support for forwarding extra arguments to test
executables using a :option:`-- <ctest -->` separator.
+6
View File
@@ -707,6 +707,12 @@ void cmCTestRunTest::ComputeArguments()
testCommand += "\"";
this->Arguments.push_back(*j);
}
// Append passthrough arguments from ctest command line (after --)
for (std::string const& arg :
this->TestHandler->TestOptions.TestPassthroughArguments) {
testCommand = cmStrCat(std::move(testCommand), " \"", arg, '"');
this->Arguments.push_back(arg);
}
this->TestResult.FullCommandLine = testCommand;
// Print the test command in verbose mode
+2
View File
@@ -55,6 +55,8 @@ struct cmCTestTestOptions
std::string ExcludeTestListFile;
std::string ResourceSpecFile;
std::string JUnitXMLFileName;
std::vector<std::string> TestPassthroughArguments;
};
/** \class cmCTestTestHandler
+34
View File
@@ -12,6 +12,7 @@
#include <functional>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <map>
#include <ratio>
#include <sstream>
@@ -2506,8 +2507,20 @@ int cmCTest::Run(std::vector<std::string> const& args)
};
// process the command line arguments
bool hadPassthroughDelimiter = false;
for (size_t i = 1; i < args.size(); ++i) {
std::string const& arg = args[i];
// The "--" argument terminates processing and passes
// all remaining arguments through to the tests.
if (arg == "--"_s) {
hadPassthroughDelimiter = true;
std::copy(
args.begin() + i + 1, args.end(),
std::back_inserter(this->Impl->TestOptions.TestPassthroughArguments));
break;
}
bool matched = false;
for (auto const& m : arguments) {
if (m.matches(arg)) {
@@ -2580,6 +2593,27 @@ int cmCTest::Run(std::vector<std::string> const& args)
}
}
// Passthrough arguments (after --) are only supported in direct test
// execution mode, not --build-and-test, -S, or -D/-T/-M modes.
if (hadPassthroughDelimiter) {
if (cmakeAndTest) {
cmSystemTools::Error(
"The -- option cannot be used with --build-and-test. "
"Use --build-options or --test-command to forward arguments.");
return 1;
}
if (!runScripts.empty()) {
cmSystemTools::Error(
"The -- option cannot be used with -S or -SP script mode.");
return 1;
}
if (processSteps) {
cmSystemTools::Error(
"The -- option cannot be used with -D, -T, or -M dashboard mode.");
return 1;
}
}
// TestProgressOutput only supported if console supports it and not logging
// to a file
this->Impl->TestProgressOutput = this->Impl->TestProgressOutput &&
@@ -0,0 +1,6 @@
1: Test command: [^
]* "-E" "echo" "base_output" "--extra-flag"
.*
2: Test command: [^
]* "-E" "echo" "second_test" "--extra-flag"
.*
@@ -0,0 +1 @@
^CMake Error: The -- option cannot be used with --build-and-test\. Use --build-options or --test-command to forward arguments\.$
@@ -0,0 +1 @@
^CMake Error: The -- option cannot be used with --build-and-test\. Use --build-options or --test-command to forward arguments\.$
@@ -0,0 +1,6 @@
1: Test command: [^
]* "-E" "echo" "base_output" "-R" "--verbose"
.*
2: Test command: [^
]* "-E" "echo" "second_test" "-R" "--verbose"
.*
@@ -0,0 +1,6 @@
1: Test command: [^
]* "-E" "echo" "base_output"
.*
2: Test command: [^
]* "-E" "echo" "second_test"
.*
@@ -0,0 +1,6 @@
1: Test command: [^
]* "-E" "echo" "base_output" "--flag1" "--flag2" "value"
.*
2: Test command: [^
]* "-E" "echo" "second_test" "--flag1" "--flag2" "value"
.*
@@ -0,0 +1,8 @@
Test project [^
]*/Tests/RunCMake/CTestCommandLine/Passthrough-with-filter(
|
[^0-9][^
]*)+
2: Test command: [^
]* "-E" "echo" "second_test" "--extra"
.*
@@ -734,3 +734,38 @@ endforeach()
run_cmake_command(ScheduleRandomSeed1 ${CMAKE_CTEST_COMMAND} --schedule-random --schedule-random-seed 42)
run_cmake_command(ScheduleRandomSeed2 ${CMAKE_CTEST_COMMAND} --schedule-random --schedule-random-seed 42)
endblock()
function(run_PassthroughTest case)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/Passthrough-${case})
set(RunCMake_TEST_NO_CLEAN 1)
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
file(WRITE "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake" "
add_test(echo_test \"${CMAKE_COMMAND}\" -E echo \"base_output\")
add_test(echo_test2 \"${CMAKE_COMMAND}\" -E echo \"second_test\")
")
run_cmake_command(Passthrough-${case} ${CMAKE_CTEST_COMMAND} ${ARGN})
endfunction()
# Basic passthrough
run_PassthroughTest(basic -V -- --extra-flag)
# Multiple passthrough args
run_PassthroughTest(multiple -V -- --flag1 --flag2 value)
# Passthrough with -R filter (only echo_test2 should run)
run_PassthroughTest(with-filter -V -R echo_test2 -- --extra)
# Empty passthrough (bare --)
run_PassthroughTest(empty -V --)
# Passthrough args that look like ctest flags
run_PassthroughTest(ctest-flags -V -- -R --verbose)
# -- rejected with --build-and-test
run_cmake_command(Passthrough-build-and-test-error ${CMAKE_CTEST_COMMAND}
--build-and-test
${RunCMake_BINARY_DIR}/Passthrough-build-and-test-error/does-not-exist
${RunCMake_BINARY_DIR}/Passthrough-build-and-test-error/does-not-exist
--build-generator "None" -- --extra-flag)
# bare -- rejected with --build-and-test
run_cmake_command(Passthrough-build-and-test-empty-error ${CMAKE_CTEST_COMMAND}
--build-and-test
${RunCMake_BINARY_DIR}/Passthrough-build-and-test-empty-error/does-not-exist
${RunCMake_BINARY_DIR}/Passthrough-build-and-test-empty-error/does-not-exist
--build-generator "None" --)