Skip to content

Commit d60372f

Browse files
committed
Add custom properties support and simplify test property handling.
- Add "PROPERTIES" option to pytest_discover_tests function to allow passing custom properties; - Refactor to use lua-style long bracket syntax for serializing environment and custom properties, consolidating them within a single set_tests_properties command; - Revert to using Pytest_ROOT instead of CMAKE_PREFIX_PATH in the integration document; - Add tests and update documentation.
1 parent c59bc95 commit d60372f

20 files changed

Lines changed: 268 additions & 108 deletions

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
- name: Build and Run Unit Tests
6262
shell: bash
6363
run: |
64-
cmake -D "CMAKE_BUILD_TYPE=Release" -S ./test -B ./test/build
64+
cmake -S ./test -B ./test/build
6565
cmake --build ./test/build
6666
6767
- name: Configure Example

cmake/FindPytest.cmake

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
# This module defines the following imported targets:
44
# Pytest::Pytest
55
#
6-
# It also exposes the 'pytest_discover_tests' function which adds ctest
7-
# for each pytest tests. The "BUNDLE_PYTHON_TESTS" environment variable
8-
# can be used to run all discovered tests all together.
6+
# It also exposes the 'pytest_discover_tests' function, which adds CTest
7+
# test for each Pytest test. The "BUNDLE_PYTHON_TESTS" environment variable
8+
# can be used to run all discovered tests together.
99
#
1010
# Usage:
1111
# find_package(Pytest)
@@ -53,14 +53,15 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
5353
PROPERTIES
5454
IMPORTED_LOCATION "${PYTEST_EXECUTABLE}")
5555

56+
# Function to discover pytest tests and add them to CTest.
5657
function(pytest_discover_tests NAME)
5758
cmake_parse_arguments(
5859
PARSE_ARGV 1 "" "STRIP_PARAM_BRACKETS;INCLUDE_FILE_PATH;BUNDLE_TESTS"
5960
"WORKING_DIRECTORY;TRIM_FROM_NAME;TRIM_FROM_FULL_NAME"
60-
"LIBRARY_PATH_PREPEND;PYTHON_PATH_PREPEND;ENVIRONMENT;DEPENDS"
61+
"LIBRARY_PATH_PREPEND;PYTHON_PATH_PREPEND;ENVIRONMENT;PROPERTIES;DEPENDS"
6162
)
6263

63-
# Identify library path environment name depending on the platform.
64+
# Set platform-specific library path environment variable.
6465
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
6566
set(LIBRARY_ENV_NAME PATH)
6667
elseif(CMAKE_SYSTEM_NAME STREQUAL Darwin)
@@ -69,11 +70,11 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
6970
set(LIBRARY_ENV_NAME LD_LIBRARY_PATH)
7071
endif()
7172

72-
# Sanitize all paths for CMake.
73+
# Convert paths to CMake-friendly format.
7374
cmake_path(CONVERT "$ENV{${LIBRARY_ENV_NAME}}" TO_CMAKE_PATH_LIST LIBRARY_PATH)
7475
cmake_path(CONVERT "$ENV{PYTHONPATH}" TO_CMAKE_PATH_LIST PYTHON_PATH)
7576

76-
# Prepend input path to environment variables.
77+
# Prepend specified paths to the library and Python paths.
7778
if (_LIBRARY_PATH_PREPEND)
7879
list(REVERSE _LIBRARY_PATH_PREPEND)
7980
foreach (_path ${_LIBRARY_PATH_PREPEND})
@@ -88,7 +89,7 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
8889
endforeach()
8990
endif()
9091

91-
# Default working directory to current build path if none is provided.
92+
# Set default working directory if none is specified.
9293
if (NOT _WORKING_DIRECTORY)
9394
set(_WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
9495
endif()
@@ -100,17 +101,11 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
100101
set(_BUNDLE_TESTS $ENV{BUNDLE_PYTHON_TESTS})
101102
endif()
102103

103-
# Serialize environment if necessary.
104-
set(ENCODED_ENVIRONMENT "")
105-
foreach(env ${_ENVIRONMENT})
106-
string(REPLACE [[\]] [\\]] env ${env})
107-
string(REPLACE [[;]] [\\;]] env ${env})
108-
list(APPEND ENCODED_ENVIRONMENT ${env})
109-
endforeach()
110-
104+
# Define file paths for generated CMake include files.
111105
set(_include_file "${CMAKE_CURRENT_BINARY_DIR}/${NAME}_include.cmake")
112106
set(_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${NAME}_tests.cmake")
113107

108+
# Create a custom target to run the tests.
114109
add_custom_target(
115110
${NAME} ALL VERBATIM
116111
BYPRODUCTS "${_tests_file}"
@@ -127,7 +122,8 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
127122
-D "STRIP_PARAM_BRACKETS=${_STRIP_PARAM_BRACKETS}"
128123
-D "INCLUDE_FILE_PATH=${_INCLUDE_FILE_PATH}"
129124
-D "WORKING_DIRECTORY=${_WORKING_DIRECTORY}"
130-
-D "ENVIRONMENT=${ENCODED_ENVIRONMENT}"
125+
-D "ENVIRONMENT=${_ENVIRONMENT}"
126+
-D "TEST_PROPERTIES=${_PROPERTIES}"
131127
-D "CTEST_FILE=${_tests_file}"
132128
-P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PytestAddTests.cmake")
133129

@@ -139,7 +135,7 @@ if (Pytest_FOUND AND NOT TARGET Pytest::Pytest)
139135
"endif()\n"
140136
)
141137

142-
# Add discovered tests to directory TEST_INCLUDE_FILES
138+
# Register the include file to be processed for tests.
143139
set_property(DIRECTORY
144140
APPEND PROPERTY TEST_INCLUDE_FILES "${_include_file}")
145141

cmake/PytestAddTests.cmake

Lines changed: 58 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,62 @@ cmake_minimum_required(VERSION 3.20...3.30)
33

44
if(CMAKE_SCRIPT_MODE_FILE)
55

6-
# Set Cmake test file to execute each test.
6+
# Initialize content for the CMake test file.
77
set(_content "")
88

9+
# Convert library and Python paths to native format.
910
cmake_path(CONVERT "${LIBRARY_PATH}" TO_NATIVE_PATH_LIST LIBRARY_PATH)
1011
cmake_path(CONVERT "${PYTHON_PATH}" TO_NATIVE_PATH_LIST PYTHON_PATH)
1112

1213
# Serialize path values separated by semicolons (required on Windows).
13-
macro(encode_value VARIABLE_NAME)
14-
string(REPLACE [[\]] [[\\]] ${VARIABLE_NAME} "${${VARIABLE_NAME}}")
15-
string(REPLACE [[;]] [[\\;]] ${VARIABLE_NAME} "${${VARIABLE_NAME}}")
16-
endmacro()
17-
18-
encode_value(LIBRARY_PATH)
19-
encode_value(PYTHON_PATH)
20-
21-
if (BUNDLE_TESTS)
14+
string(REPLACE [[;]] [[\\;]] LIBRARY_PATH "${LIBRARY_PATH}")
15+
string(REPLACE [[;]] [[\\;]] PYTHON_PATH "${PYTHON_PATH}")
16+
17+
# Set up the encoded environment with required paths.
18+
set(ENCODED_ENVIRONMENT
19+
"${LIBRARY_ENV_NAME}=${LIBRARY_PATH}"
20+
"PYTHONPATH=${PYTHON_PATH}"
21+
)
22+
23+
# Serialize additional environment variables if any are provided.
24+
foreach(env ${ENVIRONMENT})
25+
string(REPLACE [[;]] [[\\;]] env "${env}")
26+
list(APPEND ENCODED_ENVIRONMENT "${env}")
27+
endforeach()
28+
29+
# Macro to create individual tests with optional test properties.
30+
macro(create_test NAME IDENTIFIER)
2231
string(APPEND _content
23-
"add_test(\n"
24-
" \"${TEST_GROUP_NAME}\"\n"
25-
" \"${PYTEST_EXECUTABLE}\" \"${WORKING_DIRECTORY}\"\n"
26-
")\n"
27-
"set_tests_properties(\n"
28-
" \"${TEST_GROUP_NAME}\" PROPERTIES\n"
29-
" ENVIRONMENT \"${LIBRARY_ENV_NAME}=${LIBRARY_PATH}\"\n"
30-
")\n"
31-
"set_tests_properties(\n"
32-
" \"${TEST_GROUP_NAME}\"\n"
33-
" APPEND PROPERTIES\n"
34-
" ENVIRONMENT \"PYTHONPATH=${PYTHON_PATH}\"\n"
35-
")\n"
32+
"add_test(\"${NAME}\" \"${PYTEST_EXECUTABLE}\" \"${IDENTIFIER}\")\n"
3633
)
3734

38-
foreach(env ${ENVIRONMENT})
39-
string(APPEND _content
40-
"set_tests_properties(\n"
41-
" \"${TEST_GROUP_NAME}\"\n"
42-
" APPEND PROPERTIES\n"
43-
" ENVIRONMENT ${env}\n"
44-
")\n"
45-
)
35+
# Prepare the properties for the test, including the environment settings.
36+
set(args "PROPERTIES ENVIRONMENT [==[${ENCODED_ENVIRONMENT}]==]")
37+
38+
# Append any additional properties, escaping complex characters if necessary.
39+
foreach(property ${TEST_PROPERTIES})
40+
if(property MATCHES "[^-./:a-zA-Z0-9_]")
41+
string(APPEND args " [==[${property}]==]")
42+
else()
43+
string(APPEND args " ${property}")
44+
endif()
4645
endforeach()
4746

47+
# Append the test properties to the content.
48+
string(APPEND _content "set_tests_properties(\"${NAME}\" ${args})\n")
49+
endmacro()
50+
51+
# If tests are bundled together, create a single test group.
52+
if (BUNDLE_TESTS)
53+
create_test("${TEST_GROUP_NAME}" "${WORKING_DIRECTORY}")
54+
4855
else()
49-
# Set environment for collecting tests.
56+
# Set environment variables for collecting tests.
5057
set(ENV{${LIBRARY_ENV_NAME}} "${LIBRARY_PATH}")
5158
set(ENV{PYTHONPATH} "${PYTHON_PATH}")
5259
set(ENV{PYTHONWARNINGS} "ignore")
5360

61+
# Collect tests.
5462
execute_process(
5563
COMMAND "${PYTEST_EXECUTABLE}"
5664
--collect-only -q
@@ -61,91 +69,80 @@ if(CMAKE_SCRIPT_MODE_FILE)
6169
WORKING_DIRECTORY ${WORKING_DIRECTORY}
6270
)
6371

72+
# Check for errors during test collection.
6473
string(REGEX MATCH "=+ ERRORS =+(.*)" _error "${_output_lines}")
6574

6675
if (_error)
6776
message(${_error})
6877
message(FATAL_ERROR "An error occurred during the collection of Python tests.")
6978
endif()
7079

71-
# Convert output into list.
80+
# Convert the collected output into a list of lines.
7281
string(REPLACE [[;]] [[\;]] _output_lines "${_output_lines}")
7382
string(REPLACE "\n" ";" _output_lines "${_output_lines}")
7483

84+
# Regex pattern to identify pytest test identifiers.
7585
set(test_pattern "([^:]+)\.py(::([^:]+))?::([^:]+)")
7686

77-
foreach (line ${_output_lines})
87+
# Iterate through each line to identify and process tests.
88+
foreach(line ${_output_lines})
7889
string(REGEX MATCHALL ${test_pattern} matching "${line}")
7990

80-
# Ignore lines not identified as a test.
91+
# Skip lines that are not identified as tests.
8192
if (NOT matching)
8293
continue()
8394
endif()
8495

96+
# Extract file, class, and function names from the test pattern.
8597
set(_file ${CMAKE_MATCH_1})
8698
set(_class ${CMAKE_MATCH_3})
8799
set(_func ${CMAKE_MATCH_4})
88100

101+
# Optionally trim parts of the class or function name.
89102
if (TRIM_FROM_NAME)
90103
string(REGEX REPLACE "${TRIM_FROM_NAME}" "" _class "${_class}")
91104
string(REGEX REPLACE "${TRIM_FROM_NAME}" "" _func "${_func}")
92105
endif()
93106

107+
# Form the test name using class and function.
94108
if (_class)
95109
set(test_name "${_class}.${_func}")
96110
else()
97111
set(test_name "${_func}")
98112
endif()
99113

114+
# Optionally strip parameter brackets from the test name.
100115
if (STRIP_PARAM_BRACKETS)
101116
string(REGEX REPLACE "\\[(.+)\\]$" ".\\1" test_name "${test_name}")
102117
endif()
103118

119+
# Optionally include the file path in the test name.
104120
if (INCLUDE_FILE_PATH)
105121
cmake_path(CONVERT "${_file}" TO_CMAKE_PATH_LIST _file)
106122
string(REGEX REPLACE "/" "." _file "${_file}")
107123
set(test_name "${_file}.${test_name}")
108124
endif()
109125

126+
# Optionally trim parts of the full test name.
110127
if (TRIM_FROM_FULL_NAME)
111128
string(REGEX REPLACE "${TRIM_FROM_FULL_NAME}" "" test_name "${test_name}")
112129
endif()
113130

131+
# Prefix the test name with the test group name.
114132
set(test_name "${TEST_GROUP_NAME}.${test_name}")
115133
set(test_case "${WORKING_DIRECTORY}/${line}")
116134

117-
string(APPEND _content
118-
"add_test(\n"
119-
" \"${test_name}\"\n"
120-
" \"${PYTEST_EXECUTABLE}\" \"${test_case}\"\n"
121-
")\n"
122-
"set_tests_properties(\n"
123-
" \"${test_name}\" PROPERTIES\n"
124-
" ENVIRONMENT \"${LIBRARY_ENV_NAME}=${LIBRARY_PATH}\"\n"
125-
")\n"
126-
"set_tests_properties(\n"
127-
" \"${test_name}\"\n"
128-
" APPEND PROPERTIES\n"
129-
" ENVIRONMENT \"PYTHONPATH=${PYTHON_PATH}\"\n"
130-
")\n"
131-
)
132-
133-
foreach(env ${ENVIRONMENT})
134-
string(APPEND _content
135-
"set_tests_properties(\n"
136-
" \"${test_name}\"\n"
137-
" APPEND PROPERTIES\n"
138-
" ENVIRONMENT ${env}\n"
139-
")\n"
140-
)
141-
endforeach()
142-
135+
# Create the test for CTest.
136+
create_test("${test_name}" "${test_case}")
143137
endforeach()
144138

139+
# Warn if no tests were discovered.
145140
if(NOT _content)
146141
message(WARNING "No Python tests have been discovered.")
147142
endif()
148143
endif()
149144

145+
# Write the generated test content to the specified CTest file.
150146
file(WRITE ${CTEST_FILE} ${_content})
147+
151148
endif()

doc/api_reference.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ API Reference
1818
[LIBRARY_PATH_PREPEND path1 path2...]
1919
[PYTHON_PATH_PREPEND path1 path2...]
2020
[ENVIRONMENT env1 env2...]
21+
[PROPERTIES prop1 prop2...]
2122
[DEPENDS target1 target2...]
2223
[INCLUDE_FILE_PATH]
2324
[STRIP_PARAM_BRACKETS]
@@ -129,6 +130,19 @@ API Reference
129130
"ENV_VAR3=VALUE3"
130131
)
131132

133+
* ``PROPERTIES``
134+
135+
List of custom `test properties
136+
<https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#test-properties>`_
137+
to apply for all generated tests::
138+
139+
pytest_discover_tests(
140+
...
141+
PROPERTIES
142+
LABELS "python;unit"
143+
TIMEOUT 120
144+
)
145+
132146
* ``DEPENDS``
133147

134148
List of dependent targets that need to be executed before running

doc/integration.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ able to discover the newly installed configuration automatically using its
4343
option should not be set to False.
4444

4545
When using a Python virtual environment, or if Python is installed in a
46-
non-standard location, the :envvar:`Pytest_ROOT` environment variable
46+
non-standard location, the :envvar:`CMAKE_PREFIX_PATH` environment variable
4747
(or :term:`CMake` option) can be used to guide the discovery process::
4848

49-
cmake -S . -B ./build -D "Pytest_ROOT=/path/to/python/prefix"
49+
cmake -S . -B ./build -D "CMAKE_PREFIX_PATH=/path/to/python/prefix"
5050

5151
This is also necessary when installing the configuration in the
5252
`Python user directory

doc/release/release_notes.rst

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,27 @@
44
Release Notes
55
*************
66

7+
.. release:: Upcoming
8+
9+
.. change:: new
10+
11+
Added ``PROPERTIES`` option to the :func:`pytest_discover_tests`
12+
function, providing custom `test properties
13+
<https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#test-properties>`_
14+
for all generated tests.
15+
716
.. release:: 0.10.0
817
:date: 2024-10-11
918

1019
.. change:: new
1120

1221
Added ``INCLUDE_FILE_PATH`` option to the :func:`pytest_discover_tests`
13-
function use the file path to compute the test identifier.
14-
15-
.. seealso:: :ref:`tutorial/function`
22+
function, allowing the file path to be included in the test identifier.
1623

1724
.. change:: new
1825

1926
Added ``TRIM_FROM_FULL_NAME`` option to the :func:`pytest_discover_tests`
20-
function trim parts of the full test name generated.
21-
22-
.. seealso:: :ref:`tutorial/function`
27+
function, enabling parts of the full test name to be trimmed.
2328

2429
.. change:: fixed
2530

@@ -40,8 +45,6 @@ Release Notes
4045
Added ``STRIP_PARAM_BRACKETS`` option to the :func:`pytest_discover_tests`
4146
function to strip square brackets used for :term:`parametrizing tests`.
4247

43-
.. seealso:: :ref:`tutorial/function`
44-
4548
.. release:: 0.8.4
4649
:date: 2024-10-06
4750

0 commit comments

Comments
 (0)