diff --git a/Help/prop_dir/TEST_INCLUDE_FILE.rst b/Help/prop_dir/TEST_INCLUDE_FILE.rst index 883b124a4e..e329cb7ac0 100644 --- a/Help/prop_dir/TEST_INCLUDE_FILE.rst +++ b/Help/prop_dir/TEST_INCLUDE_FILE.rst @@ -12,3 +12,9 @@ included and processed when ``ctest`` is run on the directory. If both the ``TEST_INCLUDE_FILE`` and :prop_dir:`TEST_INCLUDE_FILES` directory properties are set, the script specified in ``TEST_INCLUDE_FILE`` is included first, followed by the scripts listed in ``TEST_INCLUDE_FILES``. + +.. versionadded:: 4.5 + + The include path may use :manual:`generator expressions + `, with the same per-configuration semantics + as :prop_dir:`TEST_INCLUDE_FILES`. diff --git a/Help/prop_dir/TEST_INCLUDE_FILES.rst b/Help/prop_dir/TEST_INCLUDE_FILES.rst index f1c583facd..724513c35c 100644 --- a/Help/prop_dir/TEST_INCLUDE_FILES.rst +++ b/Help/prop_dir/TEST_INCLUDE_FILES.rst @@ -13,6 +13,19 @@ were CTest dashboard scripts. It is common to generate such scripts dynamically since many variables and commands available during configuration are not accessible at test phase. +.. versionadded:: 4.5 + + Include paths may use :manual:`generator expressions + `. This is most useful with + :prop_gbl:`multi-config generators `, where a + ``$``-parameterized path selects a different script per + configuration. On multi-config generators, an entry whose evaluated result + varies by configuration is emitted under a per-configuration guard, so it is + included only for the matching ``ctest -C `` run. A path whose result + is the same for all configurations is included unconditionally. + + No build dependency is added for targets referenced in an include path. + Examples ^^^^^^^^ @@ -38,3 +51,20 @@ Setting this directory property to append one or more CMake scripts: execute_process( COMMAND "@CMAKE_COMMAND@" -E echo "script.cmake executed during CTest" ) + +Selecting a per-configuration script with a generator expression: + +.. code-block:: cmake + :caption: CMakeLists.txt + + file(GENERATE + OUTPUT "props_$.cmake" + CONTENT "set_tests_properties(some.test PROPERTIES LABELS \"$\")\n" + ) + + set_property( + DIRECTORY + APPEND + PROPERTY TEST_INCLUDE_FILES + "${CMAKE_CURRENT_BINARY_DIR}/props_$.cmake" + ) diff --git a/Help/release/dev/test-include-files-genex.rst b/Help/release/dev/test-include-files-genex.rst new file mode 100644 index 0000000000..463eb9c338 --- /dev/null +++ b/Help/release/dev/test-include-files-genex.rst @@ -0,0 +1,8 @@ +test-include-files-genex +------------------------ + +* The :prop_dir:`TEST_INCLUDE_FILES` and :prop_dir:`TEST_INCLUDE_FILE` + directory properties now support :manual:`generator expressions + ` in the include path. On multi-config + generators a ``$``-parameterized path can select a different script + per configuration. diff --git a/Source/cmLocalGenerator.cxx b/Source/cmLocalGenerator.cxx index 126bdf1d68..cfb5d27090 100644 --- a/Source/cmLocalGenerator.cxx +++ b/Source/cmLocalGenerator.cxx @@ -386,16 +386,91 @@ void cmLocalGenerator::GenerateTestFiles() fout << "set(CTEST_RESOURCE_SPEC_FILE \"" << resourceSpecFile << "\")\n"; } + auto writeTestIncludeFile = [this, &fout, &configurationTypes, + &config](std::string const& entry) { + // Entries without a generator expression are emitted verbatim, + // preserving CTest-time ${VAR} expansion of the path. + if (cmGeneratorExpression::Find(entry) == std::string::npos) { + fout << "include(\"" << entry << "\")\n"; + return; + } + + // Emit one include() per path in an evaluated result. As for list-valued + // properties, the result is split on ';', and each path is quoted so that + // any '"', '$', or '\' cannot break the include() argument or trigger a + // second-stage CTest ${...} expansion. An empty result adds no include. + auto writeIncludes = [&fout](std::vector const& paths, + char const* indent) { + for (std::string const& path : paths) { + fout << indent << "include(" << cmScriptGenerator::Quote(path) + << ")\n"; + } + }; + + if (!this->GlobalGenerator->IsMultiConfig() || + configurationTypes.empty()) { + // Single-config generator, or a multi-config generator with no + // configuration types: evaluate once with the default configuration + // and emit unconditional includes. + std::vector paths; + cmExpandList(cmGeneratorExpression::Evaluate(entry, this, config), + paths); + writeIncludes(paths, ""); + return; + } + + // Multi-config generator: evaluate the entry for every configuration. + std::vector results; + results.reserve(configurationTypes.size()); + bool allSame = true; + for (std::string const& ct : configurationTypes) { + results.emplace_back(cmGeneratorExpression::Evaluate(entry, this, ct)); + if (results.back() != results.front()) { + allSame = false; + } + } + + // A config-independent result collapses to unconditional includes, so it + // still runs when ctest is invoked without -C. + if (allSame) { + std::vector paths; + cmExpandList(results.front(), paths); + writeIncludes(paths, ""); + return; + } + + // Otherwise guard each per-config result to match the config guards used + // for add_test(). A configuration whose result yields no include is + // skipped so no empty guard is emitted. + bool first = true; + for (size_t i = 0; i < configurationTypes.size(); ++i) { + std::vector paths; + cmExpandList(results[i], paths); + if (paths.empty()) { + continue; + } + fout << (first ? "if(" : "elseif(") + << cmScriptGenerator::CreateConfigTest("CTEST_CONFIGURATION_TYPE", + configurationTypes[i]) + << ")\n"; + writeIncludes(paths, " "); + first = false; + } + if (!first) { + fout << "endif()\n"; + } + }; + cmValue testIncludeFile = this->Makefile->GetProperty("TEST_INCLUDE_FILE"); if (testIncludeFile) { - fout << "include(\"" << *testIncludeFile << "\")\n"; + writeTestIncludeFile(*testIncludeFile); } cmValue testIncludeFiles = this->Makefile->GetProperty("TEST_INCLUDE_FILES"); if (testIncludeFiles) { cmList includesList{ *testIncludeFiles }; for (std::string const& i : includesList) { - fout << "include(\"" << i << "\")\n"; + writeTestIncludeFile(i); } } diff --git a/Source/cmScriptGenerator.cxx b/Source/cmScriptGenerator.cxx index 5efc243452..857884adff 100644 --- a/Source/cmScriptGenerator.cxx +++ b/Source/cmScriptGenerator.cxx @@ -100,9 +100,10 @@ static void cmScriptGeneratorEncodeConfig(std::string const& config, } } -std::string cmScriptGenerator::CreateConfigTest(std::string const& config) +std::string cmScriptGenerator::CreateConfigTest(std::string const& configVar, + std::string const& config) { - std::string result = cmStrCat(this->RuntimeConfigVariable, " MATCHES \"^("); + std::string result = cmStrCat(configVar, " MATCHES \"^("); if (!config.empty()) { cmScriptGeneratorEncodeConfig(config, result); } @@ -111,9 +112,9 @@ std::string cmScriptGenerator::CreateConfigTest(std::string const& config) } std::string cmScriptGenerator::CreateConfigTest( - std::vector const& configs) + std::string const& configVar, std::vector const& configs) { - std::string result = cmStrCat(this->RuntimeConfigVariable, " MATCHES \"^("); + std::string result = cmStrCat(configVar, " MATCHES \"^("); char const* sep = ""; for (std::string const& config : configs) { result += sep; @@ -124,6 +125,19 @@ std::string cmScriptGenerator::CreateConfigTest( return result; } +std::string cmScriptGenerator::CreateConfigTest(std::string const& config) +{ + return cmScriptGenerator::CreateConfigTest(this->RuntimeConfigVariable, + config); +} + +std::string cmScriptGenerator::CreateConfigTest( + std::vector const& configs) +{ + return cmScriptGenerator::CreateConfigTest(this->RuntimeConfigVariable, + configs); +} + void cmScriptGenerator::GenerateScript(std::ostream& os) { // Track indentation. diff --git a/Source/cmScriptGenerator.h b/Source/cmScriptGenerator.h index 2a6ff4d73a..e59d473b5a 100644 --- a/Source/cmScriptGenerator.h +++ b/Source/cmScriptGenerator.h @@ -78,6 +78,11 @@ public: static cmScriptGeneratorQuoted Quote(cm::string_view value); + static std::string CreateConfigTest(std::string const& configVar, + std::string const& config); + static std::string CreateConfigTest(std::string const& configVar, + std::vector const& configs); + protected: using Indent = cmScriptGeneratorIndent; virtual void GenerateScript(std::ostream& os); diff --git a/Tests/RunCMake/test_include_dirs/RunCMakeTest.cmake b/Tests/RunCMake/test_include_dirs/RunCMakeTest.cmake index 74795c205b..9dd4f2fa3e 100644 --- a/Tests/RunCMake/test_include_dirs/RunCMakeTest.cmake +++ b/Tests/RunCMake/test_include_dirs/RunCMakeTest.cmake @@ -19,3 +19,28 @@ function(run_TID) endfunction() run_TID() + +function(run_TIDGenex) + # Generator expressions in TEST_INCLUDE_FILE(S) include paths. + set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/TIDGenex-build) + set(RunCMake_TEST_NO_CLEAN 1) + if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG) + set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug) + endif() + file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") + file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") + run_cmake(TIDGenex) + run_cmake_command(TIDGenex-ctest-noC ${CMAKE_CTEST_COMMAND}) + run_cmake_command(TIDGenex-ctest-Debug ${CMAKE_CTEST_COMMAND} -C Debug) + if(RunCMake_GENERATOR_IS_MULTI_CONFIG) + run_cmake_command(TIDGenex-ctest-Release ${CMAKE_CTEST_COMMAND} -C Release) + endif() +endfunction() + +run_TIDGenex() + +# Serialization/quoting rules for genex-evaluated include paths (configure only). +run_cmake(TIDGenexQuoting) + +# An unknown generator expression must fail at generate time. +run_cmake(TIDGenexBadGenex) diff --git a/Tests/RunCMake/test_include_dirs/TIDGenex-check.cmake b/Tests/RunCMake/test_include_dirs/TIDGenex-check.cmake new file mode 100644 index 0000000000..16c1efaf6a --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenex-check.cmake @@ -0,0 +1,49 @@ +# Assert the serialization of the generated CTestTestfile.cmake for the +# TEST_INCLUDE_FILE(S) generator-expression feature. +set(ctf "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake") +if(NOT EXISTS "${ctf}") + set(RunCMake_TEST_FAILED "CTestTestfile.cmake not found:\n ${ctf}") + return() +endif() +file(READ "${ctf}" content) + +# Plain (singular) entry stays a bare, double-quoted include (regression). +if(NOT content MATCHES "include\\(\"[^\n]*plain[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "plain include not emitted as a bare double-quoted include\n") +endif() + +# Config-independent genex collapses to one unconditional bare include. +if(NOT content MATCHES "include\\(\"[^\n]*indep[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "config-independent genex not collapsed to one unconditional include\n") +endif() + +# A conditional-empty genex must never emit an empty include(). +if(content MATCHES "include\\(\"\"\\)") + string(APPEND RunCMake_TEST_FAILED "empty include() emitted\n") +endif() + +if(RunCMake_GENERATOR_IS_MULTI_CONFIG) + if(NOT content MATCHES "CTEST_CONFIGURATION_TYPE MATCHES \"\\^\\(\\[Dd\\]\\[Ee\\]\\[Bb\\]\\[Uu\\]\\[Gg\\]\\)[$]\"") + string(APPEND RunCMake_TEST_FAILED + "expected per-config Debug guard not found on multi-config\n") + endif() + if(NOT content MATCHES "include\\(\"[^\n]*props_Release[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "expected props_Release.cmake per-config branch not found\n") + endif() +else() + if(NOT content MATCHES "include\\(\"[^\n]*props_Debug[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "expected unconditional props_Debug.cmake include not found\n") + endif() + if(NOT content MATCHES "include\\(\"[^\n]*dbgonly[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "expected unconditional dbgonly.cmake include not found\n") + endif() + if(content MATCHES "CTEST_CONFIGURATION_TYPE MATCHES") + string(APPEND RunCMake_TEST_FAILED + "unexpected per-config guard on single-config generator\n") + endif() +endif() diff --git a/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Debug-check.cmake b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Debug-check.cmake new file mode 100644 index 0000000000..8d46d03ba0 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Debug-check.cmake @@ -0,0 +1,19 @@ +# ctest -C Debug: the Debug per-config include and the Debug-only conditional +# include both run; the Release per-config include does not. +if(NOT actual_stdout MATCHES "plain_test") + string(APPEND RunCMake_TEST_FAILED "plain_test missing with -C Debug\n") +endif() +if(NOT actual_stdout MATCHES "indep_test") + string(APPEND RunCMake_TEST_FAILED "indep_test missing with -C Debug\n") +endif() +if(NOT actual_stdout MATCHES "config_Debug") + string(APPEND RunCMake_TEST_FAILED "config_Debug missing with -C Debug\n") +endif() +if(NOT actual_stdout MATCHES "dbgonly_test") + string(APPEND RunCMake_TEST_FAILED "dbgonly_test missing with -C Debug\n") +endif() +if(RunCMake_GENERATOR_IS_MULTI_CONFIG) + if(actual_stdout MATCHES "config_Release") + string(APPEND RunCMake_TEST_FAILED "config_Release ran under -C Debug\n") + endif() +endif() diff --git a/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Release-check.cmake b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Release-check.cmake new file mode 100644 index 0000000000..3f9f657132 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-Release-check.cmake @@ -0,0 +1,17 @@ +# ctest -C Release (multi-config only): the Release per-config include runs; +# the Debug per-config include and the Debug-only conditional include do not. +if(NOT actual_stdout MATCHES "plain_test") + string(APPEND RunCMake_TEST_FAILED "plain_test missing with -C Release\n") +endif() +if(NOT actual_stdout MATCHES "indep_test") + string(APPEND RunCMake_TEST_FAILED "indep_test missing with -C Release\n") +endif() +if(NOT actual_stdout MATCHES "config_Release") + string(APPEND RunCMake_TEST_FAILED "config_Release missing with -C Release\n") +endif() +if(actual_stdout MATCHES "config_Debug") + string(APPEND RunCMake_TEST_FAILED "config_Debug ran under -C Release\n") +endif() +if(actual_stdout MATCHES "dbgonly_test") + string(APPEND RunCMake_TEST_FAILED "dbgonly_test ran under -C Release\n") +endif() diff --git a/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-noC-check.cmake b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-noC-check.cmake new file mode 100644 index 0000000000..a234c22a74 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenex-ctest-noC-check.cmake @@ -0,0 +1,27 @@ +# ctest invoked WITHOUT -C. Unconditional includes always run; guarded +# per-config includes run only on single-config (where they collapse). +if(NOT actual_stdout MATCHES "plain_test") + string(APPEND RunCMake_TEST_FAILED "plain_test missing without -C\n") +endif() +if(NOT actual_stdout MATCHES "indep_test") + string(APPEND RunCMake_TEST_FAILED "indep_test missing without -C\n") +endif() +if(RunCMake_GENERATOR_IS_MULTI_CONFIG) + if(actual_stdout MATCHES "config_Debug|config_Release") + string(APPEND RunCMake_TEST_FAILED + "guarded config-dependent test ran without -C on multi-config\n") + endif() + if(actual_stdout MATCHES "dbgonly_test") + string(APPEND RunCMake_TEST_FAILED + "dbgonly_test ran without -C on multi-config\n") + endif() +else() + if(NOT actual_stdout MATCHES "config_Debug") + string(APPEND RunCMake_TEST_FAILED + "config_Debug missing on single-config without -C\n") + endif() + if(NOT actual_stdout MATCHES "dbgonly_test") + string(APPEND RunCMake_TEST_FAILED + "dbgonly_test missing on single-config without -C\n") + endif() +endif() diff --git a/Tests/RunCMake/test_include_dirs/TIDGenex.cmake b/Tests/RunCMake/test_include_dirs/TIDGenex.cmake new file mode 100644 index 0000000000..e73af9be14 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenex.cmake @@ -0,0 +1,30 @@ +enable_testing() + +set(gen_dir "${CMAKE_CURRENT_BINARY_DIR}") + +# Plain include script (no generator expression in the path); used via the +# deprecated singular TEST_INCLUDE_FILE to also cover singular-first ordering. +file(GENERATE OUTPUT "${gen_dir}/plain.cmake" + CONTENT "add_test(plain_test \"${CMAKE_COMMAND}\" -E true)\n") + +# Per-configuration script selected by $ in the include path. +file(GENERATE OUTPUT "${gen_dir}/props_$.cmake" + CONTENT "add_test(config_$ \"${CMAKE_COMMAND}\" -E true)\n") + +# Config-independent generator expression in the path: collapses to a single +# unconditional include (still runs without -C). +file(GENERATE OUTPUT "${gen_dir}/indep.cmake" + CONTENT "add_test(indep_test \"${CMAKE_COMMAND}\" -E true)\n") + +# Debug-only include via a conditional-empty generator expression. +file(GENERATE OUTPUT "${gen_dir}/dbgonly.cmake" + CONTENT "add_test(dbgonly_test \"${CMAKE_COMMAND}\" -E true)\n") + +set_property(DIRECTORY PROPERTY TEST_INCLUDE_FILE + "${gen_dir}/plain.cmake") +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${gen_dir}/props_$.cmake") +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "$<1:${gen_dir}/indep.cmake>") +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "$<$:${gen_dir}/dbgonly.cmake>") diff --git a/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-result.txt b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-stderr.txt b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-stderr.txt new file mode 100644 index 0000000000..350581a796 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex-stderr.txt @@ -0,0 +1,2 @@ +Error evaluating generator expression:.* +.*NOTAGENEX diff --git a/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex.cmake b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex.cmake new file mode 100644 index 0000000000..4a7e5871ed --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenexBadGenex.cmake @@ -0,0 +1,6 @@ +enable_testing() + +# An unknown generator expression in the include path must fail at generate +# time (diagnostics are best-effort: they need not name the set_property call). +set_property(DIRECTORY PROPERTY TEST_INCLUDE_FILE + "${CMAKE_CURRENT_BINARY_DIR}/x_$.cmake") diff --git a/Tests/RunCMake/test_include_dirs/TIDGenexQuoting-check.cmake b/Tests/RunCMake/test_include_dirs/TIDGenexQuoting-check.cmake new file mode 100644 index 0000000000..de026486ac --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenexQuoting-check.cmake @@ -0,0 +1,44 @@ +set(ctf "${RunCMake_TEST_BINARY_DIR}/CTestTestfile.cmake") +if(NOT EXISTS "${ctf}") + set(RunCMake_TEST_FAILED "CTestTestfile.cmake not found:\n ${ctf}") + return() +endif() +file(READ "${ctf}" content) + +# (1) ';' result: split into two separate double-quoted includes (fan-out), +# not one combined include. +if(NOT content MATCHES "include\\(\"[^\n]*/a\"\\)") + string(APPEND RunCMake_TEST_FAILED + "semicolon result first element not emitted as its own include\n") +endif() +if(NOT content MATCHES "include\\(\"b[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "semicolon result second element not emitted as its own include\n") +endif() +if(content MATCHES "include\\(\"[^\n]*a;b[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED + "semicolon result was not split into multiple includes\n") +endif() + +# (2) space result: double-quoted. +if(NOT content MATCHES "include\\(\"[^\n]*a b[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED "space result not double-quoted\n") +endif() + +# (3) '$' result: bracket form, and NOT double-quoted. +if(NOT content MATCHES "include\\(\\[=*\\[[^\n]*x[$]y[.]cmake[^\n]*\\]=*\\]\\)") + string(APPEND RunCMake_TEST_FAILED "dollar result not emitted in bracket form\n") +endif() +if(content MATCHES "include\\(\"[^\n]*x[$]y[.]cmake") + string(APPEND RunCMake_TEST_FAILED + "dollar result was double-quoted (should be bracketed)\n") +endif() + +# (4) plain ${VAR} entry preserved raw and bare (double-quoted, not bracketed). +if(NOT content MATCHES "include\\(\"[^\n]*lit_[^\n]*MY_VAR[^\n]*[.]cmake\"\\)") + string(APPEND RunCMake_TEST_FAILED "plain \${VAR} entry not preserved raw\n") +endif() +if(content MATCHES "include\\(\\[=*\\[[^\n]*MY_VAR") + string(APPEND RunCMake_TEST_FAILED + "plain \${VAR} entry was bracketed (should stay raw double-quoted)\n") +endif() diff --git a/Tests/RunCMake/test_include_dirs/TIDGenexQuoting.cmake b/Tests/RunCMake/test_include_dirs/TIDGenexQuoting.cmake new file mode 100644 index 0000000000..4661a5f747 --- /dev/null +++ b/Tests/RunCMake/test_include_dirs/TIDGenexQuoting.cmake @@ -0,0 +1,27 @@ +enable_testing() + +set(d "${CMAKE_CURRENT_BINARY_DIR}") + +# This case only asserts serialization of the generated CTestTestfile.cmake; +# ctest is never run, so the referenced paths need not exist. + +# (1) A genex result containing ';' is split into multiple includes (list +# semantics, as for usage-requirement properties). $ keeps the +# ';' out of the pre-eval list split so it appears only in the evaluated +# result, which is then re-split into one include() per element. +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "$<1:${d}/a$b.cmake>") + +# (2) A genex result containing a space stays double-quoted. +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "$<1:${d}/a b.cmake>") + +# (3) A genex result containing '$' is forced to bracket form so CTest does not +# perform a second-stage ${...} expansion. $<1:$> composes a literal '$'. +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "$<1:${d}/x$<1:$>y.cmake>") + +# (4) A plain entry containing ${VAR} stays a raw, bare double-quoted include so +# CTest still expands it at test time (must NOT be routed through Quote). +set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES + "${d}/lit_\${MY_VAR}.cmake")