From 2aab0ae5763a2ebf7bbb26e8381005c40cf4c3df Mon Sep 17 00:00:00 2001 From: manuel Date: Thu, 29 Oct 2026 09:24:10 +0000 Subject: kstd: add Catch2 string makers for printable test output Co-Authored-By: Claude Opus 5.5 --- libs/kstd/CMakeLists.txt | 4 + libs/kstd/kstd/test_support/os_panic.tests.cpp | 2 +- libs/kstd/kstd/test_support/string_makers.hpp | 180 +++++++++++++++++++++ .../kstd/kstd/test_support/string_makers.tests.cpp | 94 +++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 libs/kstd/kstd/test_support/string_makers.hpp create mode 100644 libs/kstd/kstd/test_support/string_makers.tests.cpp (limited to 'libs') diff --git a/libs/kstd/CMakeLists.txt b/libs/kstd/CMakeLists.txt index 1902d0d9..d7af1952 100644 --- a/libs/kstd/CMakeLists.txt +++ b/libs/kstd/CMakeLists.txt @@ -83,6 +83,10 @@ if(BUILD_TESTING) teachos_add_tests("kstd") + target_compile_options("kstd_tests" PRIVATE + "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/kstd/test_support/string_makers.hpp" + ) + if(COMMAND "enable_coverage" AND KSTD_ENABLE_TEST_COVERAGE) enable_coverage("kstd") enable_coverage("kstd_tests") diff --git a/libs/kstd/kstd/test_support/os_panic.tests.cpp b/libs/kstd/kstd/test_support/os_panic.tests.cpp index c30411aa..17af74a7 100644 --- a/libs/kstd/kstd/test_support/os_panic.tests.cpp +++ b/libs/kstd/kstd/test_support/os_panic.tests.cpp @@ -7,7 +7,7 @@ namespace kstd::os { - auto panic(std::string_view message, std::source_location location) + auto panic(std::string_view message, std::source_location location) -> void { throw kstd::tests::os_panic{std::string{message}, location}; } diff --git a/libs/kstd/kstd/test_support/string_makers.hpp b/libs/kstd/kstd/test_support/string_makers.hpp new file mode 100644 index 00000000..c2c6a38a --- /dev/null +++ b/libs/kstd/kstd/test_support/string_makers.hpp @@ -0,0 +1,180 @@ +#ifndef KSTD_TEST_SUPPORT_STRING_MAKERS_HPP +#define KSTD_TEST_SUPPORT_STRING_MAKERS_HPP + +//! Catch2 string conversions for kstd types. +//! +//! Catch2 prints values it does not know how to convert as "{?}". This header teaches Catch2 how to print kstd types, +//! so that failed assertions show the actual values. It is force-included into every test translation unit by the +//! build system and must therefore never be included by kernel code. +//! +//! Any type with a kstd::formatter specialization is printed via kstd::format, so the easiest way to make a type +//! printable in tests is to give it a formatter. Types without a formatter can get a Catch::StringMaker specialization +//! here, or in the test support header of the library they belong to. + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace kstd::tests +{ + + //! A type that can be printed via kstd::format. + template + concept kstd_formattable = std::default_initializable>>; + + //! A type that can be written to a std::ostream. + template + concept stream_insertable = requires(std::ostream & stream, T const & value) { stream << value; }; + + //! A type that Catch2 can not print on its own, but which is printable via kstd::format. + //! + //! Types that are stream insertable, pointers, arrays, and ranges are handled by Catch2 already. They are excluded + //! before checking for a formatter, since probing the formatter of a range with unformattable elements is a hard + //! error. + template + concept catch_printable_via_kstd = + !std::ranges::range && !Catch::is_range::value && !std::is_pointer_v && !std::is_array_v && + !std::is_arithmetic_v && !stream_insertable && kstd_formattable; + + //! Convert a kstd string to a std::string. + inline auto to_std_string(kstd::string const & string) -> std::string + { + return std::string{string.data(), string.size()}; + } + + //! Convert an error code or error condition to its textual representation, e.g. "generic:22:invalid argument". + template + auto error_to_string(ErrorType const & error) -> std::string + { + return std::string{error.category().name()} + ':' + std::to_string(error.value()) + ':' + + std::string{error.message()}; + } + + //! Convert a smart pointer to the same textual representation Catch2 uses for raw pointers. + template + auto pointer_to_string(Pointer const & pointer) -> std::string + { + return Catch::Detail::stringify(pointer.get()); + } + +} // namespace kstd::tests + +namespace Catch +{ + + template + struct StringMaker + { + static auto convert(T const & value) -> std::string + { + return kstd::tests::to_std_string(kstd::format("{}", value)); + } + }; + + template<> + struct StringMaker + { + static auto convert(kstd::string const & value) -> std::string + { + return Catch::Detail::stringify(kstd::tests::to_std_string(value)); + } + }; + + template<> + struct StringMaker + { + static auto convert(kstd::error_category const & category) -> std::string + { + return std::string{category.name()}; + } + }; + + template<> + struct StringMaker + { + static auto convert(kstd::error_condition const & condition) -> std::string + { + return kstd::tests::error_to_string(condition); + } + }; + + template + struct StringMaker + { + static auto convert(Enum value) -> std::string + { + return kstd::tests::error_to_string(kstd::error_code{value}); + } + }; + + template + requires(!kstd::error_code_enum) + struct StringMaker + { + static auto convert(Enum value) -> std::string + { + return kstd::tests::error_to_string(make_error_condition(value)); + } + }; + + template + struct StringMaker> + { + static auto convert(kstd::shared_ptr const & pointer) -> std::string + { + return kstd::tests::pointer_to_string(pointer); + } + }; + + template + struct StringMaker> + { + static auto convert(kstd::unique_ptr const & pointer) -> std::string + { + return kstd::tests::pointer_to_string(pointer); + } + }; + + template + struct StringMaker> + { + static auto convert(kstd::observer_ptr const & pointer) -> std::string + { + return kstd::tests::pointer_to_string(pointer); + } + }; + + template + struct StringMaker> + { + static auto convert(std::expected const & expected) -> std::string + { + if (!expected) + { + return "error(" + Catch::Detail::stringify(expected.error()) + ')'; + } + + if constexpr (std::is_void_v) + { + return "value()"; + } + else + { + return "value(" + Catch::Detail::stringify(*expected) + ')'; + } + } + }; + +} // namespace Catch + +#endif diff --git a/libs/kstd/kstd/test_support/string_makers.tests.cpp b/libs/kstd/kstd/test_support/string_makers.tests.cpp new file mode 100644 index 00000000..887bec37 --- /dev/null +++ b/libs/kstd/kstd/test_support/string_makers.tests.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +using namespace kstd::units_literals; + +SCENARIO("Catch2 string conversion of kstd types", "[test_support][string_makers]") +{ + GIVEN("a kstd string") + { + auto const string = kstd::string{"hello"}; + + THEN("it is printed like a std::string") + { + REQUIRE(Catch::Detail::stringify(string) == "\"hello\""); + } + } + + GIVEN("a type with a kstd formatter") + { + THEN("it is printed via kstd::format") + { + REQUIRE(Catch::Detail::stringify(42_B) == "42"); + REQUIRE(Catch::Detail::stringify(std::strong_ordering::less) == "less"); + } + } + + GIVEN("an error code") + { + auto const error = kstd::make_error_code(kstd::errc::invalid_argument); + + THEN("it is printed with its category, value, and message") + { + REQUIRE(Catch::Detail::stringify(error) == kstd::tests::error_to_string(error)); + REQUIRE(Catch::Detail::stringify(error).starts_with(std::string{error.category().name()} + ':')); + } + } + + GIVEN("an error condition enumerator") + { + THEN("it is printed like the matching error condition") + { + REQUIRE(Catch::Detail::stringify(kstd::errc::invalid_argument) == + kstd::tests::error_to_string(kstd::make_error_condition(kstd::errc::invalid_argument))); + } + } + + GIVEN("a successful result") + { + auto const result = kstd::result{42}; + auto const void_result = kstd::result{}; + + THEN("it is printed as value(...)") + { + REQUIRE(Catch::Detail::stringify(result) == "value(42)"); + REQUIRE(Catch::Detail::stringify(void_result) == "value()"); + } + } + + GIVEN("a failed result") + { + auto const error = kstd::make_error_code(kstd::errc::invalid_argument); + auto const result = kstd::result{std::unexpect, error}; + + THEN("it is printed as error(...)") + { + REQUIRE(Catch::Detail::stringify(result) == "error(" + Catch::Detail::stringify(error) + ')'); + } + } + + GIVEN("smart pointers") + { + auto const empty = kstd::shared_ptr{}; + auto const shared = kstd::make_shared(1); + auto const observer = kstd::observer_ptr{shared.get()}; + + THEN("they are printed like raw pointers") + { + REQUIRE(Catch::Detail::stringify(empty) == "nullptr"); + REQUIRE(Catch::Detail::stringify(shared) == Catch::Detail::stringify(shared.get())); + REQUIRE(Catch::Detail::stringify(observer) == Catch::Detail::stringify(shared.get())); + } + } +} -- cgit v1.2.3