Sourcemeta Core 0.0.0
Loading...
Searching...
No Matches
test.h
1#ifndef SOURCEMETA_CORE_TEST_H_
2#define SOURCEMETA_CORE_TEST_H_
3
4#ifndef SOURCEMETA_CORE_TEST_EXPORT
5#include <sourcemeta/core/test_export.h>
6#endif
7
8#include <sourcemeta/core/numeric_util.h>
9
10#include <functional> // std::function
11#include <ostream> // std::ostream
12#include <source_location> // std::source_location
13#include <sstream> // std::ostringstream
14#include <string> // std::string
15#include <string_view> // std::string_view
16#include <type_traits> // std::is_integral_v, std::is_same_v, std::remove_cv_t, std::is_convertible_v, std::is_pointer_v, std::remove_cvref_t
17#include <utility> // std::cmp_equal, std::cmp_not_equal, std::cmp_less, std::cmp_greater, std::cmp_less_equal, std::cmp_greater_equal
18
27
28namespace sourcemeta::core {
29
30// Exporting symbols that depends on the standard C++ library is considered
31// safe.
32// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN
33#if defined(_MSC_VER)
34#pragma warning(push)
35#pragma warning(disable : 4251 4275)
36#endif
37
43struct SOURCEMETA_CORE_TEST_EXPORT TestAbortError {
45 std::string message;
46};
47
52SOURCEMETA_CORE_TEST_EXPORT
53auto test_register(std::string_view suite, std::string_view name,
54 std::string_view file, int line, std::function<void()> body)
55 -> int;
56
61SOURCEMETA_CORE_TEST_EXPORT
63 std::string_view name, std::function<void()> body,
64 std::source_location location = std::source_location::current()) -> int;
65
69[[noreturn]] SOURCEMETA_CORE_TEST_EXPORT auto
70test_report_failure(std::string_view file, int line, std::string_view message)
71 -> void;
72
77SOURCEMETA_CORE_TEST_EXPORT
78auto test_suite_from_path(std::string_view path) -> std::string;
79
84SOURCEMETA_CORE_TEST_EXPORT
85auto test_run(int argc, char **argv) -> int;
86
87template <typename Type>
89 std::is_integral_v<Type> && !std::is_same_v<std::remove_cv_t<Type>, bool> &&
90 !std::is_same_v<std::remove_cv_t<Type>, char> &&
91 !std::is_same_v<std::remove_cv_t<Type>, wchar_t> &&
92 !std::is_same_v<std::remove_cv_t<Type>, char8_t> &&
93 !std::is_same_v<std::remove_cv_t<Type>, char16_t> &&
94 !std::is_same_v<std::remove_cv_t<Type>, char32_t>;
95
96template <typename Left, typename Right>
97auto test_compare_equal(const Left &left, const Right &right) -> bool {
98 if constexpr (test_comparable_integer<Left> &&
100 return std::cmp_equal(left, right);
101 } else {
102 // Character types reach this branch, and comparing whatever the caller
103 // passed is the whole point of the helper
104 // NOLINTNEXTLINE(bugprone-signed-char-misuse)
105 return left == right;
106 }
107}
108
109template <typename Left, typename Right>
110auto test_compare_not_equal(const Left &left, const Right &right) -> bool {
111 if constexpr (test_comparable_integer<Left> &&
113 return std::cmp_not_equal(left, right);
114 } else {
115 return left != right;
116 }
117}
118
119template <typename Left, typename Right>
120auto test_compare_less(const Left &left, const Right &right) -> bool {
121 if constexpr (test_comparable_integer<Left> &&
123 return std::cmp_less(left, right);
124 } else {
125 return left < right;
126 }
127}
128
129template <typename Left, typename Right>
130auto test_compare_greater(const Left &left, const Right &right) -> bool {
131 if constexpr (test_comparable_integer<Left> &&
133 return std::cmp_greater(left, right);
134 } else {
135 return left > right;
136 }
137}
138
139template <typename Left, typename Right>
140auto test_compare_less_equal(const Left &left, const Right &right) -> bool {
141 if constexpr (test_comparable_integer<Left> &&
143 return std::cmp_less_equal(left, right);
144 } else {
145 return left <= right;
146 }
147}
148
149template <typename Left, typename Right>
150auto test_compare_greater_equal(const Left &left, const Right &right) -> bool {
151 if constexpr (test_comparable_integer<Left> &&
153 return std::cmp_greater_equal(left, right);
154 } else {
155 return left >= right;
156 }
157}
158
159template <typename Type>
161 requires(std::ostream &stream, const Type &value) { stream << value; };
162
163// A pointer is not taken as text, as it may be null and as comparing two of
164// them compares addresses rather than the characters they point to
165template <typename Type>
167 test_streamable<Type> && !std::is_pointer_v<std::remove_cvref_t<Type>> &&
168 std::is_convertible_v<const Type &, std::string_view>;
169
170template <typename Type> auto test_stringify(const Type &value) -> std::string {
171 if constexpr (test_streamable<Type>) {
172 std::ostringstream stream;
173 stream << value;
174 return stream.str();
175 } else {
176 return "<unprintable>";
177 }
178}
179
184SOURCEMETA_CORE_TEST_EXPORT
185auto test_stringify_difference(std::string_view actual,
186 std::string_view expected) -> std::string;
187
188template <typename Left, typename Right>
189auto test_describe_mismatch(std::string_view expression, const Left &left,
190 const Right &right) -> std::string {
192 const auto difference{test_stringify_difference(left, right)};
193 if (!difference.empty()) {
194 return std::string{"expected "} + std::string{expression} + "\n" +
195 difference;
196 }
197 }
198
199 return std::string{"expected "} + std::string{expression} +
200 "\n actual: " + test_stringify(left) +
201 "\n expected: " + test_stringify(right);
202}
203
204inline auto test_c_string_equal(const char *const left, const char *const right)
205 -> bool {
206 // A null pointer is not a valid string to view, so compare pointers directly
207 if (left == nullptr || right == nullptr) {
208 return left == right;
209 }
210
211 return std::string_view{left} == std::string_view{right};
212}
213
214inline auto test_c_string_label(const char *const value) -> std::string_view {
215 return value == nullptr ? std::string_view{"(null)"}
216 : std::string_view{value};
217}
218
219// References, rather than values, so that the operands bind without copying and
220// without rejecting comparisons of non-copyable types. This aggregate only ever
221// lives for the duration of a single comparison call, so the usual hazards of
222// reference members do not apply here.
224template <typename Left, typename Right> struct TestOperands {
225 // NOLINTBEGIN(cppcoreguidelines-avoid-const-or-ref-data-members)
227 const Left &left;
229 const Right &right;
230 // NOLINTEND(cppcoreguidelines-avoid-const-or-ref-data-members)
231};
232
233// Collecting both operands through a single braced initializer keeps their
234// temporaries alive for the whole comparison and pins their evaluation to
235// left-to-right order. Binding each operand to its own reference inside the
236// macro guarantees neither
237template <typename Left, typename Right, typename Comparator>
238auto test_expect_comparison(std::string_view file, int line,
239 std::string_view expression,
240 const TestOperands<Left, Right> &operands,
241 Comparator comparator) -> void {
242 if (!comparator(operands.left, operands.right)) {
244 file, line,
245 test_describe_mismatch(expression, operands.left, operands.right));
246 }
247}
248
249#if defined(_MSC_VER)
250#pragma warning(pop)
251#endif
252
253} // namespace sourcemeta::core
254
255// The registration symbol is a namespace-scope object whose initializer runs
256// the registry call, which no static initialization check can prove
257// non-throwing
258// NOLINTBEGIN(cert-err58-cpp,bugprone-throwing-static-initialization)
259#define SOURCEMETA_CORE_TEST_REGISTER(name) \
260 static auto sourcemeta_test_body_##name()->void; \
261 [[maybe_unused]] static const int sourcemeta_test_registration_##name = \
262 ::sourcemeta::core::test_register( \
263 ::sourcemeta::core::test_suite_from_path(__FILE__), #name, __FILE__, \
264 __LINE__, &sourcemeta_test_body_##name); \
265 static auto sourcemeta_test_body_##name()->void
266
267#define TEST(name) SOURCEMETA_CORE_TEST_REGISTER(name)
268
269// The fixture name is used as a base class and in token pasting, neither of
270// which can be parenthesized. The registration symbol is keyed on the test name
271// alone so that reusing a name within a file is a redefinition error, matching
272// the behavior of a plain test.
273// NOLINTBEGIN(bugprone-macro-parentheses)
274#define SOURCEMETA_CORE_TEST_REGISTER_FIXTURE(fixture, name) \
275 namespace { \
276 struct sourcemeta_test_fixture_##fixture##_##name final : public fixture { \
277 auto sourcemeta_test_body() -> void; \
278 }; \
279 } \
280 [[maybe_unused]] static const int sourcemeta_test_registration_##name = \
281 ::sourcemeta::core::test_register( \
282 ::sourcemeta::core::test_suite_from_path(__FILE__), #name, __FILE__, \
283 __LINE__, [] { \
284 sourcemeta_test_fixture_##fixture##_##name instance; \
285 instance.sourcemeta_test_body(); \
286 }); \
287 auto sourcemeta_test_fixture_##fixture##_##name::sourcemeta_test_body()->void
288// NOLINTEND(bugprone-macro-parentheses)
289
290#define TEST_F(fixture, name) \
291 SOURCEMETA_CORE_TEST_REGISTER_FIXTURE(fixture, name)
292// NOLINTEND(cert-err58-cpp,bugprone-throwing-static-initialization)
293
294#define SOURCEMETA_CORE_TEST_COMPARE(actual, expected, comparator, operation) \
295 ::sourcemeta::core::test_expect_comparison( \
296 __FILE__, __LINE__, #actual " " operation " " #expected, \
297 ::sourcemeta::core::TestOperands{(actual), (expected)}, \
298 [](const auto &sourcemeta_test_left, \
299 const auto &sourcemeta_test_right) { \
300 return ::sourcemeta::core::comparator(sourcemeta_test_left, \
301 sourcemeta_test_right); \
302 })
303
304#define EXPECT_EQ(actual, expected) \
305 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_equal, "==")
306#define EXPECT_NE(actual, expected) \
307 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_not_equal, "!=")
308#define EXPECT_LT(actual, expected) \
309 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_less, "<")
310#define EXPECT_GT(actual, expected) \
311 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_greater, ">")
312#define EXPECT_LE(actual, expected) \
313 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_less_equal, "<=")
314#define EXPECT_GE(actual, expected) \
315 SOURCEMETA_CORE_TEST_COMPARE(actual, expected, test_compare_greater_equal, \
316 ">=")
317
318// The assertion macros wrap their body in a loop so that they expand into a
319// single statement that still requires a trailing semicolon at the call site
320// NOLINTBEGIN(cppcoreguidelines-avoid-do-while)
321#define SOURCEMETA_CORE_TEST_COMPARE_FLOATING(actual, expected, type) \
322 do { \
323 const type sourcemeta_test_actual{static_cast<type>(actual)}; \
324 const type sourcemeta_test_expected{static_cast<type>(expected)}; \
325 if (!::sourcemeta::core::real_equal(sourcemeta_test_actual, \
326 sourcemeta_test_expected)) { \
327 ::sourcemeta::core::test_report_failure( \
328 __FILE__, __LINE__, \
329 ::sourcemeta::core::test_describe_mismatch( \
330 #actual " ~= " #expected, sourcemeta_test_actual, \
331 sourcemeta_test_expected)); \
332 } \
333 } while (false)
334
335#define EXPECT_DOUBLE_EQ(actual, expected) \
336 SOURCEMETA_CORE_TEST_COMPARE_FLOATING(actual, expected, double)
337#define EXPECT_FLOAT_EQ(actual, expected) \
338 SOURCEMETA_CORE_TEST_COMPARE_FLOATING(actual, expected, float)
339
340#define EXPECT_STREQ(actual, expected) \
341 do { \
342 const char *const sourcemeta_test_actual{(actual)}; \
343 const char *const sourcemeta_test_expected{(expected)}; \
344 if (!::sourcemeta::core::test_c_string_equal(sourcemeta_test_actual, \
345 sourcemeta_test_expected)) { \
346 ::sourcemeta::core::test_report_failure( \
347 __FILE__, __LINE__, \
348 ::sourcemeta::core::test_describe_mismatch( \
349 #actual " == " #expected, \
350 ::sourcemeta::core::test_c_string_label(sourcemeta_test_actual), \
351 ::sourcemeta::core::test_c_string_label( \
352 sourcemeta_test_expected))); \
353 } \
354 } while (false)
355
356#define EXPECT_TRUE(condition) \
357 do { \
358 if (!(condition)) { \
359 ::sourcemeta::core::test_report_failure(__FILE__, __LINE__, \
360 "expected true: " #condition); \
361 } \
362 } while (false)
363
364#define EXPECT_FALSE(condition) \
365 do { \
366 if ((condition)) { \
367 ::sourcemeta::core::test_report_failure(__FILE__, __LINE__, \
368 "expected false: " #condition); \
369 } \
370 } while (false)
371
372// NOLINTEND(cppcoreguidelines-avoid-do-while)
373
374#define FAIL() \
375 ::sourcemeta::core::test_report_failure(__FILE__, __LINE__, \
376 "explicit failure")
377
378#endif
@ Type
The token type does not match the expected media type.
Definition jose_verify.h:216
std::string message
The message describing the failed expectation.
Definition test.h:45
SOURCEMETA_CORE_TEST_EXPORT auto test_register(std::string_view suite, std::string_view name, std::string_view file, int line, std::function< void()> body) -> int
SOURCEMETA_CORE_TEST_EXPORT auto test_run(int argc, char **argv) -> int
SOURCEMETA_CORE_TEST_EXPORT auto test_suite_from_path(std::string_view path) -> std::string
SOURCEMETA_CORE_TEST_EXPORT auto test_report_failure(std::string_view file, int line, std::string_view message) -> void
SOURCEMETA_CORE_TEST_EXPORT auto test_stringify_difference(std::string_view actual, std::string_view expected) -> std::string
A pair of operands captured by reference for one comparison.
Definition test.h:224
const Right & right
The right-hand operand.
Definition test.h:229
const Left & left
The left-hand operand.
Definition test.h:227