Browse Source

tests: Preserve empty arguments in test command lines

This will now preserve empty values in the TEST_LAUNCHER and
CROSSCOMPILING_EMULATOR target properties for tests added by:

- The add_test() command.
- The ExternalData_Add_Test() command from the ExternalData module.
- The gtest_add_tests() or gtest_discover_tests() commands from the
  GoogleTest module.

For the gtest_add_tests() and gtest_discover_tests() commands,
empty elements in the values passed after the EXTRA_ARGS keyword
are also now preserved.

Policy CMP0178 is added to provide backward compatibility with the
old behavior where empty values were silently discarded from the
above cases.

Fixes: #26337
Craig Scott 1 year ago
parent
commit
fc7aa3cd69

+ 1 - 0
Help/manual/cmake-policies.7.rst

@@ -57,6 +57,7 @@ Policies Introduced by CMake 3.31
 .. toctree::
    :maxdepth: 1
 
+   CMP0178: Test command lines preserve empty arguments. </policy/CMP0178>
    CMP0177: install() DESTINATION paths are normalized. </policy/CMP0177>
    CMP0176: execute_process() ENCODING is UTF-8 by default. </policy/CMP0176>
    CMP0175: add_custom_command() rejects invalid arguments. </policy/CMP0175>

+ 37 - 0
Help/policy/CMP0178.rst

@@ -0,0 +1,37 @@
+CMP0178
+-------
+
+.. versionadded:: 3.31
+
+Test command lines preserve empty arguments.
+
+Empty values in the :prop_tgt:`TEST_LAUNCHER` and
+:prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are now preserved
+for tests added by the following:
+
+* The :command:`add_test` command.
+* The :command:`ExternalData_Add_Test` command from the :module:`ExternalData`
+  module.
+* The :command:`gtest_add_tests` or :command:`gtest_discover_tests` commands
+  from the :module:`GoogleTest` module.
+
+For the :command:`gtest_add_tests` and :command:`gtest_discover_tests`
+commands, empty elements in the values passed after the ``EXTRA_ARGS``
+keyword are also now preserved.
+
+The ``OLD`` behavior of this policy silently discards empty list items
+from the :prop_tgt:`TEST_LAUNCHER` and :prop_tgt:`CROSSCOMPILING_EMULATOR`
+target properties in the above-mentioned cases.  It also silently discards
+empty items from the values given after ``EXTRA_ARGS`` for the
+:command:`gtest_add_tests` and :command:`gtest_discover_tests` commands.
+
+The ``NEW`` behavior of this policy preserves empty list items in the
+:prop_tgt:`TEST_LAUNCHER` and :prop_tgt:`CROSSCOMPILING_EMULATOR` target
+properties, and in values given after ``EXTRA_ARGS`` for
+:command:`gtest_add_tests` and :command:`gtest_discover_tests`.
+
+.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 3.31
+.. |WARNS_OR_DOES_NOT_WARN| replace:: warns
+.. include:: STANDARD_ADVICE.txt
+
+.. include:: DEPRECATED.txt

+ 19 - 0
Help/release/dev/preserve-empty-args-test-command-lines.rst

@@ -0,0 +1,19 @@
+preserve-empty-args-test-command-lines
+--------------------------------------
+
+* Empty list elements in the :prop_tgt:`TEST_LAUNCHER` and
+  :prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are now preserved
+  when the executable for a command given to :command:`add_test` is a CMake
+  target. See policy :policy:`CMP0178`.
+
+* Empty list elements in the :prop_tgt:`TEST_LAUNCHER` and
+  :prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are now preserved
+  for the test created by :command:`ExternalData_Add_Test` from the
+  :module:`ExternalData` module.  See policy :policy:`CMP0178`.
+
+* Empty list elements in the :prop_tgt:`TEST_LAUNCHER` and
+  :prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are now preserved
+  for tests created by :command:`gtest_add_tests` and
+  :command:`gtest_discover_tests` from the :module:`GoogleTest` module.
+  Empty list elements after the ``EXTRA_ARGS`` keyword of these two commands
+  are also now preserved.  See policy :policy:`CMP0178`.

+ 2 - 0
Modules/AndroidTestUtilities.cmake

@@ -147,6 +147,8 @@ function(android_add_test_data test_name)
 
   if(ANDROID)
     string(REGEX REPLACE "DATA{([^ ;]+)}" "\\1"  processed_FILES "${AST_FILES}")
+    # There's no target used for this command, so we don't need to do anything
+    # here for CMP0178.
     add_test(
       NAME ${test_name}
       COMMAND ${CMAKE_COMMAND}

+ 17 - 1
Modules/ExternalData.cmake

@@ -72,6 +72,12 @@ Module Functions
   It passes its arguments through ``ExternalData_Expand_Arguments`` and then
   invokes the :command:`add_test` command using the results.
 
+  .. versionchanged:: 3.31
+    If the arguments after ``<target>`` define a test with an executable
+    that is a CMake target, empty values in the :prop_tgt:`TEST_LAUNCHER`
+    and :prop_tgt:`CROSSCOMPILING_EMULATOR` properties of that target are
+    preserved.  See policy :policy:`CMP0178`.
+
 .. command:: ExternalData_Add_Target
 
   The ``ExternalData_Add_Target`` function creates a custom target to
@@ -353,7 +359,17 @@ file or set a variable:
 function(ExternalData_add_test target)
   # Expand all arguments as a single string to preserve escaped semicolons.
   ExternalData_expand_arguments("${target}" testArgs "${ARGN}")
-  add_test(${testArgs})
+
+  # We need the caller's CMP0178 policy setting to apply here
+  cmake_policy(GET CMP0178 cmp0178
+    PARENT_SCOPE  # undocumented, do not use outside of CMake
+  )
+
+  # ExternalData_expand_arguments() escapes semicolons, so we should still be
+  # preserving empty elements from ARGN here. But CMP0178 is still important
+  # for correctly handling TEST_LAUNCHER and CROSSCOMPILING_EMULATOR target
+  # properties that contain empty elements.
+  add_test(${testArgs} __CMP0178 "${cmp0178}")
 endfunction()
 
 function(ExternalData_add_target target)

+ 2 - 0
Modules/FindCxxTest.cmake

@@ -163,6 +163,8 @@ macro(CXXTEST_ADD_TEST _cxxtest_testname _cxxtest_outfname)
     set_source_files_properties(${_cxxtest_real_outfname} PROPERTIES GENERATED true)
     add_executable(${_cxxtest_testname} ${_cxxtest_real_outfname} ${ARGN})
 
+    # There's no target used for these commands, so we don't need to do
+    # anything here for CMP0178.
     if(CMAKE_RUNTIME_OUTPUT_DIRECTORY)
         add_test(${_cxxtest_testname} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${_cxxtest_testname})
     elseif(EXECUTABLE_OUTPUT_PATH)

+ 4 - 0
Modules/FindMatlab.cmake

@@ -1012,6 +1012,10 @@ function(matlab_add_unit_test)
     endif()
   endif()
 
+  # The ${${prefix}_TEST_ARGS} and ${${prefix}_UNPARSED_ARGUMENTS} used below
+  # should have semicolons escaped, so empty arguments should be preserved.
+  # There's also no target used for the command, so we don't need to do
+  # anything here for CMP0178.
   add_test(NAME ${${prefix}_NAME}
            COMMAND ${CMAKE_COMMAND}
             "-Dtest_name=${${prefix}_NAME}"

+ 4 - 0
Modules/FindSquish.cmake

@@ -200,6 +200,8 @@ macro(squish_v3_add_test testName testAUT testCase envVars testWraper)
     message(STATUS "Using squish_v3_add_test(), but SQUISH_VERSION_MAJOR is ${SQUISH_VERSION_MAJOR}.\nThis may not work.")
   endif()
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(${testName}
     ${CMAKE_COMMAND} -V -VV
     "-Dsquish_version:STRING=3"
@@ -258,6 +260,8 @@ function(squish_v4_add_test testName)
     message("SETTINGSGROUP is deprecated and will be ignored.")
   endif()
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(NAME ${testName}
     COMMAND ${CMAKE_COMMAND} -V -VV
     "-Dsquish_version:STRING=4"

+ 2 - 0
Modules/FindXCTest.cmake

@@ -210,6 +210,8 @@ function(xctest_add_test name bundle)
 
   # register test
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(
     NAME ${name}
     COMMAND ${XCTest_EXECUTABLE} $<TARGET_BUNDLE_DIR:${bundle}>)

+ 112 - 18
Modules/GoogleTest.cmake

@@ -42,7 +42,7 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
 
     gtest_add_tests(TARGET target
                     [SOURCES src1...]
-                    [EXTRA_ARGS arg1...]
+                    [EXTRA_ARGS args...]
                     [WORKING_DIRECTORY dir]
                     [TEST_PREFIX prefix]
                     [TEST_SUFFIX suffix]
@@ -72,9 +72,12 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
     this option is not given, the :prop_tgt:`SOURCES` property of the
     specified ``target`` will be used to obtain the list of sources.
 
-  ``EXTRA_ARGS arg1...``
+  ``EXTRA_ARGS args...``
     Any extra arguments to pass on the command line to each test case.
 
+    .. versionchanged:: 3.31
+      Empty values in ``args...`` are preserved, see :policy:`CMP0178`.
+
   ``WORKING_DIRECTORY dir``
     Specifies the directory in which to run the discovered test cases.  If this
     option is not provided, the current binary directory is used.
@@ -101,6 +104,11 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
     with the list of discovered test cases.  This allows the caller to do
     things like manipulate test properties of the discovered tests.
 
+  .. versionchanged:: 3.31
+    Empty values in the :prop_tgt:`TEST_LAUNCHER` and
+    :prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are preserved,
+    see policy :policy:`CMP0178`.
+
   Usage example:
 
   .. code-block:: cmake
@@ -147,7 +155,7 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
   for available tests::
 
     gtest_discover_tests(target
-                         [EXTRA_ARGS arg1...]
+                         [EXTRA_ARGS args...]
                          [WORKING_DIRECTORY dir]
                          [TEST_PREFIX prefix]
                          [TEST_SUFFIX suffix]
@@ -187,9 +195,12 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
     executable target.  CMake will substitute the location of the built
     executable when running the test.
 
-  ``EXTRA_ARGS arg1...``
+  ``EXTRA_ARGS args...``
     Any extra arguments to pass on the command line to each test case.
 
+    .. versionchanged:: 3.31
+      Empty values in ``args...`` are preserved, see :policy:`CMP0178`.
+
   ``WORKING_DIRECTORY dir``
     Specifies the directory in which to run the discovered test cases.  If this
     option is not provided, the current binary directory is used.
@@ -283,6 +294,15 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
     for globally selecting a preferred test discovery behavior without having
     to modify each call site.
 
+  .. versionadded:: 3.29
+    The :prop_tgt:`TEST_LAUNCHER` target property is honored during test
+    discovery and test execution.
+
+  .. versionchanged:: 3.31
+    Empty values in the :prop_tgt:`TEST_LAUNCHER` and
+    :prop_tgt:`CROSSCOMPILING_EMULATOR` target properties are preserved,
+    see policy :policy:`CMP0178`.
+
 #]=======================================================================]
 
 # Save project's policies
@@ -312,9 +332,41 @@ function(gtest_add_tests)
   )
   set(allKeywords ${options} ${oneValueArgs} ${multiValueArgs})
 
+  cmake_policy(GET CMP0178 cmp0178
+    PARENT_SCOPE # undocumented, do not use outside of CMake
+  )
+
   unset(sources)
   if("${ARGV0}" IN_LIST allKeywords)
-    cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+    if(cmp0178 STREQUAL "NEW")
+      cmake_parse_arguments(PARSE_ARGV 0 arg
+        "${options}" "${oneValueArgs}" "${multiValueArgs}"
+      )
+    else()
+      cmake_parse_arguments(arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+      if(NOT cmp0178 STREQUAL "OLD")
+        block(SCOPE_FOR VARIABLES)
+          cmake_parse_arguments(PARSE_ARGV 0 arg_new
+            "${options}" "${oneValueArgs}" "${multiValueArgs}"
+          )
+          # Due to a quirk of cmake_parse_arguments(PARSE_ARGV),
+          # arg_new_EXTRA_ARGS will have semicolons already escaped, but
+          # arg_EXTRA_ARGS won't. We need to pass the former through one round
+          # of command argument parsing to de-escape them for comparison with
+          # the latter.
+          set(__newArgs ${arg_new_EXTRA_ARGS})
+          if(NOT "${arg_EXTRA_ARGS}" STREQUAL "${__newArgs}")
+            cmake_policy(GET_WARNING CMP0178 cmp0178_warning)
+            message(AUTHOR_WARNING
+              "The EXTRA_ARGS contain one or more empty values. Those empty "
+              "values are being silently discarded to preserve backward "
+              "compatibility.\n"
+              "${cmp0178_warning}"
+            )
+          endif()
+        endblock()
+      endif()
+    endif()
     set(autoAddSources YES)
   else()
     # Non-keyword syntax, convert to keyword form
@@ -408,6 +460,11 @@ function(gtest_add_tests)
         continue()
       endif()
 
+      set(extra_args "")
+      foreach(arg IN LISTS arg_EXTRA_ARGS)
+        string(APPEND extra_args " [==[${arg}]==]")
+      endforeach()
+
       # Make sure tests disabled in GTest get disabled in CTest
       if(gtest_test_name MATCHES "(^|\\.)DISABLED_")
         # Add the disabled test if CMake is new enough
@@ -422,12 +479,15 @@ function(gtest_add_tests)
           set(ctest_test_name
               ${arg_TEST_PREFIX}${orig_test_name}${arg_TEST_SUFFIX}
           )
-          add_test(NAME ${ctest_test_name}
-                   ${workDir}
-                   COMMAND ${arg_TARGET}
-                     --gtest_also_run_disabled_tests
-                     --gtest_filter=${gtest_test_name}
-                     ${arg_EXTRA_ARGS}
+          cmake_language(EVAL CODE "
+            add_test(NAME ${ctest_test_name}
+                     ${workDir}
+                     COMMAND ${arg_TARGET}
+                       --gtest_also_run_disabled_tests
+                       --gtest_filter=${gtest_test_name}
+                       ${extra_args}
+                     __CMP0178 [==[${cmp0178}]==]
+            )"
           )
           set_tests_properties(${ctest_test_name} PROPERTIES DISABLED TRUE
             DEF_SOURCE_LINE "${source}:${accumulate_line}")
@@ -435,11 +495,14 @@ function(gtest_add_tests)
         endif()
       else()
         set(ctest_test_name ${arg_TEST_PREFIX}${gtest_test_name}${arg_TEST_SUFFIX})
-        add_test(NAME ${ctest_test_name}
-                 ${workDir}
-                 COMMAND ${arg_TARGET}
-                   --gtest_filter=${gtest_test_name}
-                   ${arg_EXTRA_ARGS}
+        cmake_language(EVAL CODE "
+          add_test(NAME ${ctest_test_name}
+                   ${workDir}
+                   COMMAND ${arg_TARGET}
+                     --gtest_filter=${gtest_test_name}
+                     ${extra_args}
+                   __CMP0178 [==[${cmp0178}]==]
+          )"
         )
         # Makes sure a skipped GTest is reported as so by CTest
         set_tests_properties(
@@ -480,9 +543,8 @@ function(gtest_discover_tests target)
     PROPERTIES
     TEST_FILTER
   )
-  cmake_parse_arguments(arg
+  cmake_parse_arguments(PARSE_ARGV 1 arg
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
-    ${ARGN}
   )
 
   if(NOT arg_WORKING_DIRECTORY)
@@ -551,6 +613,38 @@ function(gtest_discover_tests target)
     set(test_executor "")
   endif()
 
+  cmake_policy(GET CMP0178 cmp0178
+    PARENT_SCOPE # undocumented, do not use outside of CMake
+  )
+  if(NOT cmp0178 STREQUAL "NEW")
+    # Preserve old behavior where empty list items are silently discarded
+    set(test_executor_orig "${test_executor}")
+    set(test_executor ${test_executor})
+    set(arg_EXTRA_ARGS_orig "${arg_EXTRA_ARGS}")
+    set(arg_EXTRA_ARGS ${arg_EXTRA_ARGS})
+    if(NOT cmp0178 STREQUAL "OLD")
+      if(NOT "${test_executor}" STREQUAL "${test_executor_orig}")
+        cmake_policy(GET_WARNING CMP0178 cmp0178_warning)
+        message(AUTHOR_WARNING
+          "The '${target}' target's TEST_LAUNCHER or CROSSCOMPILING_EMULATOR "
+          "test properties contain one or more empty values. Those empty "
+          "values are being silently discarded to preserve backward "
+          "compatibility.\n"
+          "${cmp0178_warning}"
+        )
+      endif()
+      if(NOT "${arg_EXTRA_ARGS}" STREQUAL "${arg_EXTRA_ARGS_orig}")
+        cmake_policy(GET_WARNING CMP0178 cmp0178_warning)
+        message(AUTHOR_WARNING
+          "The EXTRA_ARGS value contains one or more empty values. "
+          "Those empty values are being silently discarded to preserve "
+          "backward compatibility.\n"
+          "${cmp0178_warning}"
+        )
+      endif()
+    endif()
+  endif()
+
   if(arg_DISCOVERY_MODE STREQUAL "POST_BUILD")
     add_custom_command(
       TARGET ${target} POST_BUILD

+ 57 - 25
Modules/GoogleTestAddTests.cmake

@@ -2,6 +2,7 @@
 # file Copyright.txt or https://cmake.org/licensing for details.
 
 cmake_minimum_required(VERSION 3.30)
+cmake_policy(SET CMP0174 NEW)   # TODO: Remove this when we can update the above to 3.31
 
 # Overwrite possibly existing ${arg_CTEST_FILE} with empty file
 set(flush_tests_MODE WRITE)
@@ -65,9 +66,9 @@ endfunction()
 
 function(gtest_discover_tests_impl)
 
-  set(options )
+  set(options "")
   set(oneValueArgs
-    NO_PRETTY_TYPES   # These two take a value, unlike gtest_discover_tests
+    NO_PRETTY_TYPES   # These two take a value, unlike gtest_discover_tests()
     NO_PRETTY_VALUES  #
     TEST_EXECUTABLE
     TEST_WORKING_DIR
@@ -77,22 +78,21 @@ function(gtest_discover_tests_impl)
     CTEST_FILE
     TEST_DISCOVERY_TIMEOUT
     TEST_XML_OUTPUT_DIR
-    TEST_FILTER   # This is a multi-value argument in gtest_discover_tests
-  )
-  set(multiValueArgs
+    # The following are all multi-value arguments in gtest_discover_tests(),
+    # but they are each given to us as a single argument. We parse them that
+    # way to avoid problems with preserving empty list values and escaping.
+    TEST_FILTER
     TEST_EXTRA_ARGS
     TEST_PROPERTIES
     TEST_EXECUTOR
   )
-  cmake_parse_arguments(arg
+  set(multiValueArgs "")
+  cmake_parse_arguments(PARSE_ARGV 0 arg
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
-    ${ARGN}
   )
 
   set(prefix "${arg_TEST_PREFIX}")
   set(suffix "${arg_TEST_SUFFIX}")
-  set(extra_args ${arg_TEST_EXTRA_ARGS})
-  set(properties ${arg_TEST_PROPERTIES})
   set(script)
   set(suite)
   set(tests)
@@ -104,6 +104,16 @@ function(gtest_discover_tests_impl)
     set(filter)
   endif()
 
+  # CMP0178 has already been handled in gtest_discover_tests(), so we only need
+  # to implement NEW behavior here. This means preserving empty arguments for
+  # TEST_EXECUTOR. For OLD or WARN, gtest_discover_tests() already removed any
+  # empty arguments.
+  set(launcherArgs "")
+  if(NOT "${arg_TEST_EXECUTOR}" STREQUAL "")
+    list(JOIN arg_TEST_EXECUTOR "]==] [==[" launcherArgs)
+    set(launcherArgs "[==[${launcherArgs}]==]")
+  endif()
+
   # Run test executable to get list of available tests
   if(NOT EXISTS "${arg_TEST_EXECUTABLE}")
     message(FATAL_ERROR
@@ -111,12 +121,14 @@ function(gtest_discover_tests_impl)
       "  Path: '${arg_TEST_EXECUTABLE}'"
     )
   endif()
-  execute_process(
-    COMMAND ${arg_TEST_EXECUTOR} "${arg_TEST_EXECUTABLE}" --gtest_list_tests ${filter}
-    WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
-    TIMEOUT ${arg_TEST_DISCOVERY_TIMEOUT}
-    OUTPUT_VARIABLE output
-    RESULT_VARIABLE result
+  cmake_language(EVAL CODE
+    "execute_process(
+      COMMAND ${launcherArgs} [==[${arg_TEST_EXECUTABLE}]==] --gtest_list_tests ${filter}
+      WORKING_DIRECTORY [==[${arg_TEST_WORKING_DIR}]==]
+      TIMEOUT ${arg_TEST_DISCOVERY_TIMEOUT}
+      OUTPUT_VARIABLE output
+      RESULT_VARIABLE result
+    )"
   )
   if(NOT ${result} EQUAL 0)
     string(REPLACE "\n" "\n    " output "${output}")
@@ -188,16 +200,36 @@ function(gtest_discover_tests_impl)
         endif()
         set(guarded_testname "${open_guard}${testname}${close_guard}")
 
-        # add to script
-        add_command(add_test
-          "${guarded_testname}"
-          ${arg_TEST_EXECUTOR}
+        # Add to script. Do not use add_command() here because it messes up the
+        # handling of empty values when forwarding arguments, and we need to
+        # preserve those carefully for arg_TEST_EXECUTOR and arg_EXTRA_ARGS.
+        string(APPEND script "add_test(${guarded_testname} ${launcherArgs}")
+        foreach(arg IN ITEMS
           "${arg_TEST_EXECUTABLE}"
           "--gtest_filter=${suite}.${test}"
           "--gtest_also_run_disabled_tests"
           ${TEST_XML_OUTPUT_PARAM}
-          ${extra_args}
-        )
+          )
+          if(arg MATCHES "[^-./:a-zA-Z0-9_]")
+            string(APPEND script " [==[${arg}]==]")
+          else()
+            string(APPEND script " ${arg}")
+          endif()
+        endforeach()
+        if(arg_TEST_EXTRA_ARGS)
+          list(JOIN arg_TEST_EXTRA_ARGS "]==] [==[" extra_args)
+          string(APPEND script " [==[${extra_args}]==]")
+        endif()
+        string(APPEND script ")\n")
+        string(LENGTH "${script}" script_len)
+        if(${script_len} GREATER "50000")
+          # flush_script() expects to set variables in the parent scope, so we
+          # need to create one since we actually want the changes in our scope
+          block(SCOPE_FOR VARIABLES)
+            flush_script()
+          endblock()
+        endif()
+
         if(suite MATCHES "^DISABLED_" OR test MATCHES "^DISABLED_")
           add_command(set_tests_properties
             "${guarded_testname}"
@@ -210,7 +242,7 @@ function(gtest_discover_tests_impl)
           PROPERTIES
           WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
           SKIP_REGULAR_EXPRESSION "\\[  SKIPPED \\]"
-          ${properties}
+          ${arg_TEST_PROPERTIES}
         )
 
         # possibly unbalanced square brackets render lists invalid so skip such
@@ -244,7 +276,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
     NO_PRETTY_TYPES ${NO_PRETTY_TYPES}
     NO_PRETTY_VALUES ${NO_PRETTY_VALUES}
     TEST_EXECUTABLE ${TEST_EXECUTABLE}
-    TEST_EXECUTOR ${TEST_EXECUTOR}
+    TEST_EXECUTOR "${TEST_EXECUTOR}"
     TEST_WORKING_DIR ${TEST_WORKING_DIR}
     TEST_PREFIX ${TEST_PREFIX}
     TEST_SUFFIX ${TEST_SUFFIX}
@@ -253,7 +285,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
     CTEST_FILE ${CTEST_FILE}
     TEST_DISCOVERY_TIMEOUT ${TEST_DISCOVERY_TIMEOUT}
     TEST_XML_OUTPUT_DIR ${TEST_XML_OUTPUT_DIR}
-    TEST_EXTRA_ARGS ${TEST_EXTRA_ARGS}
-    TEST_PROPERTIES ${TEST_PROPERTIES}
+    TEST_EXTRA_ARGS "${TEST_EXTRA_ARGS}"
+    TEST_PROPERTIES "${TEST_PROPERTIES}"
   )
 endif()

+ 45 - 3
Source/cmAddTestCommand.cxx

@@ -2,14 +2,19 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmAddTestCommand.h"
 
+#include <algorithm>
+
 #include <cm/memory>
 
 #include "cmExecutionStatus.h"
 #include "cmMakefile.h"
+#include "cmPolicies.h"
 #include "cmStringAlgorithms.h"
 #include "cmTest.h"
 #include "cmTestGenerator.h"
 
+static std::string const keywordCMP0178 = "__CMP0178";
+
 static bool cmAddTestCommandHandleNameMode(
   std::vector<std::string> const& args, cmExecutionStatus& status);
 
@@ -29,8 +34,30 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
   }
 
   cmMakefile& mf = status.GetMakefile();
+  cmPolicies::PolicyStatus cmp0178;
+
+  // If the __CMP0178 keyword is present, it is always at the end
+  auto endOfCommandIter =
+    std::find(args.begin() + 2, args.end(), keywordCMP0178);
+  if (endOfCommandIter != args.end()) {
+    auto cmp0178Iter = endOfCommandIter + 1;
+    if (cmp0178Iter == args.end()) {
+      status.SetError(cmStrCat(keywordCMP0178, " keyword missing value"));
+      return false;
+    }
+    if (*cmp0178Iter == "NEW") {
+      cmp0178 = cmPolicies::PolicyStatus::NEW;
+    } else if (*cmp0178Iter == "OLD") {
+      cmp0178 = cmPolicies::PolicyStatus::OLD;
+    } else {
+      cmp0178 = cmPolicies::PolicyStatus::WARN;
+    }
+  } else {
+    cmp0178 = mf.GetPolicyStatus(cmPolicies::CMP0178);
+  }
+
   // Collect the command with arguments.
-  std::vector<std::string> command(args.begin() + 1, args.end());
+  std::vector<std::string> command(args.begin() + 1, endOfCommandIter);
 
   // Create the test but add a generator only the first time it is
   // seen.  This preserves behavior from before test generators.
@@ -46,6 +73,7 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
   } else {
     test = mf.CreateTest(args[0]);
     test->SetOldStyle(true);
+    test->SetCMP0178(cmp0178);
     mf.AddTestGenerator(cm::make_unique<cmTestGenerator>(test));
   }
   test->SetCommand(command);
@@ -56,11 +84,14 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
 bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
                                     cmExecutionStatus& status)
 {
+  cmMakefile& mf = status.GetMakefile();
+
   std::string name;
   std::vector<std::string> configurations;
   std::string working_directory;
   std::vector<std::string> command;
   bool command_expand_lists = false;
+  cmPolicies::PolicyStatus cmp0178 = mf.GetPolicyStatus(cmPolicies::CMP0178);
 
   // Read the arguments.
   enum Doing
@@ -69,6 +100,7 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
     DoingCommand,
     DoingConfigs,
     DoingWorkingDirectory,
+    DoingCmp0178,
     DoingNone
   };
   Doing doing = DoingName;
@@ -91,6 +123,8 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
         return false;
       }
       doing = DoingWorkingDirectory;
+    } else if (args[i] == keywordCMP0178) {
+      doing = DoingCmp0178;
     } else if (args[i] == "COMMAND_EXPAND_LISTS") {
       if (command_expand_lists) {
         status.SetError(" may be given at most one COMMAND_EXPAND_LISTS.");
@@ -108,6 +142,15 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
     } else if (doing == DoingWorkingDirectory) {
       working_directory = args[i];
       doing = DoingNone;
+    } else if (doing == DoingCmp0178) {
+      if (args[i] == "NEW") {
+        cmp0178 = cmPolicies::PolicyStatus::NEW;
+      } else if (args[i] == "OLD") {
+        cmp0178 = cmPolicies::PolicyStatus::OLD;
+      } else {
+        cmp0178 = cmPolicies::PolicyStatus::WARN;
+      }
+      doing = DoingNone;
     } else {
       status.SetError(cmStrCat(" given unknown argument:\n  ", args[i], "\n"));
       return false;
@@ -126,8 +169,6 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
     return false;
   }
 
-  cmMakefile& mf = status.GetMakefile();
-
   // Require a unique test name within the directory.
   if (mf.GetTest(name)) {
     status.SetError(cmStrCat(" given test NAME \"", name,
@@ -138,6 +179,7 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
   // Add the test.
   cmTest* test = mf.CreateTest(name);
   test->SetOldStyle(false);
+  test->SetCMP0178(cmp0178);
   test->SetCommand(command);
   if (!working_directory.empty()) {
     test->SetProperty("WORKING_DIRECTORY", working_directory);

+ 2 - 0
Source/cmPolicies.h

@@ -543,6 +543,8 @@ class cmMakefile;
   SELECT(POLICY, CMP0176, "execute_process() ENCODING is UTF-8 by default.",  \
          3, 31, 0, cmPolicies::WARN)                                          \
   SELECT(POLICY, CMP0177, "install() DESTINATION paths are normalized.", 3,   \
+         31, 0, cmPolicies::WARN)                                             \
+  SELECT(POLICY, CMP0178, "Test command lines preserve empty arguments.", 3,  \
          31, 0, cmPolicies::WARN)
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)

+ 1 - 0
Source/cmTest.cxx

@@ -10,6 +10,7 @@
 cmTest::cmTest(cmMakefile* mf)
   : Backtrace(mf->GetBacktrace())
   , PolicyStatusCMP0158(mf->GetPolicyStatus(cmPolicies::CMP0158))
+  , PolicyStatusCMP0178(mf->GetPolicyStatus(cmPolicies::CMP0178))
 {
   this->Makefile = mf;
   this->OldStyle = true;

+ 12 - 1
Source/cmTest.h

@@ -61,12 +61,22 @@ public:
   bool GetOldStyle() const { return this->OldStyle; }
   void SetOldStyle(bool b) { this->OldStyle = b; }
 
-  /** Get/Set if CMP0158 policy is NEW */
+  /** Get if CMP0158 policy is NEW */
   bool GetCMP0158IsNew() const
   {
     return this->PolicyStatusCMP0158 == cmPolicies::NEW;
   }
 
+  /** Get/Set the CMP0178 policy setting */
+  cmPolicies::PolicyStatus GetCMP0178() const
+  {
+    return this->PolicyStatusCMP0178;
+  }
+  void SetCMP0178(cmPolicies::PolicyStatus p)
+  {
+    this->PolicyStatusCMP0178 = p;
+  }
+
   /** Set/Get whether lists in command lines should be expanded. */
   bool GetCommandExpandLists() const;
   void SetCommandExpandLists(bool b);
@@ -82,4 +92,5 @@ private:
   cmMakefile* Makefile;
   cmListFileBacktrace Backtrace;
   cmPolicies::PolicyStatus PolicyStatusCMP0158;
+  cmPolicies::PolicyStatus PolicyStatusCMP0178;
 };

+ 24 - 3
Source/cmTestGenerator.cxx

@@ -174,15 +174,36 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
       if (!cmNonempty(launcher)) {
         return;
       }
-      cmList launcherWithArgs{ ge.Parse(*launcher)->Evaluate(this->LG,
-                                                             config) };
+      const auto propVal = ge.Parse(*launcher)->Evaluate(this->LG, config);
+      cmList launcherWithArgs(propVal, cmList::ExpandElements::Yes,
+                              this->Test->GetCMP0178() == cmPolicies::NEW
+                                ? cmList::EmptyElements::Yes
+                                : cmList::EmptyElements::No);
       if (!launcherWithArgs.empty() && !launcherWithArgs[0].empty()) {
+        if (this->Test->GetCMP0178() == cmPolicies::WARN) {
+          cmList argsWithEmptyValuesPreserved(
+            propVal, cmList::ExpandElements::Yes, cmList::EmptyElements::Yes);
+          if (launcherWithArgs != argsWithEmptyValuesPreserved) {
+            this->Test->GetMakefile()->IssueMessage(
+              MessageType::AUTHOR_WARNING,
+              cmStrCat("The ", propertyName, " property of target '",
+                       target->GetName(),
+                       "' contains empty list items. Those empty items are "
+                       "being silently discarded to preserve backward "
+                       "compatibility.\n",
+                       cmPolicies::GetPolicyWarning(cmPolicies::CMP0178)));
+          }
+        }
         std::string launcherExe(launcherWithArgs[0]);
         cmSystemTools::ConvertToUnixSlashes(launcherExe);
         os << cmOutputConverter::EscapeForCMake(launcherExe) << " ";
         for (std::string const& arg :
              cmMakeRange(launcherWithArgs).advance(1)) {
-          os << cmOutputConverter::EscapeForCMake(arg) << " ";
+          if (arg.empty()) {
+            os << "\"\" ";
+          } else {
+            os << cmOutputConverter::EscapeForCMake(arg) << " ";
+          }
         }
       }
     };

+ 0 - 17
Tests/RunCMake/GoogleTest/GoogleTestLauncher-test-stdout.txt

@@ -1,17 +0,0 @@
-1: Test command: "?[^
-]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]GoogleTestLauncher-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests"
-1: Working Directory: [^
-]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build
-1: Test timeout computed to be: [0-9]+
-1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]GoogleTestLauncher-build([/\]Debug)?[/\]test_launcher(\.exe)?'
-1: test_launcher: got arg 1 'launcherparam'
-1: test_launcher: got arg 2 '--'
-1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/test_launcher(\.exe)?'
-1: launching: "[^"]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests"
-1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]GoogleTestLauncher-build([/\]Debug)?[/\]test_launcher(\.exe)?'
-1: test_launcher: got arg 1 'emulatorparam'
-1: test_launcher: got arg 2 '--'
-1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/launcher_test(\.exe)?'
-1: launching: "[^"]*/Tests/RunCMake/GoogleTest/GoogleTestLauncher-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests"
-1: launcher_test\.test1
-1/1 Test #1: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec

+ 38 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-test-stdout.txt

@@ -0,0 +1,38 @@
+test 1
+    Start 1: launcher_test\.test1
+
+1: Test command: [^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-NEW-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "" "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/test_launcher(\.exe)?" "" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "" "b"
+1: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build
+1: Test timeout computed to be: [0-9]+
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-NEW-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 ''
+1: test_launcher: got arg 2 'launcherparam'
+1: test_launcher: got arg 3 '--'
+1: test_launcher: got arg 4 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/test_launcher(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/test_launcher(\.exe)?" "" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "" "b"
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-NEW-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 ''
+1: test_launcher: got arg 2 'emulatorparam'
+1: test_launcher: got arg 3 '--'
+1: test_launcher: got arg 4 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "" "b"
+1: launcher_test\.test1
+1/2 Test #1: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec
+test 2
+    Start 2: launcher_test\.test1
+
+2: Test command: [^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-NEW-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "" "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "" "b"
+2: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build
+2: Test timeout computed to be: [0-9]+
+2: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-NEW-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+2: test_launcher: got arg 1 ''
+2: test_launcher: got arg 2 'launcherparam'
+2: test_launcher: got arg 3 '--'
+2: test_launcher: got arg 4 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?'
+2: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "" "b"
+2: launcher_test.test1
+2/2 Test #2: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec

+ 2 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-NEW.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0178 NEW)
+include(Launcher.cmake)

+ 35 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-test-stdout.txt

@@ -0,0 +1,35 @@
+test 1
+    Start 1: launcher_test\.test1
+
+1: Test command: "?[^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-OLD-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build
+1: Test timeout computed to be: [0-9]+
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-OLD-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 'launcherparam'
+1: test_launcher: got arg 2 '--'
+1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/test_launcher(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-OLD-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 'emulatorparam'
+1: test_launcher: got arg 2 '--'
+1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: launcher_test\.test1
+1/2 Test #1: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec
+test 2
+    Start 2: launcher_test\.test1
+
+2: Test command: [^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-OLD-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "b"
+2: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build
+2: Test timeout computed to be: [0-9]+
+2: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-OLD-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+2: test_launcher: got arg 1 'launcherparam'
+2: test_launcher: got arg 2 '--'
+2: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?'
+2: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "b"
+2: launcher_test.test1
+2/2 Test #2: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec

+ 2 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-OLD.cmake

@@ -0,0 +1,2 @@
+cmake_policy(SET CMP0178 OLD)
+include(Launcher.cmake)

+ 38 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-stderr.txt

@@ -0,0 +1,38 @@
+CMake Warning \(dev\) at [^
+]*/Modules/GoogleTest\.cmake:[0-9]+ \(message\):
+  The 'launcher_test' target's TEST_LAUNCHER or CROSSCOMPILING_EMULATOR test
+  properties contain one or more empty values\.  Those empty values are being
+  silently discarded to preserve backward compatibility\.
+
+  Policy CMP0178 is not set: Test command lines preserve empty arguments\.
+  Run "cmake --help-policy CMP0178" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  Launcher\.cmake:[0-9]+ \(gtest_discover_tests\)
+  Launcher-CMP0178-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) at [^
+]*/Modules/GoogleTest\.cmake:[0-9]+ \(message\):
+  The EXTRA_ARGS value contains one or more empty values\.  Those empty values
+  are being silently discarded to preserve backward compatibility\.
+
+  Policy CMP0178 is not set: Test command lines preserve empty arguments\.
+  Run "cmake --help-policy CMP0178" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+Call Stack \(most recent call first\):
+  Launcher\.cmake:[0-9]+ \(gtest_discover_tests\)
+  Launcher-CMP0178-WARN\.cmake:1 \(include\)
+  CMakeLists\.txt:3 \(include\)
+This warning is for project developers\.  Use -Wno-dev to suppress it\.
+
+CMake Warning \(dev\) in CMakeLists\.txt:
+  The TEST_LAUNCHER property of target 'launcher_test' contains empty list
+  items\.  Those empty items are being silently discarded to preserve backward
+  compatibility\.
+
+  Policy CMP0178 is not set: Test command lines preserve empty arguments\.
+  Run "cmake --help-policy CMP0178" for policy details\.  Use the cmake_policy
+  command to set the policy and suppress this warning\.
+This warning is for project developers\.  Use -Wno-dev to suppress it\.

+ 35 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-test-stdout.txt

@@ -0,0 +1,35 @@
+test 1
+    Start 1: launcher_test\.test1
+
+1: Test command: "?[^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-WARN-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build
+1: Test timeout computed to be: [0-9]+
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-WARN-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 'launcherparam'
+1: test_launcher: got arg 2 '--'
+1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/test_launcher(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/test_launcher(\.exe)?" "emulatorparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-WARN-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+1: test_launcher: got arg 1 'emulatorparam'
+1: test_launcher: got arg 2 '--'
+1: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?'
+1: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "--gtest_also_run_disabled_tests" "a" "b"
+1: launcher_test\.test1
+1/2 Test #1: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec
+test 2
+    Start 2: launcher_test\.test1
+
+2: Test command: [^
+]*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-WARN-build([/\]Debug)?[/\]test_launcher(\.exe)?"? "launcherparam" "--" "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "b"
+2: Working Directory: [^
+]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build
+2: Test timeout computed to be: [0-9]+
+2: test_launcher: got arg 0 '[^']*[/\]Tests[/\]RunCMake[/\]GoogleTest[/\]Launcher-CMP0178-WARN-build([/\]Debug)?[/\]test_launcher(\.exe)?'
+2: test_launcher: got arg 1 'launcherparam'
+2: test_launcher: got arg 2 '--'
+2: test_launcher: got arg 3 '[^']*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?'
+2: launching: "[^"]*/Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN-build(/Debug)?/launcher_test(\.exe)?" "--gtest_filter=launcher_test\.test1" "a" "b"
+2: launcher_test.test1
+2/2 Test #2: launcher_test\.test1 [.]+ +Passed +[0-9.]+ sec

+ 1 - 0
Tests/RunCMake/GoogleTest/Launcher-CMP0178-WARN.cmake

@@ -0,0 +1 @@
+include(Launcher.cmake)

+ 8 - 2
Tests/RunCMake/GoogleTest/GoogleTestLauncher.cmake → Tests/RunCMake/GoogleTest/Launcher.cmake

@@ -11,14 +11,14 @@ add_executable(launcher_test launcher_test.c)
 xcode_sign_adhoc(launcher_test)
 set(launcher
   "$<TARGET_FILE:test_launcher>"
-  ""   # Verify that an empty list item will be preserved
+  ""   # Verify CMP0178's handling of an empty list item
   "launcherparam"
   "--"
 )
 set_property(TARGET launcher_test PROPERTY TEST_LAUNCHER "${launcher}")
 set(emulator
   "$<TARGET_FILE:test_launcher>"
-  ""   # Verify that an empty list item will be preserved
+  ""   # Verify CMP0178's handling of an empty list item
   "emulatorparam"
   "--"
 )
@@ -26,4 +26,10 @@ set_property(TARGET launcher_test PROPERTY CROSSCOMPILING_EMULATOR "${emulator}"
 
 gtest_discover_tests(
   launcher_test
+  EXTRA_ARGS a "" b
+)
+
+gtest_add_tests(
+  TARGET launcher_test
+  EXTRA_ARGS a "" b
 )

+ 11 - 9
Tests/RunCMake/GoogleTest/RunCMakeTest.cmake

@@ -101,7 +101,7 @@ function(run_GoogleTest DISCOVERY_MODE)
   )
 endfunction()
 
-function(run_GoogleTestLauncher DISCOVERY_MODE)
+function(run_Launcher_CMP0178 DISCOVERY_MODE cmp0178)
   if(CMAKE_C_COMPILER_ID STREQUAL "MSVC" AND CMAKE_C_COMPILER_VERSION VERSION_LESS "14.0")
     return()
   endif()
@@ -110,12 +110,12 @@ function(run_GoogleTestLauncher DISCOVERY_MODE)
   endif()
 
   # Use a single build tree for a few tests without cleaning.
-  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/GoogleTestLauncher-build)
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/Launcher-CMP0178-${cmp0178}-build)
   if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
     set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
   endif()
 
-  run_cmake_with_options(GoogleTestLauncher
+  run_cmake_with_options(Launcher-CMP0178-${cmp0178}
     -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=${DISCOVERY_MODE}
   )
 
@@ -123,14 +123,14 @@ function(run_GoogleTestLauncher DISCOVERY_MODE)
 
   # do not issue any warnings on stderr that would cause the build to fail
   set(RunCMake_TEST_OUTPUT_MERGE 1)
-  run_cmake_command(GoogleTestLauncher-build
+  run_cmake_command(Launcher-CMP0178-${cmp0178}-build
     ${CMAKE_COMMAND}
     --build .
     --config Debug
   )
   unset(RunCMake_TEST_OUTPUT_MERGE)
 
-  run_cmake_command(GoogleTestLauncher-test
+  run_cmake_command(Launcher-CMP0178-${cmp0178}-test
     ${CMAKE_CTEST_COMMAND}
     -C Debug
     -V
@@ -356,11 +356,13 @@ function(run_GoogleTest_discovery_test_list_scoped DISCOVERY_MODE)
 endfunction()
 
 foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
-  message("Testing ${DISCOVERY_MODE} discovery mode via CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE global override...")
+  message(STATUS "Testing ${DISCOVERY_MODE} discovery mode via CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE global override...")
   run_GoogleTest(${DISCOVERY_MODE})
-  run_GoogleTestLauncher(${DISCOVERY_MODE})
   run_GoogleTestXML(${DISCOVERY_MODE})
-  message("Testing ${DISCOVERY_MODE} discovery mode via DISCOVERY_MODE option...")
+  run_Launcher_CMP0178(${DISCOVERY_MODE} NEW)
+  run_Launcher_CMP0178(${DISCOVERY_MODE} OLD)
+  run_Launcher_CMP0178(${DISCOVERY_MODE} WARN)
+  message(STATUS "Testing ${DISCOVERY_MODE} discovery mode via DISCOVERY_MODE option...")
   run_GoogleTest_discovery_timeout(${DISCOVERY_MODE})
   run_GoogleTest_discovery_arg_change(${DISCOVERY_MODE})
   run_GoogleTest_discovery_test_list(${DISCOVERY_MODE})
@@ -369,6 +371,6 @@ foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
 endforeach()
 
 if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
-  message("Testing PRE_TEST discovery multi configuration...")
+  message(STATUS "Testing PRE_TEST discovery multi configuration...")
   run_GoogleTest_discovery_multi_config()
 endif()

+ 1 - 1
Tests/RunCMake/GoogleTest/launcher_test.c

@@ -10,7 +10,7 @@ TEST_F( launcher_test, test1 )
 
 int main(int argc, char** argv)
 {
-  /* Note: GoogleTest.cmake doesn't actually depend on Google Test as such;
+  /* Note: Launcher.cmake doesn't actually depend on Google Test as such;
    * it only requires that we produces output in the expected format when
    * invoked with --gtest_list_tests. Thus, we fake that here. This allows us
    * to test the module without actually needing Google Test.  */