aboutsummaryrefslogtreecommitdiff
path: root/libs/kstd
diff options
context:
space:
mode:
authormanuel <manuel.bott@eps.ch>2026-10-29 09:24:10 +0000
committermanuel <manuel.bott@eps.ch>2026-10-31 09:55:52 +0000
commit2aab0ae5763a2ebf7bbb26e8381005c40cf4c3df (patch)
tree9f60ef9291545470291b216df2bf9769661b3084 /libs/kstd
parent22bd54edce3b0878ac294ff6e8e858354898f0d9 (diff)
downloadkernel-2aab0ae5763a2ebf7bbb26e8381005c40cf4c3df.tar.xz
kernel-2aab0ae5763a2ebf7bbb26e8381005c40cf4c3df.zip
kstd: add Catch2 string makers for printable test output
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'libs/kstd')
-rw-r--r--libs/kstd/CMakeLists.txt4
-rw-r--r--libs/kstd/kstd/test_support/os_panic.tests.cpp2
-rw-r--r--libs/kstd/kstd/test_support/string_makers.hpp180
-rw-r--r--libs/kstd/kstd/test_support/string_makers.tests.cpp94
4 files changed, 279 insertions, 1 deletions
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 <kstd/format.hpp>
+#include <kstd/memory.hpp>
+#include <kstd/string.hpp>
+#include <kstd/system_error.hpp>
+
+#include <catch2/catch_tostring.hpp>
+
+#include <concepts>
+#include <expected>
+#include <ostream>
+#include <ranges>
+#include <string>
+#include <type_traits>
+
+namespace kstd::tests
+{
+
+ //! A type that can be printed via kstd::format.
+ template<typename T>
+ concept kstd_formattable = std::default_initializable<kstd::formatter<std::remove_cvref_t<T>>>;
+
+ //! A type that can be written to a std::ostream.
+ template<typename T>
+ 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<typename T>
+ concept catch_printable_via_kstd =
+ !std::ranges::range<T> && !Catch::is_range<T>::value && !std::is_pointer_v<T> && !std::is_array_v<T> &&
+ !std::is_arithmetic_v<T> && !stream_insertable<T> && kstd_formattable<T>;
+
+ //! 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<typename ErrorType>
+ 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<typename Pointer>
+ auto pointer_to_string(Pointer const & pointer) -> std::string
+ {
+ return Catch::Detail::stringify(pointer.get());
+ }
+
+} // namespace kstd::tests
+
+namespace Catch
+{
+
+ template<kstd::tests::catch_printable_via_kstd T>
+ struct StringMaker<T>
+ {
+ static auto convert(T const & value) -> std::string
+ {
+ return kstd::tests::to_std_string(kstd::format("{}", value));
+ }
+ };
+
+ template<>
+ struct StringMaker<kstd::string>
+ {
+ static auto convert(kstd::string const & value) -> std::string
+ {
+ return Catch::Detail::stringify(kstd::tests::to_std_string(value));
+ }
+ };
+
+ template<>
+ struct StringMaker<kstd::error_category>
+ {
+ static auto convert(kstd::error_category const & category) -> std::string
+ {
+ return std::string{category.name()};
+ }
+ };
+
+ template<>
+ struct StringMaker<kstd::error_condition>
+ {
+ static auto convert(kstd::error_condition const & condition) -> std::string
+ {
+ return kstd::tests::error_to_string(condition);
+ }
+ };
+
+ template<kstd::error_code_enum Enum>
+ struct StringMaker<Enum>
+ {
+ static auto convert(Enum value) -> std::string
+ {
+ return kstd::tests::error_to_string(kstd::error_code{value});
+ }
+ };
+
+ template<kstd::error_condition_enum Enum>
+ requires(!kstd::error_code_enum<Enum>)
+ struct StringMaker<Enum>
+ {
+ static auto convert(Enum value) -> std::string
+ {
+ return kstd::tests::error_to_string(make_error_condition(value));
+ }
+ };
+
+ template<typename T>
+ struct StringMaker<kstd::shared_ptr<T>>
+ {
+ static auto convert(kstd::shared_ptr<T> const & pointer) -> std::string
+ {
+ return kstd::tests::pointer_to_string(pointer);
+ }
+ };
+
+ template<typename T>
+ struct StringMaker<kstd::unique_ptr<T>>
+ {
+ static auto convert(kstd::unique_ptr<T> const & pointer) -> std::string
+ {
+ return kstd::tests::pointer_to_string(pointer);
+ }
+ };
+
+ template<typename T>
+ struct StringMaker<kstd::observer_ptr<T>>
+ {
+ static auto convert(kstd::observer_ptr<T> const & pointer) -> std::string
+ {
+ return kstd::tests::pointer_to_string(pointer);
+ }
+ };
+
+ template<typename T, typename E>
+ struct StringMaker<std::expected<T, E>>
+ {
+ static auto convert(std::expected<T, E> const & expected) -> std::string
+ {
+ if (!expected)
+ {
+ return "error(" + Catch::Detail::stringify(expected.error()) + ')';
+ }
+
+ if constexpr (std::is_void_v<T>)
+ {
+ 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 <kstd/test_support/string_makers.hpp>
+
+#include <kstd/memory.hpp>
+#include <kstd/result.hpp>
+#include <kstd/string.hpp>
+#include <kstd/system_error.hpp>
+#include <kstd/units.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/catch_tostring.hpp>
+
+#include <compare>
+#include <expected>
+
+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<int>{42};
+ auto const void_result = kstd::result<void>{};
+
+ 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<int>{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<int>{};
+ auto const shared = kstd::make_shared<int>(1);
+ auto const observer = kstd::observer_ptr<int>{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()));
+ }
+ }
+}