Skip to content

Commit 2dce267

Browse files
committed
cc tests: run ctest tests in parallel
* Added automatic assignment of cmake `RESOURCE_LOCK` properties to `userver_testsuite_add[_simple]`, `userver_add_utest` and `userver_add_ubench_test`. The locks are named `userver_<target>` for each major `userver::<target>`. * Added a new `RESOURCE_LOCKS` parameter to `userver_testsuite_add[_simple]`. Use it to fix parallel ctest runs when using a database indirectly, e.g. through @ref scripts/docs/en/userver/odbc.md . commit_hash:b4ccccf9fc4ee86af372146a111e6d1714427e29
1 parent df1104b commit 2dce267

6 files changed

Lines changed: 138 additions & 6 deletions

File tree

.mapping.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@
692692
"cmake/SetupScyllaDeps.cmake":"taxi/uservices/userver/cmake/SetupScyllaDeps.cmake",
693693
"cmake/SetupYdbCppSDK.cmake":"taxi/uservices/userver/cmake/SetupYdbCppSDK.cmake",
694694
"cmake/Stacktrace.cmake":"taxi/uservices/userver/cmake/Stacktrace.cmake",
695+
"cmake/TargetIteration.cmake":"taxi/uservices/userver/cmake/TargetIteration.cmake",
695696
"cmake/UserverCodegenTarget.cmake":"taxi/uservices/userver/cmake/UserverCodegenTarget.cmake",
696697
"cmake/UserverCxxCompileOptionsIfSupported.cmake":"taxi/uservices/userver/cmake/UserverCxxCompileOptionsIfSupported.cmake",
697698
"cmake/UserverEmbedFile.cmake":"taxi/uservices/userver/cmake/UserverEmbedFile.cmake",

cmake/TargetIteration.cmake

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Functions for traversing the CMake target link graph.
2+
#
3+
# Provides:
4+
#
5+
# * _userver_collect_linked_targets function that collects all targets a given target
6+
# links against, directly or transitively.
7+
#
8+
# Implementation note: public functions here should be usable even without a direct
9+
# include of this script, so the functions should not rely on non-cache variables
10+
# being present.
11+
include_guard(GLOBAL)
12+
13+
# Collects all CMake targets that `target` links against, directly or transitively,
14+
# into `output_var` (deduplicated). There is no built-in configure-time query for
15+
# transitive dependencies, so the link graph is walked iteratively over
16+
# LINK_LIBRARIES / INTERFACE_LINK_LIBRARIES. Link entries that are not plain targets
17+
# (raw libraries, generator expressions, etc.) are ignored without inspection.
18+
#
19+
# Membership is tracked via per-target marker variables (O(1) lookup) instead of a
20+
# list scan, so the traversal is linear in the number of targets and edges and is
21+
# immune to cycles and diamond-dependency blowup. The markers are plain function-local
22+
# variables: they are visible across the whole loop, are cleaned up automatically when
23+
# the function returns, and never leak between separate invocations.
24+
function(_userver_collect_linked_targets target output_var)
25+
set(visited "")
26+
set(frontier "${target}")
27+
28+
while(frontier)
29+
set(next_frontier "")
30+
foreach(current IN LISTS frontier)
31+
if(NOT TARGET "${current}")
32+
continue()
33+
endif()
34+
35+
# Resolve aliases (e.g. userver::postgresql -> userver-postgresql).
36+
get_target_property(aliased_target "${current}" ALIASED_TARGET)
37+
if(aliased_target)
38+
set(current "${aliased_target}")
39+
endif()
40+
41+
string(MAKE_C_IDENTIFIER "userver_clt_seen_${current}" seen_marker)
42+
if(DEFINED ${seen_marker})
43+
continue()
44+
endif()
45+
set(${seen_marker} TRUE)
46+
list(APPEND visited "${current}")
47+
48+
get_target_property(target_type "${current}" TYPE)
49+
# LINK_LIBRARIES may not be read on INTERFACE libraries on old CMake.
50+
if(NOT target_type STREQUAL "INTERFACE_LIBRARY")
51+
get_target_property(link_libraries "${current}" LINK_LIBRARIES)
52+
if(link_libraries)
53+
list(APPEND next_frontier ${link_libraries})
54+
endif()
55+
endif()
56+
get_target_property(interface_libraries "${current}" INTERFACE_LINK_LIBRARIES)
57+
if(interface_libraries)
58+
list(APPEND next_frontier ${interface_libraries})
59+
endif()
60+
endforeach()
61+
set(frontier "${next_frontier}")
62+
endwhile()
63+
64+
set(${output_var}
65+
"${visited}"
66+
PARENT_SCOPE
67+
)
68+
endfunction()

cmake/UserverTestsuite.cmake

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ include_guard(GLOBAL)
1515
# Pack initialization into a function to avoid non-cache variable leakage.
1616
function(_userver_prepare_testsuite)
1717
include("${CMAKE_CURRENT_LIST_DIR}/UserverVenv.cmake")
18+
include("${CMAKE_CURRENT_LIST_DIR}/TargetIteration.cmake")
1819
set_property(GLOBAL PROPERTY userver_cmake_dir "${CMAKE_CURRENT_LIST_DIR}")
1920

2021
# @ingroup libraries
@@ -143,7 +144,7 @@ endfunction()
143144
# TODO
144145
function(userver_testsuite_add)
145146
set(oneValueArgs SERVICE_TARGET TEST_SUFFIX WORKING_DIRECTORY PYTHON_BINARY PRETTY_LOGS SQL_LIBRARY)
146-
set(multiValueArgs PYTEST_ARGS REQUIREMENTS PYTHONPATH TEST_ENV)
147+
set(multiValueArgs PYTEST_ARGS REQUIREMENTS PYTHONPATH TEST_ENV RESOURCE_LOCKS)
147148
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
148149

149150
_userver_setup_environment_validate_impl()
@@ -207,7 +208,8 @@ function(userver_testsuite_add)
207208
set(TESTSUITE_RUNNER "${CMAKE_CURRENT_BINARY_DIR}/runtests-${service_target_with_suffix}")
208209
list(APPEND ARG_PYTHONPATH "${USERVER_TESTSUITE_DIR}/pytest_plugins")
209210

210-
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary)
211+
set(testsuite_temp_dir "${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/${service_target_with_suffix}")
212+
file(MAKE_DIRECTORY "${testsuite_temp_dir}")
211213

212214
set(TESTS_PATHS ${ARG_WORKING_DIRECTORY})
213215
if(ARG_SQL_LIBRARY)
@@ -224,8 +226,8 @@ function(userver_testsuite_add)
224226
"${python_binary}" "${USERVER_TESTSUITE_DIR}/create_runner.py" "--output=${TESTSUITE_RUNNER}"
225227
"--python=${python_binary}" "--tests-path=${TESTS_PATHS}" "--working-dir=${CMAKE_CURRENT_BINARY_DIR}"
226228
"--python-path=${ARG_PYTHONPATH}" -- "--build-dir=${CMAKE_CURRENT_BINARY_DIR}"
227-
"--service-logs-file=${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary/service.log"
228-
"--basetemp=${CMAKE_CURRENT_BINARY_DIR}/Testing/Temporary" ${ARG_PYTEST_ARGS}
229+
"--service-logs-file=${testsuite_temp_dir}/service.log"
230+
"--basetemp=${testsuite_temp_dir}" ${ARG_PYTEST_ARGS}
229231
DEPENDS "${USERVER_TESTSUITE_DIR}/create_runner.py"
230232
COMMENT "Creating testsuite runner at ${TESTSUITE_RUNNER}"
231233
VERBATIM ${CODEGEN}
@@ -253,6 +255,13 @@ function(userver_testsuite_add)
253255
set_tests_properties("${testsuite_test_name}" PROPERTIES ENVIRONMENT "${ARG_TEST_ENV}")
254256
endif()
255257

258+
_userver_get_test_resource_locks_for_target("${ARG_SERVICE_TARGET}" testsuite_resource_locks)
259+
list(APPEND testsuite_resource_locks ${ARG_RESOURCE_LOCKS})
260+
if(testsuite_resource_locks)
261+
list(REMOVE_DUPLICATES testsuite_resource_locks)
262+
set_tests_properties("${testsuite_test_name}" PROPERTIES RESOURCE_LOCK "${testsuite_resource_locks}")
263+
endif()
264+
256265
# Pre-collect command in a list to support spaces in paths
257266
set(testsuite_start_command)
258267
list(APPEND testsuite_start_command "${python_binary}")
@@ -293,6 +302,8 @@ endfunction()
293302
# @multiparam REQUIREMENTS
294303
# @multiparam PYTHONPATH
295304
# @multiparam TEST_ENV
305+
# @multiparam RESOURCE_LOCKS ctest resource locks for databases the service uses
306+
# without linking their module (e.g. 'userver_postgresql' for an odbc service)
296307
function(userver_testsuite_add_simple)
297308
set(oneValueArgs
298309
SERVICE_TARGET
@@ -307,7 +318,7 @@ function(userver_testsuite_add_simple)
307318
DUMP_CONFIG
308319
SQL_LIBRARY
309320
)
310-
set(multiValueArgs PYTEST_ARGS REQUIREMENTS PYTHONPATH TEST_ENV)
321+
set(multiValueArgs PYTEST_ARGS REQUIREMENTS PYTHONPATH TEST_ENV RESOURCE_LOCKS)
311322
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
312323

313324
_userver_setup_environment_validate_impl()
@@ -455,6 +466,7 @@ function(userver_testsuite_add_simple)
455466
REQUIREMENTS ${ARG_REQUIREMENTS}
456467
PYTHONPATH ${ARG_PYTHONPATH}
457468
TEST_ENV ${ARG_TEST_ENV}
469+
RESOURCE_LOCKS ${ARG_RESOURCE_LOCKS}
458470
SQL_LIBRARY ${ARG_SQL_LIBRARY}
459471
)
460472
endfunction()
@@ -495,6 +507,14 @@ function(userver_add_utest)
495507
if(ARG_TEST_ENV)
496508
set_tests_properties("${ARG_NAME}" PROPERTIES ENVIRONMENT "${ARG_TEST_ENV}")
497509
endif()
510+
511+
set(utest_resource_locks "")
512+
foreach(database IN LISTS ARG_DATABASES)
513+
list(APPEND utest_resource_locks "userver_${database}")
514+
endforeach()
515+
if(utest_resource_locks)
516+
set_tests_properties("${ARG_NAME}" PROPERTIES RESOURCE_LOCK "${utest_resource_locks}")
517+
endif()
498518
endfunction()
499519

500520
# @param NAME
@@ -528,4 +548,41 @@ function(userver_add_ubench_test)
528548
)
529549
endfunction()
530550

551+
# Converts a target name into a ';'-separated list of ctest RESOURCE_LOCK names, one
552+
# per userver database module the target links against (directly or transitively).
553+
# Tests that share a lock are never run concurrently by ctest, which protects the
554+
# per-engine database daemons started by testsuite (each on a fixed port/data-dir)
555+
# from races during parallel `ctest -j`. Lock name for an engine is "userver_<engine>".
556+
function(_userver_get_test_resource_locks_for_target target output_var)
557+
# userver database modules backed by a shared daemon. Embedded engines without a
558+
# daemon (e.g. sqlite, rocks) are intentionally omitted.
559+
set(db_engines
560+
postgresql
561+
mongo
562+
redis
563+
clickhouse
564+
rabbitmq
565+
kafka
566+
mysql
567+
ydb
568+
)
569+
570+
set(resource_locks "")
571+
if(TARGET "${target}")
572+
_userver_collect_linked_targets("${target}" linked_targets)
573+
foreach(engine IN LISTS db_engines)
574+
list(FIND linked_targets "userver-${engine}" index_dash)
575+
list(FIND linked_targets "userver::${engine}" index_colons)
576+
if(NOT index_dash EQUAL -1 OR NOT index_colons EQUAL -1)
577+
list(APPEND resource_locks "userver_${engine}")
578+
endif()
579+
endforeach()
580+
endif()
581+
582+
set(${output_var}
583+
"${resource_locks}"
584+
PARENT_SCOPE
585+
)
586+
endfunction()
587+
531588
_userver_prepare_testsuite()

core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ _userver_directory_install(
315315
_userver_directory_install(
316316
COMPONENT core
317317
FILES "${USERVER_ROOT_DIR}/cmake/UserverTestsuite.cmake"
318+
"${USERVER_ROOT_DIR}/cmake/TargetIteration.cmake"
318319
"${USERVER_ROOT_DIR}/cmake/install/userver-core-config.cmake"
319320
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/userver
320321
)

odbc/functional_tests/basic_chaos/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ target_link_libraries(${PROJECT_NAME} userver::odbc)
55

66
userver_chaos_testsuite_add(
77
ENV "TESTSUITE_PGSQL_SERVER_START_TIMEOUT=120.0"
8+
# The odbc service talks to postgresql without linking userver::postgresql, so the
9+
# shared lock must be declared explicitly.
10+
RESOURCE_LOCKS userver_postgresql
811
)

testsuite/SetupUserverTestsuiteEnv.cmake

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ userver_venv_setup(
1313
function(userver_chaos_testsuite_add)
1414
set(options)
1515
set(oneValueArgs TESTS_DIRECTORY)
16-
set(multiValueArgs PYTHONPATH ENV)
16+
set(multiValueArgs PYTHONPATH ENV RESOURCE_LOCKS)
1717
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
1818

1919
userver_testsuite_add_simple(
@@ -25,5 +25,7 @@ function(userver_chaos_testsuite_add)
2525
${ARG_PYTHONPATH}
2626
TEST_ENV
2727
"${ARG_ENV}"
28+
RESOURCE_LOCKS
29+
${ARG_RESOURCE_LOCKS}
2830
)
2931
endfunction()

0 commit comments

Comments
 (0)