Skip to content

Commit 1219518

Browse files
committed
Avoid n**2 runtime when creating list of tests in catch_discover_tests
This was another place where the script triggered the quadratic runtime from calling `string(APPEND` (or `list(APPEND`) repeatedly. As in 60c8b87, we avoid this by flushing the test list into a file every 50kB of text input. As the string with the test list never grew quite as much as the string that contains all the test definitions, this only provides significant savings for high number of tests. It starts being properly measurable around 4k tests at ~100ms, but grows to ~4s at 32k tests.
1 parent 630840c commit 1219518

1 file changed

Lines changed: 44 additions & 9 deletions

File tree

extras/CatchAddTests.cmake

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,23 @@ function(make_temp_file_path OUT_VARIABLE FALLBACK_PATH)
204204
set(${OUT_VARIABLE} "${FINAL_TEMP_PATH}" PARENT_SCOPE)
205205
endfunction()
206206

207+
# Computes the path of the test-list file based on the path for the main
208+
# CTest script file (passed in `CTEST_FILE`).
209+
#
210+
# It works by replacing the `_tests` part of the original name with
211+
# `_test-list`, or failing that, it appends `-list.cmake` instead.
212+
#
213+
# <base>_tests.cmake -> <base>_test-list.cmake
214+
# <base>_tests-Debug.cmake -> <base>_test-list-Debug.cmake
215+
function(make_test_list_file_path CTEST_FILE OUT_VARIABLE)
216+
if("${CTEST_FILE}" MATCHES "_tests(.*)\\.cmake$")
217+
string(REGEX REPLACE "_tests(.*)\\.cmake$" "_test-list\\1.cmake" list_file "${CTEST_FILE}")
218+
else()
219+
set(list_file "${CTEST_FILE}-list.cmake")
220+
endif()
221+
set(${OUT_VARIABLE} "${list_file}" PARENT_SCOPE)
222+
endfunction()
223+
207224
function(catch_discover_tests_impl)
208225
cmake_parse_arguments(
209226
""
@@ -213,9 +230,14 @@ function(catch_discover_tests_impl)
213230
${ARGN}
214231
)
215232

216-
# We periodically append to the output file below, so we have to ensure
233+
# We write the list of all discovered tests into a separate file
234+
make_test_list_file_path("${_CTEST_FILE}" _CTEST_LIST_FILE)
235+
236+
# We periodically append to the output files below, so we have to ensure
217237
# that it is empty at the start, or we get duplicated test scripts.
218238
file(REMOVE "${_CTEST_FILE}")
239+
file(REMOVE "${_CTEST_LIST_FILE}")
240+
219241
# Size (in Bytes) at which the intermediate `script` var is dumped to file.
220242
set(_WriteToFileThreshold 50000)
221243

@@ -360,10 +382,19 @@ function(catch_discover_tests_impl)
360382

361383
# Exit early if no tests are detected
362384
if(NOT tests)
363-
file(WRITE "${_CTEST_FILE}" "")
385+
# Still emit an (empty) test list file and have the main script include
386+
# it, so that consumers relying on the `${_TEST_LIST}` variable and on the
387+
# include structure get consistent behavior regardless of test count.
388+
file(WRITE "${_CTEST_LIST_FILE}" "set(${_TEST_LIST})\n")
389+
file(WRITE "${_CTEST_FILE}" "include(\"${_CTEST_LIST_FILE}\")\n")
364390
return()
365391
endif()
366392

393+
394+
# The 'set(VAR` header for the test list has to be written separately,
395+
# so that each test name can be appended file without further processing.
396+
set(test_names "set(${_TEST_LIST}")
397+
367398
# Each element in the tests is JSON-string representing one test object.
368399
# We have to parse it and then turn it into CTest script commands.
369400
foreach(single_test IN LISTS tests)
@@ -374,6 +405,11 @@ function(catch_discover_tests_impl)
374405
file(APPEND "${_CTEST_FILE}" "${script}")
375406
set(script "")
376407
endif()
408+
string(LENGTH "${test_names}" names_len)
409+
if (names_len GREATER _WriteToFileThreshold)
410+
file(APPEND "${_CTEST_LIST_FILE}" "${names_len}")
411+
set(test_names "")
412+
endif()
377413

378414
# The elements are still escaped and contain JSON-invalid characters,
379415
# they have to be unescaped before parsing them as JSON.
@@ -459,18 +495,17 @@ function(catch_discover_tests_impl)
459495
if(full_list_name MATCHES "[^-./:a-zA-Z0-9_]")
460496
# The space before the start of quote is important, so that we get
461497
# space-separated list in the final file.
462-
string(APPEND _test_names " [==[${full_list_name}]==]")
498+
string(APPEND test_names " [==[${full_list_name}]==]")
463499
else()
464-
string(APPEND _test_names " ${full_list_name}")
500+
string(APPEND test_names " ${full_list_name}")
465501
endif()
466502
endforeach()
467503

468-
# Create a list of all discovered tests, which users may use to e.g. set
469-
# properties on the tests
470-
string(APPEND script "set(${_TEST_LIST}${_test_names})\n")
471-
472-
# Write any script leftovers we have
504+
# Write any test names leftovers we have
505+
file(APPEND "${_CTEST_LIST_FILE}" "${test_names})\n")
506+
# Write any main script leftovers we have, and append the include of test names
473507
file(APPEND "${_CTEST_FILE}" "${script}")
508+
file(APPEND "${_CTEST_FILE}" "include(\"${_CTEST_LIST_FILE}\")\n")
474509
endfunction()
475510

476511
# To enable `include`ing this file in the unit test scripts, we only run

0 commit comments

Comments
 (0)