1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
|
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file LICENSE.rst or https://cmake.org/licensing for details.
cmake_minimum_required(VERSION 3.30)
cmake_policy(SET CMP0174 NEW) # TODO: Remove this when we can update the above to 3.31
function(add_command name test_name)
set(args "")
foreach(arg ${ARGN})
if(arg MATCHES "[^-./:a-zA-Z0-9_]")
string(APPEND args " [==[${arg}]==]")
else()
string(APPEND args " ${arg}")
endif()
endforeach()
string(APPEND script "${name}(${test_name} ${args})\n")
set(script "${script}" PARENT_SCOPE)
endfunction()
function(generate_testname_guards output open_guard_var close_guard_var)
set(open_guard "[=[")
set(close_guard "]=]")
set(counter 1)
while("${output}" MATCHES "${close_guard}")
math(EXPR counter "${counter} + 1")
string(REPEAT "=" ${counter} equals)
set(open_guard "[${equals}[")
set(close_guard "]${equals}]")
endwhile()
set(${open_guard_var} "${open_guard}" PARENT_SCOPE)
set(${close_guard_var} "${close_guard}" PARENT_SCOPE)
endfunction()
function(escape_square_brackets output bracket placeholder placeholder_var output_var)
if("${output}" MATCHES "\\${bracket}")
set(placeholder "${placeholder}")
while("${output}" MATCHES "${placeholder}")
set(placeholder "${placeholder}_")
endwhile()
string(REPLACE "${bracket}" "${placeholder}" output "${output}")
set(${placeholder_var} "${placeholder}" PARENT_SCOPE)
set(${output_var} "${output}" PARENT_SCOPE)
endif()
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()
if (NOT current_test_value_param STREQUAL "" AND NOT arg_NO_PRETTY_VALUES)
# Remove value param name, if any, from test name
string(REGEX REPLACE "^(.+)/.+$" "\\1" pretty_test_name "${pretty_test_name}")
set(pretty_test_name "${pretty_test_name}/${current_test_value_param}")
endif()
if(NOT current_test_type_param STREQUAL "")
# Parse type param name from suite name
if(pretty_test_suite MATCHES "^(.+)/(.+)$")
set(pretty_test_suite "${CMAKE_MATCH_1}")
set(current_type_param_name "${CMAKE_MATCH_2}")
else()
set(current_type_param_name "")
endif()
if (NOT arg_NO_PRETTY_TYPES)
string(APPEND pretty_test_name "<${current_test_type_param}>")
elseif(NOT current_type_param_name STREQUAL "")
string(APPEND pretty_test_name "<${current_type_param_name}>")
endif()
endif()
set(test_name_template "@prefix@@pretty_test_suite@.@pretty_test_name@@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")
set(maybe_LOCATION "")
if(NOT current_test_file STREQUAL "" AND NOT current_test_line STREQUAL "")
set(maybe_LOCATION DEF_SOURCE_LINE "${current_test_file}:${current_test_line}")
endif()
add_command(set_tests_properties
"${guarded_testname}"
PROPERTIES
${maybe_DISABLED}
${maybe_LOCATION}
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}")
# Command line output doesn't contain information about the file and line number of the tests
set(current_test_file "")
set(current_test_line "")
# 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 GREATER 0)
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 "file" current_test_file)
get_json_member_with_default(test_json "line" current_test_line)
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()
endif()
endmacro()
function(gtest_discover_tests_impl)
set(options "")
set(oneValueArgs
NO_PRETTY_TYPES # These two take a value, unlike gtest_discover_tests()
NO_PRETTY_VALUES #
TEST_TARGET
TEST_EXECUTABLE
TEST_WORKING_DIR
TEST_PREFIX
TEST_SUFFIX
TEST_LIST
CTEST_FILE
TEST_DISCOVERY_TIMEOUT
TEST_XML_OUTPUT_DIR
# 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_DISCOVERY_EXTRA_ARGS
TEST_PROPERTIES
TEST_EXECUTOR
)
set(multiValueArgs "")
cmake_parse_arguments(PARSE_ARGV 0 arg
"${options}" "${oneValueArgs}" "${multiValueArgs}"
)
set(prefix "${arg_TEST_PREFIX}")
set(suffix "${arg_TEST_SUFFIX}")
set(script)
set(tests)
set(tests_buffer "")
# If a file at ${arg_CTEST_FILE} already exists, we overwrite it.
file(REMOVE "${arg_CTEST_FILE}")
set(filter)
if(arg_TEST_FILTER)
set(filter "--gtest_filter=${arg_TEST_FILTER}")
endif()
# CMP0178 has already been handled in gtest_discover_tests(), so we only need
# to implement NEW behavior here. This means preserving empty arguments for
# TEST_EXECUTOR. For OLD or WARN, gtest_discover_tests() already removed any
# empty arguments.
set(launcherArgs "")
if(NOT "${arg_TEST_EXECUTOR}" STREQUAL "")
list(JOIN arg_TEST_EXECUTOR "]==] [==[" launcherArgs)
set(launcherArgs "[==[${launcherArgs}]==]")
endif()
# Run test executable to get list of available tests
if(NOT EXISTS "${arg_TEST_EXECUTABLE}")
message(FATAL_ERROR
"Specified test executable does not exist.\n"
" Path: '${arg_TEST_EXECUTABLE}'"
)
endif()
set(discovery_extra_args "")
if(NOT "${arg_TEST_DISCOVERY_EXTRA_ARGS}" STREQUAL "")
list(JOIN arg_TEST_DISCOVERY_EXTRA_ARGS "]==] [==[" discovery_extra_args)
set(discovery_extra_args "[==[${discovery_extra_args}]==]")
endif()
# Avoid a potential race condition for the POST_BUILD case when multiple
# calls are made to gtest_discover_tests() for different targets but the same
# working directory. For PRE_TEST, we're always executing serially during the
# ctest setup phase, so there is no race condition there, but POST_BUILD can
# lead to this code path being run in parallel. Use a hash to avoid potential
# problems with very long target names.
string(SHA256 target_hash "${arg_TEST_TARGET}")
string(SUBSTRING "${target_hash}" 0 10 target_hash)
set(json_file
"${arg_TEST_WORKING_DIR}/cmake_test_discovery_${target_hash}.json"
)
# Remove json file to make sure we don't pick up an outdated one
file(REMOVE "${json_file}")
cmake_language(EVAL CODE
"execute_process(
COMMAND ${launcherArgs} [==[${arg_TEST_EXECUTABLE}]==]
--gtest_list_tests
[==[--gtest_output=json:${json_file}]==]
${filter}
${discovery_extra_args}
WORKING_DIRECTORY [==[${arg_TEST_WORKING_DIR}]==]
TIMEOUT ${arg_TEST_DISCOVERY_TIMEOUT}
OUTPUT_VARIABLE output
RESULT_VARIABLE result
)"
)
if(NOT ${result} EQUAL 0)
string(REPLACE "\n" "\n " output "${output}")
if(arg_TEST_EXECUTOR)
set(path "${arg_TEST_EXECUTOR} ${arg_TEST_EXECUTABLE}")
else()
set(path "${arg_TEST_EXECUTABLE}")
endif()
message(FATAL_ERROR
"Error running test executable.\n"
" Path: '${path}'\n"
" Working directory: '${arg_TEST_WORKING_DIR}'\n"
" Result: ${result}\n"
" Output:\n"
" ${output}\n"
)
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.
# 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 "")
list(APPEND tests "${tests_buffer}")
endif()
# Create a list of all discovered tests, which users may use to e.g. set
# properties on the tests
add_command(set "" ${arg_TEST_LIST} "${tests}")
# Write remaining content to the CTest script
file(APPEND "${arg_CTEST_FILE}" "${script}")
endfunction()
if(CMAKE_SCRIPT_MODE_FILE)
gtest_discover_tests_impl(
NO_PRETTY_TYPES ${NO_PRETTY_TYPES}
NO_PRETTY_VALUES ${NO_PRETTY_VALUES}
TEST_TARGET ${TEST_TARGET}
TEST_EXECUTABLE ${TEST_EXECUTABLE}
TEST_EXECUTOR "${TEST_EXECUTOR}"
TEST_WORKING_DIR ${TEST_WORKING_DIR}
TEST_PREFIX ${TEST_PREFIX}
TEST_SUFFIX ${TEST_SUFFIX}
TEST_FILTER ${TEST_FILTER}
TEST_LIST ${TEST_LIST}
CTEST_FILE ${CTEST_FILE}
TEST_DISCOVERY_TIMEOUT ${TEST_DISCOVERY_TIMEOUT}
TEST_XML_OUTPUT_DIR ${TEST_XML_OUTPUT_DIR}
TEST_EXTRA_ARGS "${TEST_EXTRA_ARGS}"
TEST_DISCOVERY_EXTRA_ARGS "${TEST_DISCOVERY_EXTRA_ARGS}"
TEST_PROPERTIES "${TEST_PROPERTIES}"
)
endif()
|