HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
hikotest.hpp
1
2
3#include <concepts>
4#include <utility>
5#include <type_traits>
6#include <functional>
7#include <string>
8#include <string_view>
9#include <format>
10#include <vector>
11#include <chrono>
12#include <compare>
13#include <ranges>
14#include <algorithm>
15#include <numeric>
16#include <cassert>
17#include <limits>
18#include <print>
19#include <expected>
20#include <optional>
21#include <stdexcept>
22
23namespace test {
24
33#define TEST_SUITE(id) \
34 struct id; \
35 inline auto _hikotest_registered_##id = std::addressof(::test::register_suite<id>()); \
36 struct id : ::test::suite<id>
37
46#define TEST_CASE(id) \
47 void _hikotest_wrap_##id() \
48 { \
49 return id(); \
50 } \
51 inline static auto _hikotest_registered_##id = \
52 std::addressof(::test::register_test(&_hikotest_suite_type::_hikotest_wrap_##id, __FILE__, __LINE__, #id)); \
53 void id()
54
61#define REQUIRE(expression, ...) ::test::require(__FILE__, __LINE__, expression <=> ::test::error{__VA_ARGS__})
62
68#define REQUIRE_THROWS(expression, exception) \
69 do { \
70 bool _hikotest_throws = false; \
71 try { \
72 (void)expression; \
73 } catch (exception const&) { \
74 _hikotest_throws = true; \
75 } \
76 if (not _hikotest_throws) { \
77 ::test::require(__FILE__, __LINE__, std::unexpected{std::string{#expression " did not throw " #exception "."}}); \
78 } \
79 } while (false)
80
86#if defined(_MSC_VER)
87#define TEST_FORCE_INLINE __forceinline
88#elif defined(__GNUC__)
89#define TEST_FORCE_INLINE __attribute__((always_inline))
90#else
91#error "TEST_FORCE_INLINE not implemented"
92#endif
93
98#if defined(_MSC_VER)
99#define TEST_BREAKPOINT() __debugbreak()
100#elif defined(__GNUC__)
101#define TEST_BREAKPOINT() __builtin_debugtrap()
102#else
103#error "BREAKPOINT not implemented"
104#endif
105
106using hr_clock_type = std::chrono::high_resolution_clock;
107using hr_duration_type = std::chrono::duration<double>;
108using hr_time_point_type = std::chrono::time_point<hr_clock_type>;
109using utc_clock_type = std::chrono::utc_clock;
110using utc_time_point_type = utc_clock_type::time_point;
111
118inline bool break_on_failure = false;
119
127[[nodiscard]] std::string type_name_strip(std::string type);
128
134template<typename T>
135[[nodiscard]] std::string type_name() noexcept
136{
137#if defined(_MSC_VER)
138 auto signature = std::string_view{__FUNCSIG__};
139#elif defined(__GNUC__)
140 auto signature = std::string_view{__PRETTY_FUNCTION__};
141#else
142#error "type_name() not implemented"
143#endif
144
145 // constexpr std::string class_name() [with T = foo<bar>; std::string_view = std::basic_string_view<char>]
146 if (auto first = signature.find("::type_name() [with T = "); first != signature.npos) {
147 first += 24;
148 auto const last = signature.find("; ", first);
149 if (last == signature.npos) {
150 std::println(stderr, "{}({}): error: Could not parse type_name from '{}'", __FILE__, __LINE__, signature);
152 }
153 return type_name_strip(std::string{signature.substr(first, last - first)});
154 }
155
156 // __cdecltest::type_name(void)[T=foo<bar>]
157 if (auto first = signature.find("::type_name(void) [T = "); first != signature.npos) {
158 first += 23;
159 auto const last = signature.find("]", first);
160 if (last == signature.npos) {
161 std::println(stderr, "{}({}): error: Could not parse type_name from '{}'", __FILE__, __LINE__, signature);
163 }
164 return type_name_strip(std::string{signature.substr(first, last - first)});
165 }
166
167 // class std::basic_string<char,struct std::char_traits<char> > __cdecl test::type_name<struct foo<struct bar>>(void) noexcept
168 if (auto first = signature.find("::type_name<"); first != signature.npos) {
169 first += 12;
170 auto const last = signature.rfind(">(void)");
171 return type_name_strip(std::string{signature.substr(first, last - first)});
172 }
173
174 std::println(stderr, "{}({}): error: Could not parse type_name from '{}'", __FILE__, __LINE__, signature);
176}
177
183template<typename Arg>
184[[nodiscard]] std::string operand_to_string(Arg const& arg) noexcept
185{
186 std::stringbuf buf;
187
188 if constexpr (requires { std::formatter<Arg, char>{}; }) {
189 return std::format("{}", arg);
190
191 } else if constexpr (requires { buf << arg; }) {
192 buf << arg;
193 return std::move(buf).str();
194
195 } else if constexpr (requires { to_string(arg); }) {
196 return to_string(arg);
197
198 } else if constexpr (requires { arg.string(); }) {
199 return arg.string();
200
201 } else if constexpr (requires { arg.str(); }) {
202 return arg.str();
203
204 } else {
205 std::array<std::byte, sizeof(Arg)> bytes;
206 std::memcpy(bytes.data(), std::addressof(arg), sizeof(Arg));
207
208 auto r = std::string{};
209 for (auto const byte : bytes) {
210 if (r.empty()) {
211 r += std::format("<{:02x}", std::to_underlying(byte));
212 } else {
213 r += std::format(" {:02x}", std::to_underlying(byte));
214 }
215 }
216 return r + '>';
217 }
218}
219
222enum class error_class {
225 exact,
226
229 absolute,
230
233 relative
234};
235
243template<error_class ErrorClass = error_class::exact>
244struct error {
245 double v = 0.0;
246
249 [[nodiscard]] constexpr double operator+() const noexcept
250 {
251 return v;
252 }
253
256 [[nodiscard]] constexpr double operator-() const noexcept
257 {
258 return -v;
259 }
260};
261
264error(double) -> error<error_class::absolute>;
265
273template<error_class ErrorClass, typename T>
274struct operand {
275 using value_type = T;
276
278 value_type const& v;
279
280 operand(error<ErrorClass> error, value_type const &value) noexcept : e(error), v(value) {}
281
282};
283
290template<>
291struct operand<error_class::exact, bool> {
292 using value_type = bool;
293
294 value_type const& v;
295
296 operand(error<error_class::exact>, value_type const &value) noexcept : v(value) {}
297
302 operator std::expected<void, std::string>() const noexcept
303 {
304 if (v) {
305 return {};
306 } else {
307 return std::unexpected{std::string{"expression was false"}};
308 }
309 }
310};
311
312
328template<typename RHS, error_class ErrorClass>
329[[nodiscard]] constexpr operand<ErrorClass, RHS> operator<=>(RHS const& rhs, error<ErrorClass> e) noexcept
330{
331 return {e, rhs};
332}
333
334template<typename LHS, typename RHS>
335[[nodiscard]] constexpr std::expected<void, std::string>
336operator==(LHS const& lhs, operand<error_class::exact, RHS> const& rhs) noexcept
337{
338 // clang-format off
339 if constexpr (requires { { lhs == rhs.v } -> std::convertible_to<bool>; }) {
340 if (lhs == rhs.v) {
341 return {};
342 } else {
343 return std::unexpected{
344 std::format("Expected equality of these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
345 }
346
347 } else if constexpr (requires { std::equal_to<std::common_type_t<LHS, RHS>>{}(lhs, rhs.v); }) {
348 if (std::equal_to<std::common_type_t<LHS, RHS>>{}(lhs, rhs.v)) {
349 return {};
350 } else {
351 return std::unexpected{
352 std::format("Expected equality of these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
353 }
354
355 } else if constexpr (requires { std::ranges::equal(lhs, rhs.v); }) {
356 if (std::ranges::equal(lhs, rhs.v)) {
357 return {};
358 } else {
359 return std::unexpected{
360 std::format("Expected equality of these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
361 }
362
363 } else {
364 []<bool Flag = false>() {
365 static_assert(Flag, "hikotest: Unable to equality-compare two values.");
366 }();
367 }
368 // clang-format on
369}
370
371template<typename LHS, typename RHS>
372[[nodiscard]] constexpr std::expected<void, std::string>
373operator!=(LHS const& lhs, operand<error_class::exact, RHS> const& rhs) noexcept
374{
375 // clang-format off
376 if constexpr (requires { { lhs != rhs.v } -> std::convertible_to<bool>; }) {
377 if (lhs != rhs.v) {
378 return {};
379 } else {
380 return std::unexpected{
381 std::format("Expected inequality between these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
382 }
383
384 } else if constexpr (requires { std::not_equal_to<std::common_type_t<LHS, RHS>>{}(lhs, rhs.v); }) {
385 if (std::not_equal_to<std::common_type_t<LHS, RHS>>{}(lhs, rhs.v)) {
386 return {};
387 } else {
388 return std::unexpected{
389 std::format("Expected inequality between these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
390 }
391
392 } else if constexpr (requires { not std::ranges::equal(lhs, rhs.v); }) {
393 if (not std::ranges::equal(lhs, rhs.v)) {
394 return {};
395 } else {
396 return std::unexpected{
397 std::format("Expected inequality between these values:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
398 }
399
400 } else {
401 []<bool Flag = false>() {
402 static_assert(Flag, "hikotest: Unable to inequality-compare two values.");
403 }();
404 }
405 // clang-format on
406}
407
408template<typename LHS, typename RHS, typename Error>
409concept diff_ordered = requires(LHS lhs, RHS rhs) {
410 {
411 lhs - rhs
412 } -> std::totally_ordered_with<Error>;
413};
414
415template<typename LHS, typename RHS, typename Error>
416concept range_diff_ordered = std::ranges::range<LHS> and std::ranges::range<RHS> and
417 diff_ordered<std::ranges::range_value_t<LHS>, std::ranges::range_value_t<RHS>, Error>;
418
419template<typename LHS, typename RHS>
420[[nodiscard]] constexpr std::expected<void, std::string>
421operator==(LHS const& lhs, operand<error_class::absolute, RHS> const& rhs) noexcept requires diff_ordered<LHS, RHS, double>
422{
423 auto const diff = lhs - rhs.v;
424 if (diff >= -rhs.e and diff <= +rhs.e) {
425 return {};
426 } else {
427 return std::unexpected{std::format(
428 "Expected equality within {} of these values:\n {}\n {}",
429 +rhs.e,
430 operand_to_string(lhs),
431 operand_to_string(rhs.v))};
432 }
433}
434
435template<typename LHS, typename RHS>
436[[nodiscard]] constexpr std::expected<void, std::string>
437operator==(LHS const& lhs, operand<error_class::absolute, RHS> const& rhs) noexcept
439{
440 auto lit = lhs.begin();
441 auto rit = rhs.v.begin();
442
443 auto const lend = lhs.end();
444 auto const rend = rhs.v.end();
445
446 while (lit != lend and rit != rend) {
447 auto const diff = *lit - *rit;
448 if (diff < -rhs.e or diff > +rhs.e) {
449 return std::unexpected{std::format(
450 "Expected equality within {} of these values:\n {}\n {}",
451 +rhs.e,
452 operand_to_string(lhs),
453 operand_to_string(rhs.v))};
454 }
455
456 ++lit;
457 ++rit;
458 }
459
460 if (lit != lend or rit != rend) {
461 return std::unexpected{std::format(
462 "Expected both range-values to the same size:\n {}\n {}", operand_to_string(lhs), operand_to_string(rhs.v))};
463 }
464
465 return {};
466}
467
468class filter {
469public:
470 constexpr filter() noexcept : inclusions{test_filter_type{}}, exclusions() {}
471 constexpr filter(filter const&) noexcept = default;
472 constexpr filter(filter&&) noexcept = default;
473 constexpr filter& operator=(filter const&) noexcept = default;
474 constexpr filter& operator=(filter&&) noexcept = default;
475
481 filter(std::string_view str);
482 [[nodiscard]] bool match_suite(std::string_view suite) const noexcept;
483 [[nodiscard]] bool match_test(std::string_view suite, std::string_view test) const noexcept;
484
485private:
486 struct test_filter_type {
487 std::string suite_name;
488 std::string test_name;
489 };
490
493};
494
498
499TEST_FORCE_INLINE void require(char const* file, int line, std::expected<void, std::string> result)
500{
501 if (result) {
502 return;
503
504 } else if (not break_on_failure) {
505 throw require_error(std::format("{}({}): error: {}", file, line, result.error()));
506
507 } else {
508 TEST_BREAKPOINT();
510 }
511}
512
513struct test_case {
514 std::string_view file;
515 int line;
516 std::string suite_name;
517 std::string test_name;
518 std::function<void()> _run_test;
519
520 struct result_type {
521 test_case const* parent;
522 utc_time_point_type time_stamp;
523 hr_time_point_type time_point;
524 hr_duration_type duration = {};
525 std::string error_message = {};
526 bool completed = false;
527
528 result_type(result_type const&) noexcept = default;
529 result_type(result_type&&) noexcept = default;
530 result_type& operator=(result_type const&) noexcept = default;
531 result_type& operator=(result_type&&) noexcept = default;
532 result_type(test_case const* parent) noexcept;
533 [[nodiscard]] std::string suite_name() const noexcept;
534 [[nodiscard]] std::string test_name() const noexcept;
535 [[nodiscard]] std::string_view file() const noexcept;
536 [[nodiscard]] int line() const noexcept;
537 [[nodiscard]] bool success() const noexcept;
538 [[nodiscard]] bool failure() const noexcept;
539 [[nodiscard]] bool skipped() const noexcept;
540 void set_success() noexcept;
541 void set_failure(std::string message) noexcept;
542 void junit_xml(FILE* out) const noexcept;
543 };
544
545 test_case(test_case const&) = default;
546 test_case(test_case&&) = default;
547 test_case& operator=(test_case const&) = default;
548 test_case& operator=(test_case&&) = default;
549
550 template<typename Suite>
551 [[nodiscard]] test_case(
552 std::string_view file,
553 int line,
554 std::string suite_name,
555 std::string test_name,
556 void (Suite::*test)()) noexcept :
557 file(file), line(line), suite_name(std::move(suite_name)), test_name(std::move(test_name)), _run_test([test]() {
558 return (Suite{}.*test)();
559 })
560 {
561 }
562
563 [[nodiscard]] result_type run_test_break() const;
564 [[nodiscard]] result_type run_test_catch() const;
565 [[nodiscard]] result_type run_test() const;
566 [[nodiscard]] result_type layout() const noexcept;
567};
568
570 std::string suite_name;
572
573 test_suite(std::string suite_name) noexcept : suite_name(std::move(suite_name)) {}
574
575 struct result_type {
576 using const_iterator = std::vector<test_case::result_type>::const_iterator;
577
578 test_suite const* parent;
579 utc_time_point_type time_stamp = {};
580 hr_time_point_type time_point = {};
581 hr_duration_type duration = {};
583 bool completed = false;
584
585 result_type(result_type const&) noexcept = default;
586 result_type(result_type&&) noexcept = default;
587 result_type& operator=(result_type const&) noexcept = default;
588 result_type& operator=(result_type&&) noexcept = default;
589 result_type(test_suite const* parent) noexcept;
590 [[nodiscard]] std::string suite_name() const noexcept;
591 [[nodiscard]] size_t num_tests() const noexcept;
592 [[nodiscard]] size_t num_failures() const noexcept;
593 [[nodiscard]] size_t num_success() const noexcept;
594 [[nodiscard]] size_t num_skipped() const noexcept;
595 [[nodiscard]] size_t num_disabled() const noexcept;
596 [[nodiscard]] size_t num_errors() const noexcept;
597 [[nodiscard]] const_iterator begin() const noexcept;
598 [[nodiscard]] const_iterator end() const noexcept;
599 void push_back(test_case::result_type test_result) noexcept;
600 void finish() noexcept;
601 void junit_xml(FILE* out) const noexcept;
602 };
603
604 [[nodiscard]] result_type layout(filter const& filter) const noexcept;
605 [[nodiscard]] result_type run_tests(filter const& filter) const;
606};
607
608struct all_tests {
609 struct result_type {
610 using const_iterator = std::vector<test_suite::result_type>::const_iterator;
611
612 all_tests* parent;
613 utc_time_point_type time_stamp = {};
614 hr_time_point_type time_point = {};
615 hr_duration_type duration = {};
617 bool completed = false;
618
619 result_type(all_tests* parent) noexcept;
620 void finish() noexcept;
621 [[nodiscard]] size_t num_suites() const noexcept;
622 [[nodiscard]] size_t num_tests() const noexcept;
623 [[nodiscard]] size_t num_failures() const noexcept;
624 [[nodiscard]] size_t num_success() const noexcept;
625 [[nodiscard]] size_t num_disabled() const noexcept;
626 [[nodiscard]] size_t num_skipped() const noexcept;
627 [[nodiscard]] size_t num_errors() const noexcept;
628 [[nodiscard]] std::vector<std::string> fqnames_of_failed_tests() const noexcept;
629 [[nodiscard]] const_iterator begin() const noexcept;
630 [[nodiscard]] const_iterator end() const noexcept;
631 void push_back(test_suite::result_type suite_result) noexcept;
632 void junit_xml(FILE* out) const noexcept;
633 };
634
636 mutable size_t last_registered_suite = 0;
637
638 template<typename Suite>
639 [[nodiscard]] inline test_suite& register_suite() noexcept
640 {
641 auto name = type_name<Suite>();
642
643 // Remove common suffixes.
644 if (name.ends_with("_test_suite")) {
645 name = name.substr(0, name.size() - 11);
646 } else if (name.ends_with("_suite")) {
647 name = name.substr(0, name.size() - 6);
648 } else if (name.ends_with("_tests")) {
649 name = name.substr(0, name.size() - 6);
650 } else if (name.ends_with("_test")) {
651 name = name.substr(0, name.size() - 5);
652 }
653
654 // Remove namespaces.
655 if (auto i = name.rfind(':'); i != name.npos) {
656 name = name.substr(i + 1);
657 }
658
659 // Skip binary search if possible.
660 if (last_registered_suite < suites.size() and suites[last_registered_suite].suite_name == name) {
661 return suites[last_registered_suite];
662 }
663
664 auto const it = std::lower_bound(suites.begin(), suites.end(), name, [](auto const& item, auto const& name) {
665 return item.suite_name < name;
666 });
667
668 last_registered_suite = std::distance(suites.begin(), it);
669
670 if (it != suites.end() and it->suite_name == name) {
671 return *it;
672 }
673
674 return *suites.emplace(it, name);
675 }
676
677 template<typename Suite>
678 [[nodiscard]] inline test_case&
679 register_test(void (Suite::*test)(), std::string_view file, int line, std::string name) noexcept
680 {
681 if (name.ends_with("_test_case")) {
682 name = name.substr(0, name.size() - 10);
683 } else if (name.ends_with("_case")) {
684 name = name.substr(0, name.size() - 5);
685 } else if (name.ends_with("_test")) {
686 name = name.substr(0, name.size() - 5);
687 }
688
689 auto& suite = register_suite<Suite>();
690 auto& tests = suite.tests;
691
692 auto const it = std::lower_bound(tests.begin(), tests.end(), name, [](auto const& item, auto const& name) {
693 return item.test_name < name;
694 });
695
696 if (it != tests.end() and it->test_name == name) {
697 std::println(
698 stderr,
699 "{}({}): error: Test {}.{} is already registered at {}({}).",
700 file,
701 line,
702 it->suite_name,
703 it->test_name,
704 it->file,
705 it->line);
707 }
708
709 return *tests.emplace(it, file, line, suite.suite_name, name, test);
710 }
711
712 [[nodiscard]] result_type layout(::test::filter const& filter) noexcept;
713 [[nodiscard]] result_type list_tests(::test::filter const& filter) noexcept;
714 [[nodiscard]] result_type run_tests(::test::filter const& filter);
715};
716
717inline auto all = all_tests{};
718
719template<typename Suite>
720[[nodiscard]] inline test_suite& register_suite() noexcept
721{
722 return all.template register_suite<Suite>();
723}
724
725template<typename Suite>
726[[nodiscard]] inline test_case& register_test(void (Suite::*test)(), std::string_view file, int line, std::string name) noexcept
727{
728 return all.template register_test<Suite>(test, file, line, std::move(name));
729}
730
731inline all_tests::result_type list_tests(filter const& filter) noexcept;
732[[nodiscard]] inline all_tests::result_type run_tests(filter const& filter);
733
734template<typename Suite>
735struct suite {
736 using _hikotest_suite_type = Suite;
737};
738
739} // namespace test
STL namespace.
The comparison error.
Definition hikotest.hpp:244
constexpr double operator-() const noexcept
Get the error value as a negative number.
Definition hikotest.hpp:256
constexpr double operator+() const noexcept
Get the error value as a positive number.
Definition hikotest.hpp:249
Operand of a comparison, bound to an error-value.
Definition hikotest.hpp:274
Definition hikotest.hpp:468
filter(std::string_view str)
Create a filter from the string representation.
Definition hikotest.hpp:495
Definition hikotest.hpp:513
Definition hikotest.hpp:520
Definition hikotest.hpp:569
Definition hikotest.hpp:575
Definition hikotest.hpp:608
Definition hikotest.hpp:609
Definition hikotest.hpp:735
Definition hikotest.hpp:409
Definition hikotest.hpp:416
T addressof(T... args)
T begin(T... args)
T distance(T... args)
T emplace(T... args)
T end(T... args)
T find(T... args)
T lower_bound(T... args)
T memcpy(T... args)
T move(T... args)
T operator!=(T... args)
T rfind(T... args)
T size(T... args)
T str(T... args)
T substr(T... args)
T terminate(T... args)
T to_string(T... args)
T unexpected(T... args)