@@ -65,3 +65,150 @@ function(cccl_add_compile_test full_test_name_var name_prefix subdir test_id)
6565 )
6666 set (${full_test_name_var} ${test_name} PARENT_SCOPE)
6767endfunction ()
68+
69+ # cccl_add_xfail_compile_target_test(
70+ # <target_name>
71+ # [TEST_NAME <test_name>]
72+ # [ERROR_REGEX <regex>]
73+ # [SOURCE_FILE <source_file>]
74+ # [ERROR_REGEX_LABEL <error_string>]
75+ # [ERROR_NUMBER <error_number>]
76+ # [ERROR_NUMBER_TARGET_NAME_REGEX <regex>]
77+ # )
78+ #
79+ # Given a configured build target that is expected to fail to compile:
80+ # - Mark the target as excluded from the `all` target.
81+ # - Create a CTest test that compiles the target. If TEST_NAME is provided, it is used.
82+ # Otherwise, the target_name is used as the test name.
83+ # - When the test runs, it passes if exactly one of the following conditions is met:
84+ # - A provided / detected error regex matches the compilation output, ignoring exit code.
85+ # - No error regex is provided / detected, and the compilation fails.
86+ #
87+ # An error regex may be explicitly provided via ERROR_REGEX, or it may be
88+ # detected by scanning the SOURCE_FILE for a specially formatted comment.
89+ #
90+ # If ERROR_REGEX_LABEL is provided, the SOURCE_FILE will read, looking for a comment of the form:
91+ #
92+ # // <ERROR_REGEX_LABEL> {{"error_regex"}}
93+ #
94+ # An error number may be appended to the ERROR_REGEX_LABEL in the comment:
95+ #
96+ # // <ERROR_REGEX_LABEL>-<error_number> {{"error_regex"}}
97+ #
98+ # If ERROR_NUMBER_TARGET_NAME_REGEX is specified, the regex is used to capture
99+ # the error_number from the target name. If target_name is
100+ # "cccl.test.my_test.err_5.foo_3" and ERROR_NUMBER_TARGET_NAME_REGEX is
101+ # "\\.err_([0-9]+)", the captured error number "5."
102+ #
103+ # // <ERROR_REGEX_LABEL>-<captured_error_number> {{"error_regex"}}
104+ #
105+ # If ERROR_NUMBER is provided, ERROR_NUMBER_TARGET_NAME_REGEX is ignored.
106+ # If ERROR_NUMBER_TARGET_NAME_REGEX is provided but does not match, a plain ERROR_REGEX_LABEL is used.
107+ #
108+ # If both SOURCE_FILE and ERROR_REGEX_LABEL are provided, the source file will be added to the
109+ # current directory's CMAKE_CONFIGURE_DEPENDS to ensure that changes to the file will re-trigger CMake.
110+ function (cccl_add_xfail_compile_target_test target_name)
111+ set (options )
112+ set (oneValueArgs
113+ ERROR_REGEX
114+ SOURCE_FILE
115+ ERROR_REGEX_LABEL
116+ ERROR_NUMBER
117+ ERROR_NUMBER_TARGET_NAME_REGEX
118+ )
119+ set (multiValueArgs)
120+ cmake_parse_arguments (cccl_xfail "${options} " "${oneValueArgs} " "${multiValueArgs} " ${ARGN} )
121+
122+ if (cccl_xfail_UNPARSED_ARGUMENTS)
123+ message (FATAL_ERROR "Unparsed arguments: ${cccl_xfail_UNPARSED_ARGUMENTS} " )
124+ endif ()
125+
126+ set (test_name "${target_name} " )
127+ if (DEFINED cccl_xfail_TEST_NAME)
128+ set (test_name "${cccl_xfail_TEST_NAME} " )
129+ endif ()
130+
131+ set (regex )
132+ if (DEFINED cccl_xfail_ERROR_REGEX)
133+ set (regex "${cccl_xfail_ERROR_REGEX} " )
134+ elseif (DEFINED cccl_xfail_SOURCE_FILE AND DEFINED cccl_xfail_ERROR_REGEX_LABEL)
135+ get_filename_component (src_absolute "${cccl_xfail_SOURCE_FILE} " ABSOLUTE )
136+ set (error_label_regex "${cccl_xfail_ERROR_REGEX_LABEL} " )
137+
138+ # Cache all error label matches (with and without error numbers) as global properties.
139+ # This avoids re-reading and re-parsing the source file multiple times if multiple
140+ # tests are added for the same source file. Properties are used instead of cache variables
141+ # to ensure that the source is not cached in between CMake executions.
142+ string (MD5 source_filename_md5 "${src_absolute} " )
143+ set (error_cache_property "_cccl_xfail_error_cache_${source_filename_md5} " )
144+ get_property (error_cache_set GLOBAL PROPERTY "${error_cache_property} " SET)
145+ if (error_cache_set)
146+ get_property (error_cache GLOBAL PROPERTY "${error_cache_property} " )
147+ else ()
148+ file (READ "${src_absolute} " source_contents)
149+ string (REGEX MATCHALL "//[ \t ]*${error_label_regex} (-[0-9]+)?[ \t ]*{{\" ([^\" ]+)\" }}" error_cache "${source_contents} " )
150+ set_property (GLOBAL PROPERTY "${error_cache_property} " "${error_cache} " )
151+ endif ()
152+
153+ # Changes to the source file should re-run CMake to pick-up new error specs:
154+ set_property (DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${src_absolute} " )
155+
156+ set (error_number)
157+ if (DEFINED cccl_xfail_ERROR_NUMBER)
158+ set (error_number "${cccl_xfail_ERROR_NUMBER} " )
159+ elseif (DEFINED cccl_xfail_ERROR_NUMBER_TARGET_NAME_REGEX)
160+ string (REGEX MATCH "${cccl_xfail_ERROR_NUMBER_TARGET_NAME_REGEX} " matched ${target_name} )
161+ if (matched)
162+ set (error_number "${CMAKE_MATCH_1} " )
163+ endif ()
164+ endif ()
165+
166+ # Look for a labeled error with the specific error number.
167+ if (NOT "${error_number} " STREQUAL "" ) # Check strings to allow "0"
168+ string (REGEX MATCH "//[ \t ]*${error_label_regex} -${error_number} [ \t ]*{{\" ([^\" ]+)\" }}" matched "${error_cache} " )
169+ if (matched)
170+ set (regex "${CMAKE_MATCH_1} " )
171+ endif ()
172+ endif ()
173+
174+ if (NOT regex )
175+ # Look for a labeled error without an error number.
176+ string (REGEX MATCH "//[ \t ]*${error_label_regex} [ \t ]*{{\" ([^\" ]+)\" }}" matched "${error_cache} " )
177+ if (matched)
178+ set (regex "${CMAKE_MATCH_1} " )
179+ endif ()
180+ endif ()
181+ endif ()
182+
183+ message (VERBOSE "CCCL: Adding XFAIL test: ${test_name} " )
184+ if (regex )
185+ message (VERBOSE "CCCL: with expected regex: '${regex} '" )
186+ endif ()
187+
188+ set_target_properties (${test_target} PROPERTIES EXCLUDE_FROM_ALL true )
189+
190+ # The same target may be reused for multiple tests, and the output file
191+ # may exist if using a regex to check for warnings. Add a setup fixture to
192+ # delete the output file before each test run.
193+ if (NOT TEST ${target_name} .clean)
194+ add_test (NAME ${target_name} .clean COMMAND "${CMAKE_COMMAND} " -E rm -f
195+ "$<TARGET_FILE:${target_name} >"
196+ "$<TARGET_OBJECTS:${target_name} >"
197+ )
198+ set_tests_properties (${test_name} .clean PROPERTIES FIXTURES_SETUP ${target_name} .clean)
199+ endif ()
200+
201+ add_test (NAME ${test_name}
202+ COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR} "
203+ --target ${test_target}
204+ --config $<CONFIGURATION >
205+ )
206+ set_tests_properties (${test_name} PROPERTIES FIXTURES_CLEANUP ${target_name} .clean)
207+
208+ if (regex )
209+ set_tests_properties (${test_name} PROPERTIES PASS_REGULAR_EXPRESSION "${regex} " )
210+ else ()
211+ set_tests_properties (${test_name} PROPERTIES WILL_FAIL true )
212+ endif ()
213+
214+ endfunction ()
0 commit comments