Skip to content

Commit 3096674

Browse files
authored
Add cccl_add_xfail_compile_target_test CMake function (#6434)
* Remove note about a resolved cmake bug. The issue has been fixed. * Add `cccl_add_xfail_compile_target_test` CMake function. - CUB's usages of XFAIL compilation are updated to use the new utility. - test_device_segment_reduce_offset_type_fail test is fixed; the regexes weren't detected and some cases were failing for unintended reasons. - test_param_general_fail is added to test edge case.
1 parent a3cf118 commit 3096674

File tree

6 files changed

+214
-48
lines changed

6 files changed

+214
-48
lines changed

cmake/CCCLConfigureTarget.cmake

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ function(cccl_configure_target target_name)
2222
PROPERTIES
2323
CXX_STANDARD ${CCT_DIALECT}
2424
CUDA_STANDARD ${CCT_DIALECT}
25-
# Must manually request that the standards above are actually respected
26-
# or else CMake will silently fail to configure the targets correctly...
27-
# Note that this doesn't actually work as of CMake 3.16:
28-
# https://gitlab.kitware.com/cmake/cmake/-/issues/20953
29-
# We'll leave these properties enabled in hopes that they will someday
30-
# work.
3125
CXX_STANDARD_REQUIRED ON
3226
CUDA_STANDARD_REQUIRED ON
3327
)

cmake/CCCLUtilities.cmake

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
6767
endfunction()
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()

cub/cub/device/device_segmented_reduce.cuh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,10 +806,13 @@ struct DeviceSegmentedReduce
806806

807807
using InputValueT = detail::it_value_t<InputIteratorT>;
808808
using OutputTupleT = detail::non_void_value_t<OutputIteratorT, KeyValuePair<OffsetT, InputValueT>>;
809+
using OutputKeyT = typename OutputTupleT::Key;
809810
using OutputValueT = typename OutputTupleT::Value;
810811
using AccumT = OutputTupleT;
811812
using InitT = detail::reduce::empty_problem_init_t<AccumT>;
812813

814+
static_assert(::cuda::std::is_same_v<int, OutputKeyT>, "Output key type must be int.");
815+
813816
// Wrapped input iterator to produce index-value <OffsetT, InputT> tuples
814817
using ArgIndexInputIteratorT = ArgIndexInputIterator<InputIteratorT, OffsetT, OutputValueT>;
815818
ArgIndexInputIteratorT d_indexed_in(d_in);
@@ -921,9 +924,11 @@ struct DeviceSegmentedReduce
921924

922925
using init_t = detail::reduce::empty_problem_init_t<accum_t>;
923926

924-
// The output value type
927+
using output_key_t = typename output_tuple_t::first_type;
925928
using output_value_t = typename output_tuple_t::second_type;
926929

930+
static_assert(::cuda::std::is_same_v<int, output_key_t>, "Output key type must be int.");
931+
927932
// Wrapped input iterator to produce index-value <offset_t, InputT> tuples
928933
auto d_indexed_in = THRUST_NS_QUALIFIER::make_transform_iterator(
929934
THRUST_NS_QUALIFIER::counting_iterator<::cuda::std::int64_t>{0},
@@ -1265,8 +1270,11 @@ struct DeviceSegmentedReduce
12651270
using OutputTupleT = cub::detail::non_void_value_t<OutputIteratorT, KeyValuePair<OffsetT, InputValueT>>;
12661271
using AccumT = OutputTupleT;
12671272
using InitT = detail::reduce::empty_problem_init_t<AccumT>;
1273+
using OutputKeyT = typename OutputTupleT::Key;
12681274
using OutputValueT = typename OutputTupleT::Value;
12691275

1276+
static_assert(::cuda::std::is_same_v<int, OutputKeyT>, "Output key type must be int.");
1277+
12701278
// Wrapped input iterator to produce index-value <OffsetT, InputT> tuples
12711279
using ArgIndexInputIteratorT = ArgIndexInputIterator<InputIteratorT, OffsetT, OutputValueT>;
12721280
ArgIndexInputIteratorT d_indexed_in(d_in);
@@ -1375,8 +1383,11 @@ struct DeviceSegmentedReduce
13751383
using output_tuple_t = detail::non_void_value_t<OutputIteratorT, ::cuda::std::pair<input_t, input_value_t>>;
13761384
using accum_t = output_tuple_t;
13771385
using init_t = detail::reduce::empty_problem_init_t<accum_t>;
1386+
using output_key_t = typename output_tuple_t::first_type;
13781387
using output_value_t = typename output_tuple_t::second_type;
13791388

1389+
static_assert(::cuda::std::is_same_v<int, output_key_t>, "Output key type must be int.");
1390+
13801391
// Wrapped input iterator to produce index-value <input_t, InputT> tuples
13811392
auto d_indexed_in = THRUST_NS_QUALIFIER::make_transform_iterator(
13821393
THRUST_NS_QUALIFIER::counting_iterator<::cuda::std::int64_t>{0},

cub/test/CMakeLists.txt

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -175,32 +175,12 @@ function(cub_add_test target_name_var test_name test_src cub_target launcher_id)
175175

176176
_cub_is_fail_test(is_fail_test "${test_src}")
177177
if (is_fail_test)
178-
set_target_properties(${test_target} PROPERTIES EXCLUDE_FROM_ALL true
179-
EXCLUDE_FROM_DEFAULT_BUILD true)
180-
add_test(NAME ${test_target}
181-
COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}"
182-
--target ${test_target}
183-
--config $<CONFIGURATION>)
184-
string(REGEX MATCH "err_([0-9]+)" MATCH_RESULT "${test_name}")
185-
file(READ ${test_src} test_content)
186-
if(MATCH_RESULT)
187-
string(REGEX MATCH "// expected-error-${CMAKE_MATCH_1}+ {{\"([^\"]+)\"}}" expected_errors_matches ${test_content})
188-
189-
if (expected_errors_matches)
190-
set_tests_properties(${test_target} PROPERTIES PASS_REGULAR_EXPRESSION "${CMAKE_MATCH_1}")
191-
else()
192-
set_tests_properties(${test_target} PROPERTIES WILL_FAIL true)
193-
endif()
194-
else()
195-
string(REGEX MATCH "// expected-error {{\"([^\"]+)\"}}" expected_errors_matches ${test_content})
196-
197-
if (expected_errors_matches)
198-
set_tests_properties(${test_target} PROPERTIES PASS_REGULAR_EXPRESSION "${CMAKE_MATCH_1}")
199-
else()
200-
set_tests_properties(${test_target} PROPERTIES WILL_FAIL true)
201-
endif()
202-
endif()
203-
else() # Not fail test:
178+
cccl_add_xfail_compile_target_test(${test_target}
179+
SOURCE_FILE "${test_src}"
180+
ERROR_REGEX_LABEL "expected-error"
181+
ERROR_NUMBER_TARGET_NAME_REGEX "\\.err_([0-9]+)"
182+
)
183+
else()
204184
# Add to the active configuration's meta target
205185
add_dependencies(${config_meta_target} ${test_target})
206186

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,55 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
22
// SPDX-License-Identifier: BSD-3
33

4-
// %PARAM% TEST_ERR err 0:1:2:3:4:5
4+
// %PARAM% TEST_ERR err 0:1:2:3:4:5:6:7
55

66
#include <cub/device/device_segmented_reduce.cuh>
77

8+
template <typename T>
9+
void mark_as_used(T&&)
10+
{}
11+
812
int main()
913
{
1014
using offset_t = float; // error
1115
// using offset_t = int; // ok
1216
float *d_in{}, *d_out{};
17+
cub::KeyValuePair<float, float>* d_kv_out{};
18+
::cuda::std::pair<float, float>* d_pair_out{};
1319
offset_t* d_offsets{};
1420
std::size_t temp_storage_bytes{};
1521
std::uint8_t* d_temp_storage{};
1622

23+
// Only one of these is used per path, suppress undesired diagnostics:
24+
mark_as_used(d_out);
25+
mark_as_used(d_kv_out);
26+
mark_as_used(d_pair_out);
27+
mark_as_used(d_offsets);
28+
1729
#if TEST_ERR == 0
18-
// expected-error {{"Offset iterator type should be integral."}}
30+
// expected-error-0 {{"Offset iterator value type should be integral."}}
1931
cub::DeviceSegmentedReduce::Reduce(
2032
d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1, ::cuda::minimum<>{}, 0);
21-
2233
#elif TEST_ERR == 1
23-
// expected-error {{"Offset iterator type should be integral."}}
34+
// expected-error-1 {{"Offset iterator value type should be integral."}}
2435
cub::DeviceSegmentedReduce::Sum(d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1);
25-
2636
#elif TEST_ERR == 2
27-
// expected-error {{"Offset iterator type should be integral."}}
37+
// expected-error-2 {{"Offset iterator value type should be integral."}}
2838
cub::DeviceSegmentedReduce::Min(d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1);
29-
3039
#elif TEST_ERR == 3
31-
// expected-error {{"Offset iterator type should be integral."}}
32-
cub::DeviceSegmentedReduce::ArgMin(d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1);
33-
34-
#elif TEST_ERR == 4
35-
// expected-error {{"Offset iterator type should be integral."}}
40+
// expected-error-3 {{"Offset iterator value type should be integral."}}
3641
cub::DeviceSegmentedReduce::Max(d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1);
37-
42+
#elif TEST_ERR == 4
43+
// expected-error-4 {{"Output key type must be int."}}
44+
cub::DeviceSegmentedReduce::ArgMin(d_temp_storage, temp_storage_bytes, d_in, d_kv_out, 0, d_offsets, d_offsets + 1);
3845
#elif TEST_ERR == 5
39-
// expected-error {{"Offset iterator type should be integral."}}
40-
cub::DeviceSegmentedReduce::ArgMax(d_temp_storage, temp_storage_bytes, d_in, d_out, 0, d_offsets, d_offsets + 1);
46+
// expected-error-5 {{"Output key type must be int."}}
47+
cub::DeviceSegmentedReduce::ArgMin(d_temp_storage, temp_storage_bytes, d_in, d_pair_out, 0, 1);
48+
#elif TEST_ERR == 6
49+
// expected-error-6 {{"Output key type must be int."}}
50+
cub::DeviceSegmentedReduce::ArgMax(d_temp_storage, temp_storage_bytes, d_in, d_kv_out, 0, d_offsets, d_offsets + 1);
51+
#elif TEST_ERR == 7
52+
// expected-error-7 {{"Output key type must be int."}}
53+
cub::DeviceSegmentedReduce::ArgMax(d_temp_storage, temp_storage_bytes, d_in, d_pair_out, 0, 1);
4154
#endif
4255
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// %PARAM% TEST_ERR err 0:1:2:3
2+
3+
// This compilation sometimes passes, sometimes fails.
4+
// It's role is to ensure that exit code is not checked for regex matches and binary objects are cleaned
5+
// before each test run.
6+
// This allows the failure machinery to test for non-fatal warnings.
7+
int main()
8+
{
9+
// Used if not specified otherwise:
10+
// expected-error {{"fail generic"}}
11+
12+
#if TEST_ERR == 0
13+
# pragma message "fail zero" // expected-error-0 {{"fail zero"}}
14+
#elif TEST_ERR == 1
15+
# pragma message "fail generic"
16+
#elif TEST_ERR == 2
17+
static_assert(false, "fail two"); // expected-error-2 {{"fail two"}}
18+
#elif TEST_ERR == 3
19+
static_assert(false, "fail generic");
20+
#endif
21+
}

0 commit comments

Comments
 (0)