瀏覽代碼

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 年之前
父節點
當前提交
fc7aa3cd69

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

@@ -57,6 +57,7 @@ Policies Introduced by CMake 3.31
 .. toctree::
 .. toctree::
    :maxdepth: 1
    :maxdepth: 1
 
 
+   CMP0178: Test command lines preserve empty arguments. </policy/CMP0178>
    CMP0177: install() DESTINATION paths are normalized. </policy/CMP0177>
    CMP0177: install() DESTINATION paths are normalized. </policy/CMP0177>
    CMP0176: execute_process() ENCODING is UTF-8 by default. </policy/CMP0176>
    CMP0176: execute_process() ENCODING is UTF-8 by default. </policy/CMP0176>
    CMP0175: add_custom_command() rejects invalid arguments. </policy/CMP0175>
    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)
   if(ANDROID)
     string(REGEX REPLACE "DATA{([^ ;]+)}" "\\1"  processed_FILES "${AST_FILES}")
     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(
     add_test(
       NAME ${test_name}
       NAME ${test_name}
       COMMAND ${CMAKE_COMMAND}
       COMMAND ${CMAKE_COMMAND}

+ 17 - 1
Modules/ExternalData.cmake

@@ -72,6 +72,12 @@ Module Functions
   It passes its arguments through ``ExternalData_Expand_Arguments`` and then
   It passes its arguments through ``ExternalData_Expand_Arguments`` and then
   invokes the :command:`add_test` command using the results.
   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
 .. command:: ExternalData_Add_Target
 
 
   The ``ExternalData_Add_Target`` function creates a custom target to
   The ``ExternalData_Add_Target`` function creates a custom target to
@@ -353,7 +359,17 @@ file or set a variable:
 function(ExternalData_add_test target)
 function(ExternalData_add_test target)
   # Expand all arguments as a single string to preserve escaped semicolons.
   # Expand all arguments as a single string to preserve escaped semicolons.
   ExternalData_expand_arguments("${target}" testArgs "${ARGN}")
   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()
 endfunction()
 
 
 function(ExternalData_add_target target)
 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)
     set_source_files_properties(${_cxxtest_real_outfname} PROPERTIES GENERATED true)
     add_executable(${_cxxtest_testname} ${_cxxtest_real_outfname} ${ARGN})
     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)
     if(CMAKE_RUNTIME_OUTPUT_DIRECTORY)
         add_test(${_cxxtest_testname} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${_cxxtest_testname})
         add_test(${_cxxtest_testname} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${_cxxtest_testname})
     elseif(EXECUTABLE_OUTPUT_PATH)
     elseif(EXECUTABLE_OUTPUT_PATH)

+ 4 - 0
Modules/FindMatlab.cmake

@@ -1012,6 +1012,10 @@ function(matlab_add_unit_test)
     endif()
     endif()
   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}
   add_test(NAME ${${prefix}_NAME}
            COMMAND ${CMAKE_COMMAND}
            COMMAND ${CMAKE_COMMAND}
             "-Dtest_name=${${prefix}_NAME}"
             "-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.")
     message(STATUS "Using squish_v3_add_test(), but SQUISH_VERSION_MAJOR is ${SQUISH_VERSION_MAJOR}.\nThis may not work.")
   endif()
   endif()
 
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(${testName}
   add_test(${testName}
     ${CMAKE_COMMAND} -V -VV
     ${CMAKE_COMMAND} -V -VV
     "-Dsquish_version:STRING=3"
     "-Dsquish_version:STRING=3"
@@ -258,6 +260,8 @@ function(squish_v4_add_test testName)
     message("SETTINGSGROUP is deprecated and will be ignored.")
     message("SETTINGSGROUP is deprecated and will be ignored.")
   endif()
   endif()
 
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(NAME ${testName}
   add_test(NAME ${testName}
     COMMAND ${CMAKE_COMMAND} -V -VV
     COMMAND ${CMAKE_COMMAND} -V -VV
     "-Dsquish_version:STRING=4"
     "-Dsquish_version:STRING=4"

+ 2 - 0
Modules/FindXCTest.cmake

@@ -210,6 +210,8 @@ function(xctest_add_test name bundle)
 
 
   # register test
   # register test
 
 
+  # There's no target used for this command, so we don't need to do anything
+  # here for CMP0178.
   add_test(
   add_test(
     NAME ${name}
     NAME ${name}
     COMMAND ${XCTest_EXECUTABLE} $<TARGET_BUNDLE_DIR:${bundle}>)
     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
     gtest_add_tests(TARGET target
                     [SOURCES src1...]
                     [SOURCES src1...]
-                    [EXTRA_ARGS arg1...]
+                    [EXTRA_ARGS args...]
                     [WORKING_DIRECTORY dir]
                     [WORKING_DIRECTORY dir]
                     [TEST_PREFIX prefix]
                     [TEST_PREFIX prefix]
                     [TEST_SUFFIX suffix]
                     [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
     this option is not given, the :prop_tgt:`SOURCES` property of the
     specified ``target`` will be used to obtain the list of sources.
     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.
     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``
   ``WORKING_DIRECTORY dir``
     Specifies the directory in which to run the discovered test cases.  If this
     Specifies the directory in which to run the discovered test cases.  If this
     option is not provided, the current binary directory is used.
     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
     with the list of discovered test cases.  This allows the caller to do
     things like manipulate test properties of the discovered tests.
     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:
   Usage example:
 
 
   .. code-block:: cmake
   .. code-block:: cmake
@@ -147,7 +155,7 @@ same as the Google Test name (i.e. ``suite.testcase``); see also
   for available tests::
   for available tests::
 
 
     gtest_discover_tests(target
     gtest_discover_tests(target
-                         [EXTRA_ARGS arg1...]
+                         [EXTRA_ARGS args...]
                          [WORKING_DIRECTORY dir]
                          [WORKING_DIRECTORY dir]
                          [TEST_PREFIX prefix]
                          [TEST_PREFIX prefix]
                          [TEST_SUFFIX suffix]
                          [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 target.  CMake will substitute the location of the built
     executable when running the test.
     executable when running the test.
 
 
-  ``EXTRA_ARGS arg1...``
+  ``EXTRA_ARGS args...``
     Any extra arguments to pass on the command line to each test case.
     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``
   ``WORKING_DIRECTORY dir``
     Specifies the directory in which to run the discovered test cases.  If this
     Specifies the directory in which to run the discovered test cases.  If this
     option is not provided, the current binary directory is used.
     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
     for globally selecting a preferred test discovery behavior without having
     to modify each call site.
     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
 # Save project's policies
@@ -312,9 +332,41 @@ function(gtest_add_tests)
   )
   )
   set(allKeywords ${options} ${oneValueArgs} ${multiValueArgs})
   set(allKeywords ${options} ${oneValueArgs} ${multiValueArgs})
 
 
+  cmake_policy(GET CMP0178 cmp0178
+    PARENT_SCOPE # undocumented, do not use outside of CMake
+  )
+
   unset(sources)
   unset(sources)
   if("${ARGV0}" IN_LIST allKeywords)
   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)
     set(autoAddSources YES)
   else()
   else()
     # Non-keyword syntax, convert to keyword form
     # Non-keyword syntax, convert to keyword form
@@ -408,6 +460,11 @@ function(gtest_add_tests)
         continue()
         continue()
       endif()
       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
       # Make sure tests disabled in GTest get disabled in CTest
       if(gtest_test_name MATCHES "(^|\\.)DISABLED_")
       if(gtest_test_name MATCHES "(^|\\.)DISABLED_")
         # Add the disabled test if CMake is new enough
         # Add the disabled test if CMake is new enough
@@ -422,12 +479,15 @@ function(gtest_add_tests)
           set(ctest_test_name
           set(ctest_test_name
               ${arg_TEST_PREFIX}${orig_test_name}${arg_TEST_SUFFIX}
               ${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
           set_tests_properties(${ctest_test_name} PROPERTIES DISABLED TRUE
             DEF_SOURCE_LINE "${source}:${accumulate_line}")
             DEF_SOURCE_LINE "${source}:${accumulate_line}")
@@ -435,11 +495,14 @@ function(gtest_add_tests)
         endif()
         endif()
       else()
       else()
         set(ctest_test_name ${arg_TEST_PREFIX}${gtest_test_name}${arg_TEST_SUFFIX})
         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
         # Makes sure a skipped GTest is reported as so by CTest
         set_tests_properties(
         set_tests_properties(
@@ -480,9 +543,8 @@ function(gtest_discover_tests target)
     PROPERTIES
     PROPERTIES
     TEST_FILTER
     TEST_FILTER
   )
   )
-  cmake_parse_arguments(arg
+  cmake_parse_arguments(PARSE_ARGV 1 arg
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
-    ${ARGN}
   )
   )
 
 
   if(NOT arg_WORKING_DIRECTORY)
   if(NOT arg_WORKING_DIRECTORY)
@@ -551,6 +613,38 @@ function(gtest_discover_tests target)
     set(test_executor "")
     set(test_executor "")
   endif()
   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")
   if(arg_DISCOVERY_MODE STREQUAL "POST_BUILD")
     add_custom_command(
     add_custom_command(
       TARGET ${target} POST_BUILD
       TARGET ${target} POST_BUILD

+ 57 - 25
Modules/GoogleTestAddTests.cmake

@@ -2,6 +2,7 @@
 # file Copyright.txt or https://cmake.org/licensing for details.
 # file Copyright.txt or https://cmake.org/licensing for details.
 
 
 cmake_minimum_required(VERSION 3.30)
 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
 # Overwrite possibly existing ${arg_CTEST_FILE} with empty file
 set(flush_tests_MODE WRITE)
 set(flush_tests_MODE WRITE)
@@ -65,9 +66,9 @@ endfunction()
 
 
 function(gtest_discover_tests_impl)
 function(gtest_discover_tests_impl)
 
 
-  set(options )
+  set(options "")
   set(oneValueArgs
   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  #
     NO_PRETTY_VALUES  #
     TEST_EXECUTABLE
     TEST_EXECUTABLE
     TEST_WORKING_DIR
     TEST_WORKING_DIR
@@ -77,22 +78,21 @@ function(gtest_discover_tests_impl)
     CTEST_FILE
     CTEST_FILE
     TEST_DISCOVERY_TIMEOUT
     TEST_DISCOVERY_TIMEOUT
     TEST_XML_OUTPUT_DIR
     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_EXTRA_ARGS
     TEST_PROPERTIES
     TEST_PROPERTIES
     TEST_EXECUTOR
     TEST_EXECUTOR
   )
   )
-  cmake_parse_arguments(arg
+  set(multiValueArgs "")
+  cmake_parse_arguments(PARSE_ARGV 0 arg
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
     "${options}" "${oneValueArgs}" "${multiValueArgs}"
-    ${ARGN}
   )
   )
 
 
   set(prefix "${arg_TEST_PREFIX}")
   set(prefix "${arg_TEST_PREFIX}")
   set(suffix "${arg_TEST_SUFFIX}")
   set(suffix "${arg_TEST_SUFFIX}")
-  set(extra_args ${arg_TEST_EXTRA_ARGS})
-  set(properties ${arg_TEST_PROPERTIES})
   set(script)
   set(script)
   set(suite)
   set(suite)
   set(tests)
   set(tests)
@@ -104,6 +104,16 @@ function(gtest_discover_tests_impl)
     set(filter)
     set(filter)
   endif()
   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
   # Run test executable to get list of available tests
   if(NOT EXISTS "${arg_TEST_EXECUTABLE}")
   if(NOT EXISTS "${arg_TEST_EXECUTABLE}")
     message(FATAL_ERROR
     message(FATAL_ERROR
@@ -111,12 +121,14 @@ function(gtest_discover_tests_impl)
       "  Path: '${arg_TEST_EXECUTABLE}'"
       "  Path: '${arg_TEST_EXECUTABLE}'"
     )
     )
   endif()
   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)
   if(NOT ${result} EQUAL 0)
     string(REPLACE "\n" "\n    " output "${output}")
     string(REPLACE "\n" "\n    " output "${output}")
@@ -188,16 +200,36 @@ function(gtest_discover_tests_impl)
         endif()
         endif()
         set(guarded_testname "${open_guard}${testname}${close_guard}")
         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}"
           "${arg_TEST_EXECUTABLE}"
           "--gtest_filter=${suite}.${test}"
           "--gtest_filter=${suite}.${test}"
           "--gtest_also_run_disabled_tests"
           "--gtest_also_run_disabled_tests"
           ${TEST_XML_OUTPUT_PARAM}
           ${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_")
         if(suite MATCHES "^DISABLED_" OR test MATCHES "^DISABLED_")
           add_command(set_tests_properties
           add_command(set_tests_properties
             "${guarded_testname}"
             "${guarded_testname}"
@@ -210,7 +242,7 @@ function(gtest_discover_tests_impl)
           PROPERTIES
           PROPERTIES
           WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
           WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
           SKIP_REGULAR_EXPRESSION "\\[  SKIPPED \\]"
           SKIP_REGULAR_EXPRESSION "\\[  SKIPPED \\]"
-          ${properties}
+          ${arg_TEST_PROPERTIES}
         )
         )
 
 
         # possibly unbalanced square brackets render lists invalid so skip such
         # 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_TYPES ${NO_PRETTY_TYPES}
     NO_PRETTY_VALUES ${NO_PRETTY_VALUES}
     NO_PRETTY_VALUES ${NO_PRETTY_VALUES}
     TEST_EXECUTABLE ${TEST_EXECUTABLE}
     TEST_EXECUTABLE ${TEST_EXECUTABLE}
-    TEST_EXECUTOR ${TEST_EXECUTOR}
+    TEST_EXECUTOR "${TEST_EXECUTOR}"
     TEST_WORKING_DIR ${TEST_WORKING_DIR}
     TEST_WORKING_DIR ${TEST_WORKING_DIR}
     TEST_PREFIX ${TEST_PREFIX}
     TEST_PREFIX ${TEST_PREFIX}
     TEST_SUFFIX ${TEST_SUFFIX}
     TEST_SUFFIX ${TEST_SUFFIX}
@@ -253,7 +285,7 @@ if(CMAKE_SCRIPT_MODE_FILE)
     CTEST_FILE ${CTEST_FILE}
     CTEST_FILE ${CTEST_FILE}
     TEST_DISCOVERY_TIMEOUT ${TEST_DISCOVERY_TIMEOUT}
     TEST_DISCOVERY_TIMEOUT ${TEST_DISCOVERY_TIMEOUT}
     TEST_XML_OUTPUT_DIR ${TEST_XML_OUTPUT_DIR}
     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()
 endif()

+ 45 - 3
Source/cmAddTestCommand.cxx

@@ -2,14 +2,19 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmAddTestCommand.h"
 #include "cmAddTestCommand.h"
 
 
+#include <algorithm>
+
 #include <cm/memory>
 #include <cm/memory>
 
 
 #include "cmExecutionStatus.h"
 #include "cmExecutionStatus.h"
 #include "cmMakefile.h"
 #include "cmMakefile.h"
+#include "cmPolicies.h"
 #include "cmStringAlgorithms.h"
 #include "cmStringAlgorithms.h"
 #include "cmTest.h"
 #include "cmTest.h"
 #include "cmTestGenerator.h"
 #include "cmTestGenerator.h"
 
 
+static std::string const keywordCMP0178 = "__CMP0178";
+
 static bool cmAddTestCommandHandleNameMode(
 static bool cmAddTestCommandHandleNameMode(
   std::vector<std::string> const& args, cmExecutionStatus& status);
   std::vector<std::string> const& args, cmExecutionStatus& status);
 
 
@@ -29,8 +34,30 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
   }
   }
 
 
   cmMakefile& mf = status.GetMakefile();
   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.
   // 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
   // Create the test but add a generator only the first time it is
   // seen.  This preserves behavior from before test generators.
   // seen.  This preserves behavior from before test generators.
@@ -46,6 +73,7 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
   } else {
   } else {
     test = mf.CreateTest(args[0]);
     test = mf.CreateTest(args[0]);
     test->SetOldStyle(true);
     test->SetOldStyle(true);
+    test->SetCMP0178(cmp0178);
     mf.AddTestGenerator(cm::make_unique<cmTestGenerator>(test));
     mf.AddTestGenerator(cm::make_unique<cmTestGenerator>(test));
   }
   }
   test->SetCommand(command);
   test->SetCommand(command);
@@ -56,11 +84,14 @@ bool cmAddTestCommand(std::vector<std::string> const& args,
 bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
 bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
                                     cmExecutionStatus& status)
                                     cmExecutionStatus& status)
 {
 {
+  cmMakefile& mf = status.GetMakefile();
+
   std::string name;
   std::string name;
   std::vector<std::string> configurations;
   std::vector<std::string> configurations;
   std::string working_directory;
   std::string working_directory;
   std::vector<std::string> command;
   std::vector<std::string> command;
   bool command_expand_lists = false;
   bool command_expand_lists = false;
+  cmPolicies::PolicyStatus cmp0178 = mf.GetPolicyStatus(cmPolicies::CMP0178);
 
 
   // Read the arguments.
   // Read the arguments.
   enum Doing
   enum Doing
@@ -69,6 +100,7 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
     DoingCommand,
     DoingCommand,
     DoingConfigs,
     DoingConfigs,
     DoingWorkingDirectory,
     DoingWorkingDirectory,
+    DoingCmp0178,
     DoingNone
     DoingNone
   };
   };
   Doing doing = DoingName;
   Doing doing = DoingName;
@@ -91,6 +123,8 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
         return false;
         return false;
       }
       }
       doing = DoingWorkingDirectory;
       doing = DoingWorkingDirectory;
+    } else if (args[i] == keywordCMP0178) {
+      doing = DoingCmp0178;
     } else if (args[i] == "COMMAND_EXPAND_LISTS") {
     } else if (args[i] == "COMMAND_EXPAND_LISTS") {
       if (command_expand_lists) {
       if (command_expand_lists) {
         status.SetError(" may be given at most one 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) {
     } else if (doing == DoingWorkingDirectory) {
       working_directory = args[i];
       working_directory = args[i];
       doing = DoingNone;
       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 {
     } else {
       status.SetError(cmStrCat(" given unknown argument:\n  ", args[i], "\n"));
       status.SetError(cmStrCat(" given unknown argument:\n  ", args[i], "\n"));
       return false;
       return false;
@@ -126,8 +169,6 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
     return false;
     return false;
   }
   }
 
 
-  cmMakefile& mf = status.GetMakefile();
-
   // Require a unique test name within the directory.
   // Require a unique test name within the directory.
   if (mf.GetTest(name)) {
   if (mf.GetTest(name)) {
     status.SetError(cmStrCat(" given test NAME \"", name,
     status.SetError(cmStrCat(" given test NAME \"", name,
@@ -138,6 +179,7 @@ bool cmAddTestCommandHandleNameMode(std::vector<std::string> const& args,
   // Add the test.
   // Add the test.
   cmTest* test = mf.CreateTest(name);
   cmTest* test = mf.CreateTest(name);
   test->SetOldStyle(false);
   test->SetOldStyle(false);
+  test->SetCMP0178(cmp0178);
   test->SetCommand(command);
   test->SetCommand(command);
   if (!working_directory.empty()) {
   if (!working_directory.empty()) {
     test->SetProperty("WORKING_DIRECTORY", working_directory);
     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.",  \
   SELECT(POLICY, CMP0176, "execute_process() ENCODING is UTF-8 by default.",  \
          3, 31, 0, cmPolicies::WARN)                                          \
          3, 31, 0, cmPolicies::WARN)                                          \
   SELECT(POLICY, CMP0177, "install() DESTINATION paths are normalized.", 3,   \
   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)
          31, 0, cmPolicies::WARN)
 
 
 #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1)
 #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)
 cmTest::cmTest(cmMakefile* mf)
   : Backtrace(mf->GetBacktrace())
   : Backtrace(mf->GetBacktrace())
   , PolicyStatusCMP0158(mf->GetPolicyStatus(cmPolicies::CMP0158))
   , PolicyStatusCMP0158(mf->GetPolicyStatus(cmPolicies::CMP0158))
+  , PolicyStatusCMP0178(mf->GetPolicyStatus(cmPolicies::CMP0178))
 {
 {
   this->Makefile = mf;
   this->Makefile = mf;
   this->OldStyle = true;
   this->OldStyle = true;

+ 12 - 1
Source/cmTest.h

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

+ 24 - 3
Source/cmTestGenerator.cxx

@@ -174,15 +174,36 @@ void cmTestGenerator::GenerateScriptForConfig(std::ostream& os,
       if (!cmNonempty(launcher)) {
       if (!cmNonempty(launcher)) {
         return;
         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 (!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]);
         std::string launcherExe(launcherWithArgs[0]);
         cmSystemTools::ConvertToUnixSlashes(launcherExe);
         cmSystemTools::ConvertToUnixSlashes(launcherExe);
         os << cmOutputConverter::EscapeForCMake(launcherExe) << " ";
         os << cmOutputConverter::EscapeForCMake(launcherExe) << " ";
         for (std::string const& arg :
         for (std::string const& arg :
              cmMakeRange(launcherWithArgs).advance(1)) {
              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)
 xcode_sign_adhoc(launcher_test)
 set(launcher
 set(launcher
   "$<TARGET_FILE:test_launcher>"
   "$<TARGET_FILE:test_launcher>"
-  ""   # Verify that an empty list item will be preserved
+  ""   # Verify CMP0178's handling of an empty list item
   "launcherparam"
   "launcherparam"
   "--"
   "--"
 )
 )
 set_property(TARGET launcher_test PROPERTY TEST_LAUNCHER "${launcher}")
 set_property(TARGET launcher_test PROPERTY TEST_LAUNCHER "${launcher}")
 set(emulator
 set(emulator
   "$<TARGET_FILE:test_launcher>"
   "$<TARGET_FILE:test_launcher>"
-  ""   # Verify that an empty list item will be preserved
+  ""   # Verify CMP0178's handling of an empty list item
   "emulatorparam"
   "emulatorparam"
   "--"
   "--"
 )
 )
@@ -26,4 +26,10 @@ set_property(TARGET launcher_test PROPERTY CROSSCOMPILING_EMULATOR "${emulator}"
 
 
 gtest_discover_tests(
 gtest_discover_tests(
   launcher_test
   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()
 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")
   if(CMAKE_C_COMPILER_ID STREQUAL "MSVC" AND CMAKE_C_COMPILER_VERSION VERSION_LESS "14.0")
     return()
     return()
   endif()
   endif()
@@ -110,12 +110,12 @@ function(run_GoogleTestLauncher DISCOVERY_MODE)
   endif()
   endif()
 
 
   # Use a single build tree for a few tests without cleaning.
   # 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)
   if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
     set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
     set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
   endif()
   endif()
 
 
-  run_cmake_with_options(GoogleTestLauncher
+  run_cmake_with_options(Launcher-CMP0178-${cmp0178}
     -DCMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE=${DISCOVERY_MODE}
     -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
   # do not issue any warnings on stderr that would cause the build to fail
   set(RunCMake_TEST_OUTPUT_MERGE 1)
   set(RunCMake_TEST_OUTPUT_MERGE 1)
-  run_cmake_command(GoogleTestLauncher-build
+  run_cmake_command(Launcher-CMP0178-${cmp0178}-build
     ${CMAKE_COMMAND}
     ${CMAKE_COMMAND}
     --build .
     --build .
     --config Debug
     --config Debug
   )
   )
   unset(RunCMake_TEST_OUTPUT_MERGE)
   unset(RunCMake_TEST_OUTPUT_MERGE)
 
 
-  run_cmake_command(GoogleTestLauncher-test
+  run_cmake_command(Launcher-CMP0178-${cmp0178}-test
     ${CMAKE_CTEST_COMMAND}
     ${CMAKE_CTEST_COMMAND}
     -C Debug
     -C Debug
     -V
     -V
@@ -356,11 +356,13 @@ function(run_GoogleTest_discovery_test_list_scoped DISCOVERY_MODE)
 endfunction()
 endfunction()
 
 
 foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
 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_GoogleTest(${DISCOVERY_MODE})
-  run_GoogleTestLauncher(${DISCOVERY_MODE})
   run_GoogleTestXML(${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_timeout(${DISCOVERY_MODE})
   run_GoogleTest_discovery_arg_change(${DISCOVERY_MODE})
   run_GoogleTest_discovery_arg_change(${DISCOVERY_MODE})
   run_GoogleTest_discovery_test_list(${DISCOVERY_MODE})
   run_GoogleTest_discovery_test_list(${DISCOVERY_MODE})
@@ -369,6 +371,6 @@ foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
 endforeach()
 endforeach()
 
 
 if(RunCMake_GENERATOR_IS_MULTI_CONFIG)
 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()
   run_GoogleTest_discovery_multi_config()
 endif()
 endif()

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

@@ -10,7 +10,7 @@ TEST_F( launcher_test, test1 )
 
 
 int main(int argc, char** argv)
 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
    * 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
    * invoked with --gtest_list_tests. Thus, we fake that here. This allows us
    * to test the module without actually needing Google Test.  */
    * to test the module without actually needing Google Test.  */