diff options
Diffstat (limited to 'libs')
| -rw-r--r-- | libs/kstd/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | libs/kstd/kstd/test_support/os_panic.tests.cpp | 2 | ||||
| -rw-r--r-- | libs/kstd/kstd/test_support/string_makers.hpp | 180 | ||||
| -rw-r--r-- | libs/kstd/kstd/test_support/string_makers.tests.cpp | 94 |
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())); + } + } +} |
