Skip to content

Commit 30a4619

Browse files
committed
Better integration of boost.test and CTest allowing parallel test execution
1 parent 2596474 commit 30a4619

4 files changed

Lines changed: 657 additions & 8 deletions

File tree

CMakeLists.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
99
set(CMAKE_CXX_EXTENSIONS OFF)
1010
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
1111

12-
set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR} ${CMAKE_MODULE_PATH})
12+
set(CMAKE_MODULE_PATH
13+
"${CMAKE_CURRENT_SOURCE_DIR}/cmake"
14+
${CMAKE_BINARY_DIR}
15+
${CMAKE_MODULE_PATH}
16+
)
1317
set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR} ${CMAKE_PREFIX_PATH})
1418

1519
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/lib)

cmake/BoostTestAddTests.cmake

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
# Copyright 2020, 2021 Deniz Bahadir and contributors
2+
# Distributed under the Boost Software License, Version 1.0.
3+
# See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt
4+
#
5+
# This code is based on an implementation of the `gtest_discover_tests()`
6+
# functionality that was originally distributed together with CMake unter the
7+
# OSI-approved BSD 3-Clause License. The original authors and contributors of
8+
# that code were so kind to agree to dual-license their work also under the
9+
# Boost Software License, Version 1.0, so that this derived worked can be
10+
# distributed under the same license as well.
11+
# A public record of their granted permission can be found in this discussion
12+
# of merge-request 4145 in the CMake issue tracker:
13+
# https://gitlab.kitware.com/cmake/cmake/-/merge_requests/4145#note_998500
14+
#
15+
# Therefore many thanks go to the following original authors and contributors:
16+
# Matthew Woehlke <matthew.woehlke@kitware.com>
17+
# Steffen Seckler <steffen.seckler@smartronic.de>
18+
# Ryan Thornton <ThorntonRyan@JohnDeere.com>
19+
# Kevin Puetz <PuetzKevinA@JohnDeere.com>
20+
# Stefan Floeren <stefan.floeren@smartronic.de>
21+
# Alexander Stein <alexander.stein@mailbox.org>
22+
#
23+
# Deniz Bahadir in August of 2021.
24+
#
25+
26+
cmake_minimum_required(VERSION ${CMAKE_VERSION})
27+
28+
# Overwrite possibly existing ${__CTEST_FILE} with empty file.
29+
set(flush_tests_MODE WRITE)
30+
31+
32+
# Flushes script to ${__CTEST_FILE}.
33+
macro(flush_script)
34+
file(${flush_tests_MODE} "${__CTEST_FILE}" "${script}")
35+
set(flush_tests_MODE APPEND)
36+
set(script "")
37+
endmacro()
38+
39+
40+
# Flushes tests_buffer to tests variable.
41+
macro(flush_tests_buffer)
42+
list(APPEND tests "${tests_buffer}")
43+
set(tests_buffer "")
44+
endmacro()
45+
46+
47+
# Removes surrounding double quotes (if any) from value of given variable VAR.
48+
function(remove_outer_quotes VAR)
49+
string(REGEX REPLACE "^\"(.*)\"$" "\\1" ${VAR} "${${VAR}}")
50+
set(${VAR} "${${VAR}}" PARENT_SCOPE)
51+
endfunction()
52+
53+
54+
# Adds commands to script.
55+
macro(add_command NAME)
56+
set(_args "")
57+
foreach(_arg ${ARGN})
58+
if(_arg MATCHES "[^-./:a-zA-Z0-9_]")
59+
string(APPEND _args " [==[${_arg}]==]") # form a bracket argument
60+
else()
61+
string(APPEND _args " ${_arg}")
62+
endif()
63+
endforeach()
64+
string(APPEND script "${NAME}(${_args})\n")
65+
string(LENGTH "${script}" _script_len)
66+
if(${_script_len} GREATER "50000")
67+
flush_script()
68+
endif()
69+
# Unsets macro local variables to prevent leakage outside of this macro.
70+
unset(_args)
71+
unset(_script_len)
72+
endmacro()
73+
74+
# Adds another test to the script.
75+
macro(add_another_test hierarchy_list enabled source_line separator)
76+
# Create the name and path of the test-case...
77+
if (CMAKE_VERSION VERSION_LESS "3.12")
78+
set(test_name)
79+
set(test_path)
80+
foreach(hierarchy_entry IN LISTS ${hierarchy_list})
81+
if ("${test_name}" STREQUAL "")
82+
set(test_name "${hierarchy_entry}")
83+
else()
84+
set(test_name "${test_name}${separator}${hierarchy_entry}")
85+
endif()
86+
if ("${test_path}" STREQUAL "")
87+
set(test_path "${hierarchy_entry}")
88+
else()
89+
set(test_path "${test_path}/${hierarchy_entry}")
90+
endif()
91+
endforeach()
92+
else()
93+
list(JOIN ${hierarchy_list} ${separator} test_name)
94+
list(JOIN ${hierarchy_list} "/" test_path)
95+
endif()
96+
# ...and add to script.
97+
add_command(add_test
98+
"${prefix}${test_name}${suffix}"
99+
${__TEST_EXECUTOR}
100+
"${__TEST_EXECUTABLE}"
101+
"--run_test=${test_path}"
102+
${extra_args}
103+
)
104+
if(NOT ${enabled})
105+
add_command(set_tests_properties
106+
"${prefix}${test_name}${suffix}"
107+
PROPERTIES
108+
DISABLED TRUE
109+
)
110+
endif()
111+
add_command(set_tests_properties
112+
"${prefix}${test_name}${suffix}"
113+
PROPERTIES
114+
WORKING_DIRECTORY "${__TEST_WORKING_DIR}"
115+
DEF_SOURCE_LINE "${source_line}"
116+
${properties}
117+
)
118+
list(APPEND tests_buffer "${prefix}${test_name}${suffix}")
119+
list(LENGTH tests_buffer tests_buffer_length)
120+
if(${tests_buffer_length} GREATER "250")
121+
flush_tests_buffer()
122+
endif()
123+
endmacro()
124+
125+
126+
# Internal implementation for boost_test_discover_tests.
127+
function(boost_test_discover_tests_impl)
128+
129+
cmake_parse_arguments(
130+
"_"
131+
""
132+
"TEST_TARGET;TEST_EXECUTABLE;TEST_WORKING_DIR;TEST_PREFIX;TEST_SUFFIX;TEST_NAME_SEPARATOR;TEST_LIST;TEST_SKIP_DISABLED;CTEST_FILE;TEST_DISCOVERY_TIMEOUT"
133+
"TEST_EXTRA_ARGS;TEST_PROPERTIES;TEST_EXECUTOR"
134+
${ARGN}
135+
)
136+
137+
set(prefix "${__TEST_PREFIX}")
138+
set(suffix "${__TEST_SUFFIX}")
139+
set(extra_args ${__TEST_EXTRA_ARGS})
140+
set(properties ${__TEST_PROPERTIES})
141+
set(script)
142+
set(tests)
143+
set(tests_buffer)
144+
145+
# Make sure the working directory exists.
146+
file(MAKE_DIRECTORY "${__TEST_WORKING_DIR}")
147+
# Run test executable to get list of available tests.
148+
if(NOT EXISTS "${__TEST_EXECUTABLE}")
149+
message(FATAL_ERROR
150+
"Specified test executable does not exist.\n"
151+
" Path: '${__TEST_EXECUTABLE}'"
152+
)
153+
endif()
154+
155+
# DOT format is used to get the source file and line number of a test case.
156+
execute_process(
157+
COMMAND ${__TEST_EXECUTOR} "${__TEST_EXECUTABLE}" --list_content=DOT --detect_memory_leaks=0
158+
WORKING_DIRECTORY "${__TEST_WORKING_DIR}"
159+
TIMEOUT ${__TEST_DISCOVERY_TIMEOUT}
160+
OUTPUT_VARIABLE dot_output
161+
ERROR_VARIABLE dot_output # Boost.Test writes the requested content to stderr!
162+
RESULT_VARIABLE result
163+
)
164+
165+
if(NOT ${result} EQUAL 0)
166+
message(FATAL_ERROR
167+
"Error running test executable.\n"
168+
" Path: '${__TEST_EXECUTABLE}'\n"
169+
" Result: ${result}\n"
170+
" Output:\n"
171+
" ${dot_output}\n"
172+
)
173+
endif()
174+
175+
# Replace \ with / to prevent invalid escape sequence error when parsing DOT output
176+
string(REPLACE [[\]] [[/]] dot_output "${dot_output}")
177+
178+
# HRF format is used to build the test suite / test case hierarchy and determine which tests are enabled / disabled
179+
execute_process(
180+
COMMAND ${__TEST_EXECUTOR} "${__TEST_EXECUTABLE}" --list_content=HRF --detect_memory_leaks=0
181+
WORKING_DIRECTORY "${__TEST_WORKING_DIR}"
182+
TIMEOUT ${__TEST_DISCOVERY_TIMEOUT}
183+
OUTPUT_VARIABLE hrf_output
184+
ERROR_VARIABLE hrf_output # Boost.Test writes the requested content to stderr!
185+
RESULT_VARIABLE result
186+
)
187+
188+
if(NOT ${result} EQUAL 0)
189+
string(REPLACE "\n" "\n " HRF output "${hrf_output}")
190+
message(FATAL_ERROR
191+
"Error running test executable.\n"
192+
" Path: '${__TEST_EXECUTABLE}'\n"
193+
" Result: ${result}\n"
194+
" Output:\n"
195+
" ${hrf_output}\n"
196+
)
197+
endif()
198+
199+
# Preserve semicolon in test-parameters
200+
string(REPLACE [[;]] [[\;]] hrf_output "${hrf_output}")
201+
string(REPLACE "\n" ";" hrf_output "${hrf_output}")
202+
203+
# The hierarchy and its depth-level of the test of the former line.
204+
set(test_hierarchy "${TEST_TARGET}_MISSING_TESTS")
205+
set(former_level NaN)
206+
set(test_enabled 0)
207+
set(test_source_line "")
208+
209+
# Parse output
210+
foreach(line ${hrf_output})
211+
# Determine the depth-level of the next test-hierarchy.
212+
# Note: Each new depth-level (except for the top one) is indented
213+
# by 4 spaces. So we need to count the spaces.
214+
string(REGEX MATCH "^[ ]+" next_level "${line}")
215+
string(LENGTH "${next_level}" next_level)
216+
math(EXPR next_level "${next_level} / 4")
217+
218+
# Add the test for the test-case from the former loop-run?
219+
if ((next_level LESS former_level) OR (next_level EQUAL former_level))
220+
# Add test-case to the script.
221+
add_another_test(test_hierarchy ${test_enabled} "${test_source_line}" "${__TEST_NAME_SEPARATOR}")
222+
223+
# Prepare the hierarchy list for the next test-case.
224+
math(EXPR diff "${former_level} - ${next_level}")
225+
foreach(i RANGE ${diff})
226+
if (CMAKE_VERSION VERSION_LESS "3.15")
227+
list(LENGTH test_hierarchy length)
228+
math(EXPR index "${length} - 1")
229+
list(REMOVE_AT test_hierarchy ${index})
230+
else()
231+
list(POP_BACK test_hierarchy)
232+
endif()
233+
endforeach()
234+
endif()
235+
if (former_level STREQUAL NaN)
236+
set(test_hierarchy "") # Clear hierarchy, as we have at least one test.
237+
set(test_source_line "")
238+
endif()
239+
set(former_level ${next_level}) # Store depth-level for next loop-run.
240+
241+
# Extract the name of the next test suite/case and determine if enabled.
242+
# Note: A trailing '*' indicates that the test is enabled (by default).
243+
string(REGEX REPLACE ":( .*)?$" "" name "${line}")
244+
string(STRIP "${name}" name)
245+
if(name MATCHES "\\*$")
246+
set(test_enabled 1)
247+
string(REGEX REPLACE "\\*$" "" name "${name}")
248+
elseif(__TEST_SKIP_DISABLED)
249+
set(test_enabled 0)
250+
endif()
251+
252+
# Sanitize name for further processing downstream:
253+
# - escape \ ; $
254+
string(REGEX REPLACE [[([\;$])]] [[\\\1]] name "${name}")
255+
256+
# Extract the source file and line number name of the next test case.
257+
string(REGEX MATCH "${name}\\|[^\"]+" test_source_line "${dot_output}")
258+
string(REGEX REPLACE "${name}\\|([^\\(]+)\\(([0-9]+)\\)" "\\1:\\2" test_source_line "${test_source_line}")
259+
260+
# Add the name to the hierarchy list.
261+
list(APPEND test_hierarchy "${name}")
262+
endforeach()
263+
264+
# Add last test-case to the script (if any).
265+
add_another_test(test_hierarchy ${test_enabled} "${test_source_line}" "${__TEST_NAME_SEPARATOR}")
266+
267+
# Create a list of all discovered tests, which users may use to e.g. set
268+
# properties on the tests.
269+
flush_tests_buffer()
270+
if (NOT former_level STREQUAL "NaN")
271+
add_command(set ${__TEST_LIST} ${tests})
272+
endif()
273+
274+
# # Write (remaining) content to the CTest script
275+
flush_script()
276+
277+
endfunction()
278+
279+
280+
if(CMAKE_SCRIPT_MODE_FILE)
281+
# Note: Make sure to remove the outer layer of quotes that were added
282+
# to preserve whitespace when handed over via cmdline.
283+
remove_outer_quotes(TEST_TARGET)
284+
remove_outer_quotes(TEST_EXECUTABLE)
285+
remove_outer_quotes(TEST_EXECUTOR)
286+
remove_outer_quotes(TEST_WORKING_DIR)
287+
remove_outer_quotes(TEST_EXTRA_ARGS)
288+
remove_outer_quotes(TEST_PROPERTIES)
289+
remove_outer_quotes(TEST_PREFIX)
290+
remove_outer_quotes(TEST_SUFFIX)
291+
remove_outer_quotes(TEST_NAME_SEPARATOR)
292+
remove_outer_quotes(TEST_LIST)
293+
remove_outer_quotes(TEST_SKIP_DISABLED)
294+
remove_outer_quotes(CTEST_FILE)
295+
remove_outer_quotes(TEST_DISCOVERY_TIMEOUT)
296+
297+
boost_test_discover_tests_impl(
298+
TEST_TARGET ${TEST_TARGET}
299+
TEST_EXECUTABLE ${TEST_EXECUTABLE}
300+
TEST_EXECUTOR ${TEST_EXECUTOR}
301+
TEST_WORKING_DIR ${TEST_WORKING_DIR}
302+
TEST_PREFIX ${TEST_PREFIX}
303+
TEST_SUFFIX ${TEST_SUFFIX}
304+
TEST_NAME_SEPARATOR ${TEST_NAME_SEPARATOR}
305+
TEST_LIST ${TEST_LIST}
306+
TEST_SKIP_DISABLED ${TEST_SKIP_DISABLED}
307+
CTEST_FILE ${CTEST_FILE}
308+
TEST_DISCOVERY_TIMEOUT ${TEST_DISCOVERY_TIMEOUT}
309+
TEST_EXTRA_ARGS ${TEST_EXTRA_ARGS}
310+
TEST_PROPERTIES ${TEST_PROPERTIES}
311+
)
312+
endif()

0 commit comments

Comments
 (0)