Просмотр исходного кода

GoogleTest: Parse discovered test list from JSON output if supported

The --gtest_output=json option is supported from gtest 1.8.1 onwards.
Earlier versions output a warning about it being an unrecognized option,
and builds that #define GTEST_HAS_FILE_SYSTEM 0 output an error about it
being unsupported, but in both cases still continue outputting the same
plain text output as before and return an exit code of 0.

We now add that option and check for whether the JSON file is generated,
falling back to parsing the plain text output as before if it isn't or
if the environment variable NO_GTEST_JSON_OUTPUT is set.

The fake executor binaries are sensitive to parameter order. This commit
adds --gtest_output:json arguments to a number of tests added for the
new JSON parser. The argument order has been adjusted for the
invocations of those binaries.

Co-Authored-By: Craig Scott <[email protected]>
Co-Authored-By: Dennis Lambe Jr. <[email protected]>
Ottmar Zittlau 6 месяцев назад
Родитель
Сommit
1cdceae8e3

+ 262 - 126
Modules/GoogleTestAddTests.cmake

@@ -43,6 +43,240 @@ function(escape_square_brackets output bracket placeholder placeholder_var outpu
   endif()
   endif()
 endfunction()
 endfunction()
 
 
+macro(write_test_to_file)
+  # Store the gtest test name before messing with these strings
+  set(gtest_name ${current_test_suite}.${current_test_name})
+
+  set(pretty_test_suite ${current_test_suite})
+  set(pretty_test_name ${current_test_name})
+
+  # Handle disabled tests
+  set(maybe_DISABLED "")
+  if(pretty_test_suite MATCHES "^DISABLED_" OR pretty_test_name MATCHES "^DISABLED_")
+    set(maybe_DISABLED DISABLED YES)
+    string(REGEX REPLACE "^DISABLED_" "" pretty_test_suite "${pretty_test_suite}")
+    string(REGEX REPLACE "^DISABLED_" "" pretty_test_name "${pretty_test_name}")
+  endif()
+
+  set(pretty_type_param "")
+  if(NOT current_test_type_param STREQUAL "")
+    # Parse test suite index from name
+    set(current_test_suite_index "")
+    if(pretty_test_suite MATCHES "^(.+)/([0-9]+)$")
+      set(pretty_test_suite ${CMAKE_MATCH_1})
+      set(current_test_suite_index ${CMAKE_MATCH_2})
+    endif()
+    if(NOT current_test_suite_index STREQUAL "")
+      if(arg_NO_PRETTY_TYPES)
+        set(pretty_type_param "<${current_test_suite_index}>")
+      else()
+        set(pretty_type_param "<${current_test_type_param}>")
+      endif()
+    else()
+      # bug compatibility with https://gitlab.kitware.com/cmake/cmake/-/issues/26939
+      set(pretty_test_suite "${pretty_test_suite}.  # TypeParam = ${current_test_type_param}")
+      set(pretty_type_param "<${pretty_test_suite}>")
+    endif()
+  endif()
+  if (NOT current_test_value_param STREQUAL "")
+    # Parse test index from name
+    if(NOT arg_NO_PRETTY_VALUES)
+      if(pretty_test_name MATCHES "^(.+)/[0-9]+$")
+        set(pretty_test_name "${CMAKE_MATCH_1}/${current_test_value_param}")
+      else()
+        # bug compatibility with https://gitlab.kitware.com/cmake/cmake/-/issues/26939
+        set(pretty_test_name "${pretty_test_name}  # GetParam() = ${current_test_value_param}")
+      endif()
+    endif()
+  endif()
+
+  set(test_name_template "@prefix@@pretty_test_suite@.@pretty_test_name@@pretty_type_param@@suffix@")
+  string(CONFIGURE "${test_name_template}" testname)
+
+  if(NOT "${arg_TEST_XML_OUTPUT_DIR}" STREQUAL "")
+    set(TEST_XML_OUTPUT_PARAM "--gtest_output=xml:${arg_TEST_XML_OUTPUT_DIR}/${prefix}${gtest_name}${suffix}.xml")
+  else()
+    set(TEST_XML_OUTPUT_PARAM "")
+  endif()
+
+  # unescape []
+  if(open_sb)
+    string(REPLACE "${open_sb}" "[" testname "${testname}")
+  endif()
+  if(close_sb)
+    string(REPLACE "${close_sb}" "]" testname "${testname}")
+  endif()
+  set(guarded_testname "${open_guard}${testname}${close_guard}")
+  # 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=${gtest_name}"
+    "--gtest_also_run_disabled_tests"
+    ${TEST_XML_OUTPUT_PARAM}
+  )
+
+    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")
+
+  add_command(set_tests_properties
+    "${guarded_testname}"
+    PROPERTIES
+      ${maybe_DISABLED}
+      WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
+      SKIP_REGULAR_EXPRESSION "\\[  SKIPPED \\]"
+      ${arg_TEST_PROPERTIES}
+  )
+
+  # possibly unbalanced square brackets render lists invalid so skip such
+  # tests in ${arg_TEST_LIST}
+  if(NOT "${testname}" MATCHES [=[(\[|\])]=])
+    # escape ;
+    string(REPLACE [[;]] [[\\;]] testname "${testname}")
+    list(APPEND tests_buffer "${testname}")
+    list(LENGTH tests_buffer tests_buffer_length)
+    if(tests_buffer_length GREATER "250")
+      # Chunk updates to the final "tests" variable, keeping the
+      # "tests_buffer" variable that we append each test to relatively
+      # small. This mitigates worsening performance impacts for the
+      # corner case of having many thousands of tests.
+      list(APPEND tests "${tests_buffer}")
+      set(tests_buffer "")
+    endif()
+  endif()
+
+  # If we've built up a sizable script so far, write it out as a chunk now
+  # so we don't accumulate a massive string to write at the end
+  string(LENGTH "${script}" script_len)
+  if(${script_len} GREATER "50000")
+    file(APPEND "${arg_CTEST_FILE}" "${script}")
+    set(script "")
+  endif()
+endmacro()
+
+macro(parse_tests_from_output)
+  generate_testname_guards("${output}" open_guard close_guard)
+  escape_square_brackets("${output}" "[" "__osb" open_sb output)
+  escape_square_brackets("${output}" "]" "__csb" close_sb output)
+
+  # Preserve semicolon in test-parameters
+  string(REPLACE [[;]] [[\;]] output "${output}")
+  string(REPLACE "\n" ";" output "${output}")
+
+  # Parse output
+  foreach(line ${output})
+    # Skip header
+    if(line MATCHES "gtest_main\\.cc")
+      continue()
+    endif()
+
+    if(line STREQUAL "")
+      continue()
+    endif()
+
+    # Do we have a module name or a test name?
+    if(NOT line MATCHES "^  ")
+      set(current_test_type_param "")
+
+      # Module; remove trailing '.' to get just the name...
+      string(REGEX REPLACE "\\.( *#.*)?$" "" current_test_suite "${line}")
+      if(line MATCHES "# *TypeParam = (.*)$")
+        set(current_test_type_param "${CMAKE_MATCH_1}")
+      endif()
+    else()
+      string(STRIP "${line}" test)
+      string(REGEX REPLACE " ( *#.*)?$" "" current_test_name "${test}")
+
+      set(current_test_value_param "")
+      if(line MATCHES "# *GetParam\\(\\) = (.*)$")
+        set(current_test_value_param "${CMAKE_MATCH_1}")
+      endif()
+
+      write_test_to_file()
+    endif()
+  endforeach()
+endmacro()
+
+macro(get_json_member_with_default json_variable member_name out_variable)
+  string(JSON ${out_variable}
+    ERROR_VARIABLE error_param
+    GET "${${json_variable}}" "${member_name}"
+  )
+  if(error_param)
+    # Member not present
+    set(${out_variable} "")
+  endif()
+endmacro()
+
+macro(parse_tests_from_json json_file)
+  if(NOT EXISTS "${json_file}")
+    message(FATAL_ERROR "Missing expected JSON file with test list: ${json_file}")
+  endif()
+
+  file(READ "${json_file}" test_json)
+  string(JSON test_suites_json GET "${test_json}" "testsuites")
+
+  # Return if there are no testsuites
+  string(JSON len_test_suites LENGTH "${test_suites_json}")
+  if(len_test_suites LESS_EQUAL 0)
+    return()
+  endif()
+
+  set(open_sb)
+  set(close_sb)
+
+  math(EXPR upper_limit_test_suite_range "${len_test_suites} - 1")
+
+  foreach(index_test_suite RANGE ${upper_limit_test_suite_range})
+    string(JSON test_suite_json GET "${test_suites_json}" ${index_test_suite})
+
+    # "suite" is expected to be set in write_test_to_file(). When parsing the
+    # plain text output, "suite" is expected to be the original suite name
+    # before accounting for pretty names. This may be used to construct the
+    # name of XML output results files.
+    string(JSON current_test_suite GET "${test_suite_json}" "name")
+    string(JSON tests_json GET "${test_suite_json}" "testsuite")
+
+    # Skip test suites without tests
+    string(JSON len_tests LENGTH "${tests_json}")
+    if(len_tests LESS_EQUAL 0)
+      continue()
+    endif()
+
+    math(EXPR upper_limit_test_range "${len_tests} - 1")
+    foreach(index_test RANGE ${upper_limit_test_range})
+      string(JSON test_json GET "${tests_json}" ${index_test})
+
+      string(JSON len_test_parameters LENGTH "${test_json}")
+      if(len_test_parameters LESS_EQUAL 0)
+        continue()
+      endif()
+
+      get_json_member_with_default(test_json "name" current_test_name)
+      get_json_member_with_default(test_json "value_param" current_test_value_param)
+      get_json_member_with_default(test_json "type_param" current_test_type_param)
+
+      generate_testname_guards(
+        "${current_test_suite}${current_test_name}${current_test_value_param}${current_test_type_param}"
+        open_guard close_guard
+      )
+      write_test_to_file()
+    endforeach()
+  endforeach()
+endmacro()
+
 function(gtest_discover_tests_impl)
 function(gtest_discover_tests_impl)
 
 
   set(options "")
   set(options "")
@@ -74,19 +308,15 @@ function(gtest_discover_tests_impl)
   set(prefix "${arg_TEST_PREFIX}")
   set(prefix "${arg_TEST_PREFIX}")
   set(suffix "${arg_TEST_SUFFIX}")
   set(suffix "${arg_TEST_SUFFIX}")
   set(script)
   set(script)
-  set(suite)
   set(tests)
   set(tests)
   set(tests_buffer "")
   set(tests_buffer "")
 
 
   # If a file at ${arg_CTEST_FILE} already exists, we overwrite it.
   # If a file at ${arg_CTEST_FILE} already exists, we overwrite it.
-  # For performance reasons, we write to this file in chunks, and this variable
-  # is updated to APPEND after the first write.
-  set(file_write_mode WRITE)
+  file(REMOVE "${arg_CTEST_FILE}")
 
 
+  set(filter)
   if(arg_TEST_FILTER)
   if(arg_TEST_FILTER)
     set(filter "--gtest_filter=${arg_TEST_FILTER}")
     set(filter "--gtest_filter=${arg_TEST_FILTER}")
-  else()
-    set(filter)
   endif()
   endif()
 
 
   # CMP0178 has already been handled in gtest_discover_tests(), so we only need
   # CMP0178 has already been handled in gtest_discover_tests(), so we only need
@@ -113,15 +343,25 @@ function(gtest_discover_tests_impl)
     set(discovery_extra_args "[==[${discovery_extra_args}]==]")
     set(discovery_extra_args "[==[${discovery_extra_args}]==]")
   endif()
   endif()
 
 
+  set(json_file "${arg_TEST_WORKING_DIR}/cmake_test_discovery.json")
+
+  # Remove json file to make sure we don't pick up an outdated one
+  file(REMOVE "${json_file}")
+
   cmake_language(EVAL CODE
   cmake_language(EVAL CODE
     "execute_process(
     "execute_process(
-      COMMAND ${launcherArgs} [==[${arg_TEST_EXECUTABLE}]==] --gtest_list_tests ${filter} ${discovery_extra_args}
+      COMMAND ${launcherArgs} [==[${arg_TEST_EXECUTABLE}]==]
+        --gtest_list_tests
+        [==[--gtest_output=json:${json_file}]==]
+        ${filter}
+        ${discovery_extra_args}
       WORKING_DIRECTORY [==[${arg_TEST_WORKING_DIR}]==]
       WORKING_DIRECTORY [==[${arg_TEST_WORKING_DIR}]==]
       TIMEOUT ${arg_TEST_DISCOVERY_TIMEOUT}
       TIMEOUT ${arg_TEST_DISCOVERY_TIMEOUT}
       OUTPUT_VARIABLE output
       OUTPUT_VARIABLE output
       RESULT_VARIABLE result
       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}")
     if(arg_TEST_EXECUTOR)
     if(arg_TEST_EXECUTOR)
@@ -139,124 +379,21 @@ function(gtest_discover_tests_impl)
     )
     )
   endif()
   endif()
 
 
-  generate_testname_guards("${output}" open_guard close_guard)
-  escape_square_brackets("${output}" "[" "__osb" open_sb output)
-  escape_square_brackets("${output}" "]" "__csb" close_sb output)
-  # Preserve semicolon in test-parameters
-  string(REPLACE [[;]] [[\;]] output "${output}")
-  string(REPLACE "\n" ";" output "${output}")
-
-  # Parse output
-  foreach(line ${output})
-    # Skip header
-    if(NOT line MATCHES "gtest_main\\.cc")
-      # Do we have a module name or a test name?
-      if(NOT line MATCHES "^  ")
-        # Module; remove trailing '.' to get just the name...
-        string(REGEX REPLACE "\\.( *#.*)?$" "" suite "${line}")
-        if(line MATCHES "#")
-          string(REGEX REPLACE "/[0-9].*" "" pretty_suite "${line}")
-          if(NOT arg_NO_PRETTY_TYPES)
-            string(REGEX REPLACE ".*/[0-9]+[ .#]+TypeParam = (.*)" "\\1" type_parameter "${line}")
-          else()
-            string(REGEX REPLACE ".*/([0-9]+)[ .#]+TypeParam = .*" "\\1" type_parameter "${line}")
-          endif()
-          set(test_name_template "@prefix@@pretty_suite@.@pretty_test@<@type_parameter@>@suffix@")
-        else()
-          set(pretty_suite "${suite}")
-          set(test_name_template "@prefix@@pretty_suite@.@pretty_test@@suffix@")
-        endif()
-        string(REGEX REPLACE "^DISABLED_" "" pretty_suite "${pretty_suite}")
-      else()
-        string(STRIP "${line}" test)
-        if(test MATCHES "#" AND NOT arg_NO_PRETTY_VALUES)
-          string(REGEX REPLACE "/[0-9]+[ #]+GetParam\\(\\) = " "/" pretty_test "${test}")
-        else()
-          string(REGEX REPLACE " +#.*" "" pretty_test "${test}")
-        endif()
-        string(REGEX REPLACE "^DISABLED_" "" pretty_test "${pretty_test}")
-        string(REGEX REPLACE " +#.*" "" test "${test}")
-        if(NOT "${arg_TEST_XML_OUTPUT_DIR}" STREQUAL "")
-          set(TEST_XML_OUTPUT_PARAM "--gtest_output=xml:${arg_TEST_XML_OUTPUT_DIR}/${prefix}${suite}.${test}${suffix}.xml")
-        else()
-          unset(TEST_XML_OUTPUT_PARAM)
-        endif()
-
-        string(CONFIGURE "${test_name_template}" testname)
-        # unescape []
-        if(open_sb)
-          string(REPLACE "${open_sb}" "[" testname "${testname}")
-        endif()
-        if(close_sb)
-          string(REPLACE "${close_sb}" "]" testname "${testname}")
-        endif()
-        set(guarded_testname "${open_guard}${testname}${close_guard}")
-
-        # 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}
-          )
-          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")
-
-        set(maybe_disabled "")
-        if(suite MATCHES "^DISABLED_" OR test MATCHES "^DISABLED_")
-          set(maybe_disabled DISABLED TRUE)
-        endif()
-
-        add_command(set_tests_properties
-          "${guarded_testname}"
-          PROPERTIES
-          ${maybe_disabled}
-          WORKING_DIRECTORY "${arg_TEST_WORKING_DIR}"
-          SKIP_REGULAR_EXPRESSION "\\[  SKIPPED \\]"
-          ${arg_TEST_PROPERTIES}
-        )
-
-        # possibly unbalanced square brackets render lists invalid so skip such
-        # tests in ${arg_TEST_LIST}
-        if(NOT "${testname}" MATCHES [=[(\[|\])]=])
-          # escape ;
-          string(REPLACE [[;]] [[\\;]] testname "${testname}")
-          list(APPEND tests_buffer "${testname}")
-          list(LENGTH tests_buffer tests_buffer_length)
-          if(tests_buffer_length GREATER "250")
-            # Chunk updates to the final "tests" variable, keeping the
-            # "tests_buffer" variable that we append each test to relatively
-            # small. This mitigates worsening performance impacts for the
-            # corner case of having many thousands of tests.
-            list(APPEND tests "${tests_buffer}")
-            set(tests_buffer "")
-          endif()
-        endif()
-      endif()
-
-      # If we've built up a sizable script so far, write it out as a chunk now
-      # so we don't accumulate a massive string to write at the end
-      string(LENGTH "${script}" script_len)
-      if(${script_len} GREATER "50000")
-        file(${file_write_mode} "${arg_CTEST_FILE}" "${script}")
-        set(file_write_mode APPEND)
-        set(script "")
-      endif()
+  if(EXISTS "${json_file}")
+    parse_tests_from_json("${json_file}")
+  else()
+    # gtest < 1.8.1, and all gtest compiled with GTEST_HAS_FILE_SYSTEM=0, don't
+    # recognize the --gtest_output=json option, and issue a warning or error on
+    # stdout about it being unrecognized, but still return an exit code 0 for
+    # success. All versions report the test list on stdout whether
+    # --gtest_output=json is recognized or not.
 
 
-    endif()
-  endforeach()
+    # NOTE: Because we are calling a macro, we don't want to pass "output" as
+    # an argument because it messes up the contents passed through due to the
+    # different escaping, etc. that gets applied. We rely on it picking up the
+    # "output" variable we have already set here.
+    parse_tests_from_output()
+  endif()
 
 
   if(NOT tests_buffer STREQUAL "")
   if(NOT tests_buffer STREQUAL "")
     list(APPEND tests "${tests_buffer}")
     list(APPEND tests "${tests_buffer}")
@@ -267,8 +404,7 @@ function(gtest_discover_tests_impl)
   add_command(set "" ${arg_TEST_LIST} "${tests}")
   add_command(set "" ${arg_TEST_LIST} "${tests}")
 
 
   # Write remaining content to the CTest script
   # Write remaining content to the CTest script
-  file(${file_write_mode} "${arg_CTEST_FILE}" "${script}")
-
+  file(APPEND "${arg_CTEST_FILE}" "${script}")
 endfunction()
 endfunction()
 
 
 if(CMAKE_SCRIPT_MODE_FILE)
 if(CMAKE_SCRIPT_MODE_FILE)

+ 16 - 0
Tests/RunCMake/GoogleTest/GoogleTestLegacyParser.cmake

@@ -0,0 +1,16 @@
+enable_language(CXX)
+include(GoogleTest)
+
+enable_testing()
+
+include(xcode_sign_adhoc.cmake)
+
+add_executable(fake_gtest fake_gtest.cpp)
+xcode_sign_adhoc(fake_gtest)
+
+gtest_discover_tests(
+  fake_gtest
+  TEST_PREFIX TEST:
+  TEST_SUFFIX !1
+  EXTRA_ARGS how now "\"brown\" cow"
+)

+ 33 - 0
Tests/RunCMake/GoogleTest/RunCMakeTest.cmake

@@ -402,6 +402,37 @@ function(run_GoogleTest_discovery_test_list_extra_args DISCOVERY_MODE)
   )
   )
 endfunction()
 endfunction()
 
 
+function(run_GoogleTest_LegacyParser)
+  # Use a single build tree for a few tests without cleaning.
+  set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/GoogleTestLegacyParser-build)
+  set(RunCMake_TEST_NO_CLEAN 1)
+  if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
+    set(RunCMake_TEST_OPTIONS -DCMAKE_BUILD_TYPE=Debug)
+  endif()
+  file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}")
+  file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}")
+
+  set(ENV{NO_GTEST_JSON_OUTPUT} 1)
+
+  run_cmake(GoogleTestLegacyParser)
+
+  run_cmake_command(GoogleTestLegacyParser-build
+    ${CMAKE_COMMAND}
+    --build .
+    --config Debug
+    --target fake_gtest
+  )
+
+  set(RunCMake-stdout-file GoogleTest-test1-stdout.txt)
+  run_cmake_command(GoogleTestLegacyParser-test
+    ${CMAKE_CTEST_COMMAND}
+    -C Debug
+    --no-label-summary
+  )
+
+  unset(ENV{NO_GTEST_JSON_OUTPUT})
+endfunction()
+
 foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
 foreach(DISCOVERY_MODE POST_BUILD PRE_TEST)
   message(STATUS "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})
@@ -449,3 +480,5 @@ block(SCOPE_FOR VARIABLES)
     --output-on-failure
     --output-on-failure
   )
   )
 endblock()
 endblock()
+
+run_GoogleTest_LegacyParser()

+ 16 - 0
Tests/RunCMake/GoogleTest/WorkDirWithSpaces.cmake

@@ -14,6 +14,22 @@ WorkDirWithSpaces.
   test1
   test1
   test2
   test2
 ]=])
 ]=])
+file(WRITE ${workdir}/test_list_output.json [=[
+{
+    "tests": 2,
+    "name": "AllTests",
+    "testsuites": [
+        {
+            "name": "WorkDirWithSpaces",
+            "tests": 2,
+            "testsuite": [
+                { "name": "test1", "file": "file.cpp", "line": 42 },
+                { "name": "test2", "file": "file.cpp", "line": 43 }
+            ]
+        }
+    ]
+}
+]=])
 file(WRITE ${workdir}/test_output.txt [=[
 file(WRITE ${workdir}/test_output.txt [=[
 Some output text for the test.
 Some output text for the test.
 ]=])
 ]=])

+ 37 - 1
Tests/RunCMake/GoogleTest/configuration_gtest.cpp

@@ -1,3 +1,5 @@
+#include <cstdlib>
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
@@ -7,15 +9,49 @@ int main(int argc, char** argv)
   // 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.
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
     std::cout << "configuration." << std::endl;
     std::cout << "configuration." << std::endl;
 #ifdef DEBUG
 #ifdef DEBUG
     std::cout << "  DISABLED_case_release" << std::endl;
     std::cout << "  DISABLED_case_release" << std::endl;
     std::cout << "  case_debug" << std::endl;
     std::cout << "  case_debug" << std::endl;
+    std::string release_name("DISABLED_case_release");
+    std::string debug_name("case_debug");
 #else
 #else
     std::cout << "  case_release" << std::endl;
     std::cout << "  case_release" << std::endl;
     std::cout << "  DISABLED_case_debug" << std::endl;
     std::cout << "  DISABLED_case_debug" << std::endl;
+    std::string release_name("case_release");
+    std::string debug_name("DISABLED_case_debug");
 #endif
 #endif
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+
+    ostrm << "{\n"
+             "    \"tests\": 2,\n"
+             "    \"name\": \"AllTests\",\n"
+             "    \"testsuites\": [\n"
+             "        {\n"
+             "            \"name\": \"configuration\",\n"
+             "            \"tests\": 2,\n"
+             "            \"testsuite\": [\n"
+             "                { \"name\": \""
+          << release_name
+          << "\", \"file\": \"file.cpp\", \"line\": 42 },\n"
+             "                { \"name\": \""
+          << debug_name
+          << "\", \"file\": \"file.cpp\", \"line\": 43 }\n"
+             "            ]\n"
+             "        }\n"
+             "    ]\n"
+             "}";
     return 0;
     return 0;
   }
   }
 
 

+ 196 - 8
Tests/RunCMake/GoogleTest/fake_gtest.cpp

@@ -1,6 +1,16 @@
+#define _CRT_SECURE_NO_WARNINGS // work around 'getenv' deprecation on WIN32
+
+#include <cstdlib>
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
+#if defined(_MSC_VER) && _MSC_VER < 1310
+#  define GETENV ::getenv
+#else
+#  define GETENV std::getenv
+#endif
+
 #define ARRAY_SIZE(a) sizeof(a) / sizeof(*a)
 #define ARRAY_SIZE(a) sizeof(a) / sizeof(*a)
 
 
 int main(int argc, char** argv)
 int main(int argc, char** argv)
@@ -10,25 +20,70 @@ int main(int argc, char** argv)
   // 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.
   bool is_filtered =
   bool is_filtered =
-    argc > 2 && std::string(argv[2]).find("--gtest_filter=") == 0;
+    argc > 3 && std::string(argv[3]).find("--gtest_filter=") == 0;
   bool add_basic_tests =
   bool add_basic_tests =
-    !is_filtered || (std::string(argv[2]).find("basic*") != std::string::npos);
+    !is_filtered || (std::string(argv[3]).find("basic*") != std::string::npos);
   bool add_typed_tests =
   bool add_typed_tests =
-    !is_filtered || (std::string(argv[2]).find("typed*") != std::string::npos);
+    !is_filtered || (std::string(argv[3]).find("typed*") != std::string::npos);
   bool add_value_tests =
   bool add_value_tests =
-    !is_filtered || (std::string(argv[2]).find("value*") != std::string::npos);
+    !is_filtered || (std::string(argv[3]).find("value*") != std::string::npos);
   bool add_dynamic_tests = !is_filtered ||
   bool add_dynamic_tests = !is_filtered ||
-    (std::string(argv[2]).find("dynamic*") != std::string::npos);
+    (std::string(argv[3]).find("dynamic*") != std::string::npos);
+
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
+    std::ofstream ostrm;
+    if (!GETENV("NO_GTEST_JSON_OUTPUT")) {
+      std::string output_param(argv[2]);
+      std::string::size_type split = output_param.find(":");
+      std::string filepath = output_param.substr(split + 1);
+      // The full file path is passed
+      ostrm.open(filepath.c_str(), std::ios::out | std::ios::binary);
+      if (!ostrm) {
+        std::cerr << "Failed to create file: " << filepath.c_str()
+                  << std::endl;
+        return 1;
+      }
+    } else {
+      std::cout << "WARNING: An old-style warning." << std::endl;
+      std::cout << "[ WARNING ] A new-style warning." << std::endl;
+      std::cout << "[  ERROR  ] A new-style error." << std::endl;
+    }
+
+    int tests = 0;
+    ostrm << "{\n"
+             "    \"name\": \"AllTests\",\n"
+             "    \"testsuites\": [";
 
 
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
     if (add_basic_tests) {
     if (add_basic_tests) {
-      char const* basic_suite_names[] = { "basic.", "ns.basic." };
+      char const* basic_suite_names[] = { "basic", "ns.basic" };
       for (size_t i = 0; i < ARRAY_SIZE(basic_suite_names); i++) {
       for (size_t i = 0; i < ARRAY_SIZE(basic_suite_names); i++) {
-        std::cout << basic_suite_names[i] << std::endl;
+        std::cout << basic_suite_names[i] << '.' << std::endl;
         std::cout << "  case_foo" << std::endl;
         std::cout << "  case_foo" << std::endl;
         std::cout << "  case_bar" << std::endl;
         std::cout << "  case_bar" << std::endl;
         std::cout << "  DISABLED_disabled_case" << std::endl;
         std::cout << "  DISABLED_disabled_case" << std::endl;
         std::cout << "  DISABLEDnot_really_case" << std::endl;
         std::cout << "  DISABLEDnot_really_case" << std::endl;
+
+        if (tests)
+          ostrm << ",";
+        ostrm << "\n"
+                 "        {\n"
+                 "            \"name\": \""
+              << basic_suite_names[i]
+              << "\",\n"
+                 "            \"tests\": 4,\n"
+                 "            \"testsuite\": [\n"
+                 "                { \"name\": \"case_foo\", \"file\": "
+                 "\"file1.cpp\", \"line\": 1 },\n"
+                 "                { \"name\": \"case_bar\", \"file\": "
+                 "\"file with spaces.cpp\", \"line\": 2 },\n"
+                 "                { \"name\": \"DISABLED_disabled_case\", "
+                 "\"file\": \"file1.cpp\", \"line\": 3 },\n"
+                 "                { \"name\": \"DISABLEDnot_really_case\", "
+                 "\"file\": \"file1.cpp\", \"line\": 4 }\n"
+                 "            ]\n"
+                 "        }";
+        tests += 4;
       }
       }
     }
     }
     if (!is_filtered) {
     if (!is_filtered) {
@@ -36,6 +91,27 @@ int main(int argc, char** argv)
       std::cout << "  case" << std::endl;
       std::cout << "  case" << std::endl;
       std::cout << "DISABLEDnotreally." << std::endl;
       std::cout << "DISABLEDnotreally." << std::endl;
       std::cout << "  case" << std::endl;
       std::cout << "  case" << std::endl;
+
+      if (tests)
+        ostrm << ",";
+      ostrm << "\n"
+               "        {\n"
+               "            \"name\": \"DISABLED_disabled\",\n"
+               "            \"tests\": 1,\n"
+               "            \"testsuite\": [\n"
+               "                { \"name\": \"case\", \"file\": "
+               "\"file2.cpp\", \"line\": 1 }\n"
+               "            ]\n"
+               "        },\n"
+               "        {\n"
+               "            \"name\": \"DISABLEDnotreally\",\n"
+               "            \"tests\": 1,\n"
+               "            \"testsuite\": [\n"
+               "                { \"name\": \"case\", \"file\": "
+               "\"file2.cpp\", \"line\": 2 }\n"
+               "            ]\n"
+               "        }";
+      tests += 2;
     }
     }
     if (add_typed_tests) {
     if (add_typed_tests) {
       char const* typed_suite_names[] = { "typed", "ns.typed",
       char const* typed_suite_names[] = { "typed", "ns.typed",
@@ -49,6 +125,43 @@ int main(int argc, char** argv)
         std::cout << "  case" << std::endl;
         std::cout << "  case" << std::endl;
         std::cout << typed_suite_names[i] << "/named.  # TypeParam = int\n";
         std::cout << typed_suite_names[i] << "/named.  # TypeParam = int\n";
         std::cout << "  case" << std::endl;
         std::cout << "  case" << std::endl;
+
+        if (tests)
+          ostrm << ",";
+        ostrm << "\n"
+                 "        {\n"
+                 "            \"name\": \""
+              << typed_suite_names[i]
+              << "/0\",\n"
+                 "            \"tests\": 1, \"testsuite\": [ { \"name\": "
+                 "\"case\", \"type_param\": \"short\", \"file\": "
+                 "\"file3.cpp\", \"line\": 1 } ]\n"
+                 "        },\n"
+                 "        {\n"
+                 "            \"name\": \""
+              << typed_suite_names[i]
+              << "/1\",\n"
+                 "            \"tests\": 1, \"testsuite\": [ { \"name\": "
+                 "\"case\", \"type_param\": \"float\", \"file\": "
+                 "\"file3.cpp\", \"line\": 2 } ]\n"
+                 "        },\n"
+                 "        {\n"
+                 "            \"name\": \""
+              << typed_suite_names[i]
+              << "/42\",\n"
+                 "            \"tests\": 1, \"testsuite\": [ { \"name\": "
+                 "\"case\", \"type_param\": \"char\", \"file\": "
+                 "\"file3.cpp\", \"line\": 3 } ]\n"
+                 "        },\n"
+                 "        {\n"
+                 "            \"name\": \""
+              << typed_suite_names[i]
+              << "/named\",\n"
+                 "            \"tests\": 1, \"testsuite\": [ { \"name\": "
+                 "\"case\", \"type_param\": \"int\", \"file\": \"file3.cpp\", "
+                 "\"line\": 4 } ]\n"
+                 "        }";
+        tests += 4;
       }
       }
     }
     }
     if (add_value_tests) {
     if (add_value_tests) {
@@ -59,6 +172,26 @@ int main(int argc, char** argv)
         std::cout << "  case/0  # GetParam() = 1" << std::endl;
         std::cout << "  case/0  # GetParam() = 1" << std::endl;
         std::cout << "  case/1  # GetParam() = \"foo\"" << std::endl;
         std::cout << "  case/1  # GetParam() = \"foo\"" << std::endl;
         std::cout << "  case/named  # GetParam() = 'c'" << std::endl;
         std::cout << "  case/named  # GetParam() = 'c'" << std::endl;
+
+        if (tests)
+          ostrm << ",";
+        ostrm
+          << "\n"
+             "        {\n"
+             "            \"name\": \""
+          << value_suite_names[i] << "/test"
+          << "\",\n"
+             "            \"tests\": 3,\n"
+             "            \"testsuite\": [\n"
+             "                { \"name\": \"case/0\", \"value_param\": \"1\", "
+             "\"file\": \"file4.cpp\", \"line\": 1 },\n"
+             "                { \"name\": \"case/1\", \"value_param\": "
+             "\"\\\"foo\\\"\", \"file\": \"file4.cpp\", \"line\": 2 },\n"
+             "                { \"name\": \"case/named\", \"value_param\": "
+             "\"'c'\", \"file\": \"file4.cpp\", \"line\": 3 }\n"
+             "            ]\n"
+             "        }";
+        tests += 3;
       }
       }
       char const* param_suite_names[] = { "param", "ns.param",
       char const* param_suite_names[] = { "param", "ns.param",
                                           "prefix/param" };
                                           "prefix/param" };
@@ -72,6 +205,38 @@ int main(int argc, char** argv)
         std::cout << "  case/5  # GetParam() = \"__osbtext\"" << std::endl;
         std::cout << "  case/5  # GetParam() = \"__osbtext\"" << std::endl;
         std::cout << "  case/6  # GetParam() = \"__csb___text\"" << std::endl;
         std::cout << "  case/6  # GetParam() = \"__csb___text\"" << std::endl;
         std::cout << "  case/7  # GetParam() = \"S o m  e   \"" << std::endl;
         std::cout << "  case/7  # GetParam() = \"S o m  e   \"" << std::endl;
+
+        ostrm
+          << ",\n"
+             "        {\n"
+             "            \"name\": \""
+          << param_suite_names[j] << "/special"
+          << "\",\n"
+             "            \"tests\": 8,\n"
+             "            \"testsuite\": [\n"
+             "                { \"name\": \"case/0\", \"value_param\": "
+             "\"\\\"semicolon;\\\"\", \"file\": \"file4.cpp\", \"line\": 1 "
+             "},\n"
+             "                { \"name\": \"case/1\", \"value_param\": "
+             "\"\\\"backslash\\\\\\\"\", \"file\": \"file4.cpp\", \"line\": 2 "
+             "},\n"
+             "                { \"name\": \"case/2\", \"value_param\": "
+             "\"\\\"${var}\\\"\", \"file\": \"file4.cpp\", \"line\": 3 },\n"
+             "                { \"name\": \"case/3\", \"value_param\": "
+             "\"'['\", \"file\": \"file4.cpp\", \"line\": 4 },\n"
+             "                { \"name\": \"case/4\", \"value_param\": "
+             "\"\\\"]]=]\\\"\", \"file\": \"file4.cpp\", \"line\": 5 },\n"
+             "                { \"name\": \"case/5\", \"value_param\": "
+             "\"\\\"__osbtext\\\"\", \"file\": \"file4.cpp\", \"line\": 5 },\n"
+             "                { \"name\": \"case/6\", \"value_param\": "
+             "\"\\\"__csb___text\\\"\", \"file\": \"file4.cpp\", \"line\": 5 "
+             "},\n"
+             "                { \"name\": \"case/7\", \"value_param\": "
+             "\"\\\"S o m  e   \\\"\", \"file\": \"file4.cpp\", \"line\": 6 "
+             "}\n"
+             "            ]\n"
+             "        }";
+        tests += 8;
       }
       }
     }
     }
     if (add_value_tests || add_typed_tests || add_dynamic_tests) {
     if (add_value_tests || add_typed_tests || add_dynamic_tests) {
@@ -83,8 +248,31 @@ int main(int argc, char** argv)
                   << std::endl;
                   << std::endl;
         std::cout << "  test  # GetParam() = VALUE" << std::endl;
         std::cout << "  test  # GetParam() = VALUE" << std::endl;
         std::cout << "  case/test  # GetParam() = VALUE" << std::endl;
         std::cout << "  case/test  # GetParam() = VALUE" << std::endl;
+
+        if (tests)
+          ostrm << ",";
+        ostrm << "\n"
+                 "          {\n"
+                 "              \"name\": \""
+              << both_suite_names[k]
+              << "\",\n"
+                 "              \"tests\": 1,\n"
+                 "              \"testsuite\": [\n"
+                 "                  { \"name\": \"test\", \"type_param\": "
+                 "\"TYPE\", \"value_param\": \"VALUE\", \"file\": "
+                 "\"file5.cpp\", \"line\": 1 },\n"
+                 "                  { \"name\": \"case/test\", "
+                 "\"type_param\": \"TYPE\", \"value_param\": \"VALUE\", "
+                 "\"file\": \"file5.cpp\", \"line\": 2 }\n"
+                 "              ]\n"
+                 "          }";
+        tests += 2;
       }
       }
     }
     }
+    ostrm << "\n"
+             "    ],\n"
+             "    \"tests\": "
+          << tests << "\n}";
     return 0;
     return 0;
   }
   }
 
 

+ 41 - 2
Tests/RunCMake/GoogleTest/flush_script_test.cpp

@@ -1,3 +1,4 @@
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
@@ -7,14 +8,52 @@ int main(int argc, char** argv)
   // 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.
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
     std::cout << "flush_script_test.\n";
     std::cout << "flush_script_test.\n";
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+
     size_t const flushThreshold = 50000;
     size_t const flushThreshold = 50000;
     size_t const flushAfter = 4;
     size_t const flushAfter = 4;
     size_t const testCaseNum = 3 * flushAfter;
     size_t const testCaseNum = 3 * flushAfter;
+    ostrm << "{\n"
+             "    \"tests\": 4,\n"
+             "    \"name\": \"AllTests\",\n"
+             "    \"testsuites\": [\n"
+             "        {\n"
+             "            \"name\": \"flush_script_test\",\n"
+             "            \"tests\": 12,\n"
+             "            \"testsuite\": [";
+
     std::string testName(flushThreshold / flushAfter, 'T');
     std::string testName(flushThreshold / flushAfter, 'T');
-    for (size_t i = 1; i <= testCaseNum; ++i)
+    for (size_t i = 1; i <= testCaseNum; ++i) {
       std::cout << "  t" << i << testName.c_str() << "\n";
       std::cout << "  t" << i << testName.c_str() << "\n";
+      if (i != 1)
+        ostrm << ",";
+      ostrm << "\n"
+               "                {\n"
+               "                  \"name\": \"t"
+            << i << testName.c_str()
+            << "\",\n"
+               "                  \"file\": \"file2.cpp\",\n"
+               "                  \"line\": 47\n"
+               "                }";
+    }
+    ostrm << "\n"
+             "            ]\n"
+             "        }\n"
+             "    ]\n"
+             "}";
   }
   }
+
   return 0;
   return 0;
 }
 }

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

@@ -8,15 +8,46 @@ TEST_F( launcher_test, test1 )
 }
 }
 */
 */
 
 
+char const gtest_output_json_flag_prefix[] = "--gtest_output=json:";
+char const json_output[] = "{\n"
+                           "    \"tests\": 1,\n"
+                           "    \"name\": \"AllTests\",\n"
+                           "    \"testsuites\": [\n"
+                           "        {\n"
+                           "            \"name\": \"launcher_test\",\n"
+                           "            \"tests\": 1,\n"
+                           "            \"testsuite\": [\n"
+                           "                { \"name\": \"test1\", \"file\": "
+                           "\"file.cpp\", \"line\": 42 }\n"
+                           "            ]\n"
+                           "        }\n"
+                           "    ]\n"
+                           "}";
+
 int main(int argc, char** argv)
 int main(int argc, char** argv)
 {
 {
   /* Note: Launcher.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.  */
-  if (argc > 1 && strcmp(argv[1], "--gtest_list_tests") == 0) {
+  char* filepath;
+  FILE* ofile;
+
+  if (argc > 2 && strcmp(argv[1], "--gtest_list_tests") == 0 &&
+      strncmp(argv[2], gtest_output_json_flag_prefix,
+              strlen(gtest_output_json_flag_prefix)) == 0) {
     printf("launcher_test.\n");
     printf("launcher_test.\n");
     printf("  test1\n");
     printf("  test1\n");
+    filepath = strchr(argv[2], ':') + 1;
+    /* The full file path is passed */
+    ofile = fopen(filepath, "wb");
+
+    if (!ofile) {
+      fprintf(stderr, "Failed to create file: %s\n", filepath);
+      return 1;
+    }
+    fprintf(ofile, "%s", json_output);
+    fclose(ofile);
   }
   }
 
 
   printf("launcher_test.test1\n");
   printf("launcher_test.test1\n");

+ 28 - 1
Tests/RunCMake/GoogleTest/skip_test.cpp

@@ -1,3 +1,4 @@
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
@@ -14,9 +15,35 @@ int main(int argc, char** argv)
   // 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.
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
     std::cout << "skip_test." << std::endl;
     std::cout << "skip_test." << std::endl;
     std::cout << "  test1" << std::endl;
     std::cout << "  test1" << std::endl;
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+    ostrm << "{\n"
+             "    \"tests\": 1,\n"
+             "    \"name\": \"AllTests\",\n"
+             "    \"testsuites\": [\n"
+             "        {\n"
+             "            \"name\": \"skip_test\",\n"
+             "            \"tests\": 1,\n"
+             "            \"testsuite\": [\n"
+             "                { \"name\": \"test1\", \"file\": \"file.cpp\", "
+             "\"line\": 42 }\n"
+             "            ]\n"
+             "        }\n"
+             "    ]\n"
+             "}";
+
     return 0;
     return 0;
   }
   }
 
 

+ 35 - 3
Tests/RunCMake/GoogleTest/test_list_extra_args.cpp

@@ -1,3 +1,4 @@
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
@@ -9,14 +10,45 @@ int main(int argc, char** argv)
   // to test the module without actually needing Google Test.
   // to test the module without actually needing Google Test.
 
 
   // Simple test of DISCOVERY_EXTRA_ARGS
   // Simple test of DISCOVERY_EXTRA_ARGS
-  if (argc > 4 && std::string(argv[1]) == "--gtest_list_tests" &&
-      std::string(argv[2]) == "how now" && std::string(argv[3]) == "" &&
-      std::string(argv[4]) == "\"brown\" cow") {
+  if (argc > 5 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos &&
+      std::string(argv[3]) == "how now" && std::string(argv[4]) == "" &&
+      std::string(argv[5]) == "\"brown\" cow") {
     std::cout << "test_list_test/test.\n";
     std::cout << "test_list_test/test.\n";
     std::cout << "  case/0  # GetParam() = 'one'\n";
     std::cout << "  case/0  # GetParam() = 'one'\n";
     std::cout << "  case/1  # GetParam() = 'two'\n";
     std::cout << "  case/1  # GetParam() = 'two'\n";
     std::cout << "  case/2  # GetParam() = 'three'\n";
     std::cout << "  case/2  # GetParam() = 'three'\n";
     std::cout << "  case/3  # GetParam() = 'four'\n";
     std::cout << "  case/3  # GetParam() = 'four'\n";
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+    ostrm << "{\n"
+             "    \"tests\": 4,\n"
+             "    \"name\": \"AllTests\",\n"
+             "    \"testsuites\": [\n"
+             "        {\n"
+             "            \"name\": \"test_list_test/test\",\n"
+             "            \"tests\": 4,\n"
+             "            \"testsuite\": [\n"
+             "                { \"name\": \"case/0\", \"file\": \"file.cpp\", "
+             "\"line\": 42 },\n"
+             "                { \"name\": \"case/1\", \"file\": \"file.cpp\", "
+             "\"line\": 42 },\n"
+             "                { \"name\": \"case/2\", \"file\": \"file.cpp\", "
+             "\"line\": 42 },\n"
+             "                { \"name\": \"case/3\", \"file\": \"file.cpp\", "
+             "\"line\": 42 }\n"
+             "            ]\n"
+             "        }\n"
+             "    ]\n"
+             "}";
   }
   }
   return 0;
   return 0;
 }
 }

+ 34 - 1
Tests/RunCMake/GoogleTest/test_list_test.cpp

@@ -1,3 +1,4 @@
+#include <fstream>
 #include <iostream>
 #include <iostream>
 #include <string>
 #include <string>
 
 
@@ -7,12 +8,44 @@ int main(int argc, char** argv)
   // 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.
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
     std::cout << "test_list_test/test.\n";
     std::cout << "test_list_test/test.\n";
     std::cout << "  case/0  # GetParam() = \"semicolon;\"\n";
     std::cout << "  case/0  # GetParam() = \"semicolon;\"\n";
     std::cout << "  case/1  # GetParam() = 'osb['\n";
     std::cout << "  case/1  # GetParam() = 'osb['\n";
     std::cout << "  case/2  # GetParam() = 'csb]'\n";
     std::cout << "  case/2  # GetParam() = 'csb]'\n";
     std::cout << "  case/3  # GetParam() = 'S p a c e s'\n";
     std::cout << "  case/3  # GetParam() = 'S p a c e s'\n";
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+    ostrm
+      << "{\n"
+         "    \"tests\": 4,\n"
+         "    \"name\": \"AllTests\",\n"
+         "    \"testsuites\": [\n"
+         "        {\n"
+         "            \"name\": \"test_list_test/test\",\n"
+         "            \"tests\": 4,\n"
+         "            \"testsuite\": [\n"
+         "                { \"name\": \"case/0\", \"value_param\": "
+         "\"\\\"semicolon;\\\"\", \"file\": \"file.cpp\", \"line\": 42 },\n"
+         "                { \"name\": \"case/1\", \"value_param\": "
+         "\"'osb['\", \"file\": \"file.cpp\", \"line\": 42 },\n"
+         "                { \"name\": \"case/2\", \"value_param\": "
+         "\"'csb]'\", \"file\": \"file.cpp\", \"line\": 42 },\n"
+         "                { \"name\": \"case/3\", \"value_param\": \"'S p a c "
+         "e s'\", \"file\": \"file.cpp\", \"line\": 42 }\n"
+         "            ]\n"
+         "        }\n"
+         "    ]\n"
+         "}";
   }
   }
   return 0;
   return 0;
 }
 }

+ 21 - 4
Tests/RunCMake/GoogleTest/test_workdir.cpp

@@ -19,13 +19,30 @@ int main(int argc, char** argv)
   // the current directory. If we are not handling spaces in the
   // the current directory. If we are not handling spaces in the
   // working directory correctly, the files we expect won't exist.
   // working directory correctly, the files we expect won't exist.
 
 
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
-    std::ifstream inFile("test_list_output.txt");
-    if (!inFile) {
+  if (argc > 2 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
+    std::ifstream inTestListFile("test_list_output.txt");
+    if (!inTestListFile) {
       std::cout << "ERROR: Failed to open test_list_output.txt" << std::endl;
       std::cout << "ERROR: Failed to open test_list_output.txt" << std::endl;
       return 1;
       return 1;
     }
     }
-    std::cout << inFile.rdbuf();
+    std::cout << inTestListFile.rdbuf();
+
+    std::ifstream inJsonFile("test_list_output.json");
+    if (!inJsonFile) {
+      std::cout << "ERROR: Failed to open test_list_output.json" << std::endl;
+      return 1;
+    }
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+    if (!ostrm) {
+      std::cerr << "Failed to create file: " << filepath.c_str() << std::endl;
+      return 1;
+    }
+    ostrm << inJsonFile.rdbuf();
     return 0;
     return 0;
   }
   }
 
 

+ 32 - 1
Tests/RunCMake/GoogleTest/timeout_test.cpp

@@ -1,3 +1,5 @@
+#define _CRT_SECURE_NO_WARNINGS // work around 'fopen' deprecation on WIN32
+
 #if defined(_WIN32)
 #if defined(_WIN32)
 #  include <windows.h>
 #  include <windows.h>
 #else
 #else
@@ -23,9 +25,38 @@ int main(int argc, char** argv)
   // it only requires that we produce output in the expected format when
   // it only requires that we produce 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.
-  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests") {
+  if (argc > 1 && std::string(argv[1]) == "--gtest_list_tests" &&
+      std::string(argv[2]).find("--gtest_output=json:") != std::string::npos) {
     printf("timeout.\n  case\n");
     printf("timeout.\n  case\n");
     fflush(stdout);
     fflush(stdout);
+
+    std::string output_param(argv[2]);
+    std::string::size_type split = output_param.find(":");
+    std::string filepath = output_param.substr(split + 1);
+    // The full file path is passed
+    FILE* ofile = fopen(filepath.c_str(), "wb");
+
+    if (!ofile) {
+      fprintf(stderr, "Failed to create file: %s\n", filepath.c_str());
+      return 1;
+    }
+    std::string json_output = "{\n"
+                              "      \"tests\": 1,\n"
+                              "      \"name\": \"AllTests\",\n"
+                              "      \"testsuites\": [\n"
+                              "          {\n"
+                              "              \"name\": \"timeout\",\n"
+                              "              \"tests\": 1,\n"
+                              "              \"testsuite\": [\n"
+                              "                  { \"name\": \"case\", "
+                              "\"file\": \"file.cpp\", \"line\": 42 }\n"
+                              "              ]\n"
+                              "          }\n"
+                              "      ]\n"
+                              "  }";
+    fprintf(ofile, "%s", json_output.c_str());
+    fclose(ofile);
+
 #ifdef discoverySleepSec
 #ifdef discoverySleepSec
     sleepFor(discoverySleepSec);
     sleepFor(discoverySleepSec);
 #endif
 #endif

+ 37 - 0
Tests/RunCMake/GoogleTest/xml_output.cpp

@@ -19,6 +19,43 @@ int main(int argc, char** argv)
       std::cout << "  case/0  # GetParam() = 42" << std::endl;
       std::cout << "  case/0  # GetParam() = 42" << std::endl;
       std::cout << "  case/1  # GetParam() = \"string\"" << std::endl;
       std::cout << "  case/1  # GetParam() = \"string\"" << std::endl;
       std::cout << "  case/2  # GetParam() = \"path/like\"" << std::endl;
       std::cout << "  case/2  # GetParam() = \"path/like\"" << std::endl;
+    } else if (param.find("--gtest_output=json:") != std::string::npos) {
+      std::string::size_type split = param.find(":");
+      std::string filepath = param.substr(split + 1);
+      // The full file path is passed
+      std::ofstream ostrm(filepath.c_str(), std::ios::binary);
+      if (!ostrm) {
+        std::cerr << "Failed to create file: " << filepath.c_str()
+                  << std::endl;
+        return 1;
+      }
+      ostrm
+        << "{\n"
+           "    \"tests\": 4,\n"
+           "    \"name\": \"AllTests\",\n"
+           "    \"testsuites\": [\n"
+           "        {\n"
+           "            \"name\": \"GoogleTestXML\",\n"
+           "            \"tests\": 1,\n"
+           "            \"testsuite\": [\n"
+           "                { \"name\": \"Foo\", \"file\": \"file.cpp\", "
+           "\"line\": 42 }\n"
+           "            ]\n"
+           "        },\n"
+           "        {\n"
+           "            \"name\": \"GoogleTestXMLSpecial\\/cases\",\n"
+           "            \"tests\": 3,\n"
+           "            \"testsuite\": [\n"
+           "                { \"name\": \"case\\/0\", \"value_param\": "
+           "\"42\", \"file\": \"file2.cpp\", \"line\": 47 },\n"
+           "                { \"name\": \"case\\/1\", \"value_param\": "
+           "\"\\\"string\\\"\", \"file\": \"file2.cpp\", \"line\": 47 },\n"
+           "                { \"name\": \"case\\/2\", \"value_param\": "
+           "\"\\\"path/like\\\"\", \"file\": \"file2.cpp\", \"line\": 47 }\n"
+           "            ]\n"
+           "        }\n"
+           "    ]\n"
+           "}";
     } else if (param.find("--gtest_output=xml:") != std::string::npos) {
     } else if (param.find("--gtest_output=xml:") != std::string::npos) {
       std::string::size_type split = param.find(":");
       std::string::size_type split = param.find(":");
       std::string filepath = param.substr(split + 1);
       std::string filepath = param.substr(split + 1);