diff options
Diffstat (limited to 'nihil.error')
| -rw-r--r-- | nihil.error/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | nihil.error/error.ccm | 150 | ||||
| -rw-r--r-- | nihil.error/error.test.cc | 273 | ||||
| -rw-r--r-- | nihil.error/test.cc | 164 |
4 files changed, 360 insertions, 231 deletions
diff --git a/nihil.error/CMakeLists.txt b/nihil.error/CMakeLists.txt index fd5da84..8a215b7 100644 --- a/nihil.error/CMakeLists.txt +++ b/nihil.error/CMakeLists.txt @@ -12,10 +12,10 @@ if(NIHIL_TESTS) enable_testing() add_executable(nihil.error.test - test.cc) + error.test.cc) target_link_libraries(nihil.error.test PRIVATE - nihil.error + nihil.std nihil.core nihil.error Catch2::Catch2WithMain ) diff --git a/nihil.error/error.ccm b/nihil.error/error.ccm index 7ed9d5c..b821e0a 100644 --- a/nihil.error/error.ccm +++ b/nihil.error/error.ccm @@ -32,87 +32,107 @@ namespace nihil { // Things which can be errors. using error_t = std::variant<std::monostate, std::string, std::error_code, std::error_condition>; -export struct error : std::exception +// A proxy class used when constructing errors. This has implicit constructors from various types, +// which means we don't have to handle every possible combination of types in error itself. +export struct error_proxy { - // Create an empty error, representing success. - error() = default; - - // Destroy an error. - ~error() override = default; + // Construct from... - // Create an error from a freeform string. - explicit error(std::string_view what) + // ... a string_view + error_proxy(std::string_view const what) // NOLINT : m_error(std::string(what)) { } - // Create an error from a freeform string and a cause. - error(std::string_view what, error cause) - : m_error(std::string(what)) - , m_cause(std::make_shared<error>(std::move(cause))) + // ... an std::string; so we can move the string into place if it's an rvalue. + error_proxy(auto &&what) // NOLINT + requires (std::same_as<std::remove_cvref_t<decltype(what)>, std::string>) + : m_error(std::forward<decltype(what)>(what)) { } - // Create an error from a freeform string and an error code enum cause. - template <typename Cause> - requires(std::is_error_code_enum<Cause>::value || - std::is_error_condition_enum<Cause>::value) - error(std::string_view what, Cause &&cause) - : error(what, error(std::forward<Cause>(cause))) + // ... a C string + error_proxy(char const *what) // NOLINT + : m_error(std::string(what)) { } - // Create an error from an std::error_condition. - explicit error(std::error_condition what) + // ... an std::error_code + error_proxy(std::error_code const what) // NOLINT : m_error(what) { } - // Create an error from an std::error_condition and a cause. - error(std::error_condition what, error cause) + // ... an std::error_condition + error_proxy(std::error_condition const what) // NOLINT : m_error(what) - , m_cause(std::make_shared<error>(std::move(cause))) { } - // Create an error from an std::error_code. - explicit error(std::error_code what) - : m_error(what) + // ... an error code enum + template <typename T> + requires(std::is_error_code_enum<T>::value) + error_proxy(T what) // NOLINT + : m_error(make_error_code(what)) { } - // Create an error from an std::error_code and a cause. - error(std::error_code what, error cause) - : m_error(what) - , m_cause(std::make_shared<error>(std::move(cause))) + // ... an error condition enum + template <typename T> + requires(std::is_error_condition_enum<T>::value) + error_proxy(T what) // NOLINT + : m_error(make_error_condition(what)) { } - // Create an error from an std::error_code enum. - explicit error(auto errc) - requires(std::is_error_code_enum<decltype(errc)>::value) - : error(make_error_code(errc)) + // Not copyable. + error_proxy(error_proxy const &) = delete; + auto operator=(error_proxy const &) -> error_proxy & = delete; + + // Not movable. + error_proxy(error_proxy &&) = delete; + auto operator=(error_proxy &&) -> error_proxy & = delete; + + ~error_proxy() = default; + + // Let error extract the error_t. + [[nodiscard]] auto error() && -> error_t && { + return std::move(m_error); } - // Create an error from an std::error_code enum and a cause/ - error(auto errc, error cause) - requires(std::is_error_code_enum<decltype(errc)>::value) - : error(make_error_code(errc), std::move(cause)) +private: + // The error. + error_t m_error; +}; + +// The error class. +export struct error : std::exception +{ + // Create an empty error, representing success. + error() = default; + + // Destroy an error. + ~error() override = default; + + // Create an error from an error proxy. + explicit error(error_proxy &&proxy) + : m_error(std::move(std::move(proxy).error())) { } - // Create an error from an std::error_condition enum. - explicit error(auto errc) - requires(std::is_error_condition_enum<decltype(errc)>::value) - : error(make_error_condition(errc)) + // Create an error from an error proxy and an error cause. + error(error_proxy &&proxy, error cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared<error>(std::move(cause))) { } - // Create an error from an std::error_condition enum and a cause. - error(auto errc, error cause) - requires(std::is_error_condition_enum<decltype(errc)>::value) - : error(make_error_condition(errc), std::move(cause)) + // Create an error from an error proxy and an error_proxy cause. + // For example, error("cannot open file", std::errc::permission_denied). + error(error_proxy &&proxy, error_proxy &&cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared<error>(std::move(cause))) { } @@ -178,7 +198,7 @@ export struct error : std::exception [[nodiscard]] auto code(this error const &self) -> std::optional<std::error_code> { auto const *code = std::get_if<std::error_code>(&self.m_error); - if (code) + if (code != nullptr) return {*code}; return {}; } @@ -187,7 +207,7 @@ export struct error : std::exception [[nodiscard]] auto condition(this error const &self) -> std::optional<std::error_condition> { auto const *condition = std::get_if<std::error_condition>(&self.m_error); - if (condition) + if (condition != nullptr) return {*condition}; return {}; } @@ -202,7 +222,7 @@ export struct error : std::exception return m_what->c_str(); } - // Allow error to be implicitly converted to std::expectde and std::unexpected, to make using it + // Allow error to be implicitly converted to std::expected and std::unexpected, to make using it // with std::expected easier. template<typename T> @@ -228,6 +248,16 @@ export struct error : std::exception } private: + // This error. + error_t m_error = make_error_code(std::errc()); + + // The cause of this error, if any. + std::shared_ptr<error> m_cause; + + // For std::exception::what(), we need to keep the string valid + // until we're destroyed. + mutable std::optional<std::string> m_what; + // Equality comparison. [[nodiscard]] friend auto operator==(error const &lhs, error const &rhs) -> bool { @@ -251,22 +281,6 @@ private: return lhs.condition() == rhs; } - // Print an error to a stream. - friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream & - { - return strm << e.full_str(); - } - - // This error. - error_t m_error = make_error_code(std::errc()); - - // The cause of this error, if any. - std::shared_ptr<error> m_cause; - - // For std::exception::what(), we need to keep the string valid - // until we're destroyed. - mutable std::optional<std::string> m_what; - // Compare an error to an std::error_code enum. [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool requires(std::is_error_code_enum<decltype(rhs)>::value) @@ -280,6 +294,12 @@ private: { return lhs.condition() == rhs; } + + // Print an error to a stream. + friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream & + { + return strm << e.full_str(); + } }; } // namespace nihil diff --git a/nihil.error/error.test.cc b/nihil.error/error.test.cc new file mode 100644 index 0000000..db6c773 --- /dev/null +++ b/nihil.error/error.test.cc @@ -0,0 +1,273 @@ +// This source code is released into the public domain. + +#include <catch2/catch_test_macros.hpp> + +import nihil.std; +import nihil.core; +import nihil.error; + +namespace { +inline constexpr auto *test_tags = "[nihil][nihil.error]"; + +TEST_CASE("error: invariants", test_tags) +{ + static_assert(std::destructible<nihil::error>); + static_assert(std::default_initializable<nihil::error>); + static_assert(std::move_constructible<nihil::error>); + static_assert(std::copy_constructible<nihil::error>); + static_assert(std::equality_comparable<nihil::error>); + static_assert(std::totally_ordered<nihil::error>); + static_assert(std::swappable<nihil::error>); + static_assert(std::regular<nihil::error>); +} + +SCENARIO("A nihil::error can be constructed from a C string", test_tags) +{ + GIVEN ("An error object constructed from a string") { + auto e = nihil::error("an error"); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string lvalue") { + auto s = std::string("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string rvalue") { + auto e = nihil::error(std::string("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view lvalue") { + auto s = std::string_view("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view rvalue") { + auto e = nihil::error(std::string_view("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::error_condition", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::error_condition(std::errc::invalid_argument)); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from a nihil::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(nihil::errc::incomplete_command); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Incomplete command"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == nihil::errc::incomplete_command); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == nihil::errc::incomplete_command); + } + } +} + +SCENARIO("A nihil::error can be constructed with a cause", test_tags) +{ + GIVEN ("An error object constructed with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error: Invalid argument"); + } + + AND_THEN ("cause() should return the cause") { + REQUIRE(e.cause()); + REQUIRE(*e.cause() == std::errc::invalid_argument); + } + } +} + +SCENARIO("std::format with a nihil::error", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error: Invalid argument"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } +} + +SCENARIO("Print a nihil::error to an std::ostream", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error"); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error: Invalid argument"); + } + } +} + +SCENARIO("Comparison of nihil::error with operator==", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("an error"); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different strings") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("another error"); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } + + GIVEN ("Two nihil::error objects constructed from the same error code") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::invalid_argument); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different error codes") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::permission_denied); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } +} + +SCENARIO("Comparison of nihil::error with operator<", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("aaa"); + auto e2 = nihil::error("zzz"); + + THEN ("aaa should be less than zzz") { + REQUIRE(e1 < e2); + } + } +} + +SCENARIO("Throwing and catching a nihil::error object", test_tags) +{ + GIVEN ("A nihil::error object") { + THEN ("We should be able to throw and catch the error") { + REQUIRE_THROWS_AS(throw nihil::error("an error"), nihil::error); + + try { + throw nihil::error("an error"); + } catch (nihil::error const &e) { + REQUIRE(e.full_str() == "an error"); + }; + } + } +} + +} // anonymous namespace diff --git a/nihil.error/test.cc b/nihil.error/test.cc deleted file mode 100644 index 0f4f93f..0000000 --- a/nihil.error/test.cc +++ /dev/null @@ -1,164 +0,0 @@ -// This source code is released into the public domain. - -#include <catch2/catch_test_macros.hpp> - -import nihil.std; -import nihil.error; - -TEST_CASE("error: invariants", "[nihil]") -{ - static_assert(std::destructible<nihil::error>); - static_assert(std::default_initializable<nihil::error>); - static_assert(std::move_constructible<nihil::error>); - static_assert(std::copy_constructible<nihil::error>); - static_assert(std::equality_comparable<nihil::error>); - static_assert(std::totally_ordered<nihil::error>); - static_assert(std::swappable<nihil::error>); - static_assert(std::regular<nihil::error>); -} - -TEST_CASE("error: construct from string", "[nihil]") -{ - using namespace nihil; - - auto e = error("an error"); - REQUIRE(e.full_str() == "an error"); - REQUIRE(e.this_str() == e.full_str()); - REQUIRE(std::format("{}", e) == e.full_str()); -} - -TEST_CASE("error: construct from std::error_condition", "[nihil]") -{ - using namespace nihil; - - auto code = std::make_error_condition(std::errc::invalid_argument); - auto e = error(code); - - REQUIRE(!e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == true); - - REQUIRE(e == std::errc::invalid_argument); - REQUIRE(e != std::errc::no_such_file_or_directory); - - REQUIRE(e.full_str() == std::strerror(EINVAL)); - REQUIRE(e.this_str() == e.full_str()); - REQUIRE(std::format("{}", e) == e.full_str()); -} - -TEST_CASE("error: construct from std::errc", "[nihil]") -{ - using namespace nihil; - - auto e = error(std::errc::invalid_argument); - - REQUIRE(!e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == true); - - REQUIRE(e == std::errc::invalid_argument); - REQUIRE(e != std::errc::no_such_file_or_directory); - - REQUIRE(e.full_str() == std::strerror(EINVAL)); - REQUIRE(e.this_str() == e.full_str()); - REQUIRE(std::format("{}", e) == e.full_str()); -} - -TEST_CASE("error: compound error", "[nihil]") -{ - using namespace std::literals; - using namespace nihil; - - auto e = error("cannot open file", - error(std::errc::no_such_file_or_directory)); - - REQUIRE(e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == false); - - REQUIRE(*e.cause() == std::errc::no_such_file_or_directory); - - REQUIRE(e.full_str() == ("cannot open file: "s + std::strerror(ENOENT))); - REQUIRE(e.this_str() == "cannot open file"); - REQUIRE(std::format("{}", e) == e.full_str()); -} - -TEST_CASE("error: operator== with strings", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("error"); - auto e2 = error("error"); - auto e3 = error("an error"); - - REQUIRE(e1 == e2); - REQUIRE(e1 != e3); -} - -TEST_CASE("error: operator< with strings", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("aaa"); - auto e2 = error("zzz"); - - REQUIRE(e1 < e2); -} - -TEST_CASE("error: operator== with a cause", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("error", error("cause 1")); - auto e2 = error("error", error("cause 2")); - - REQUIRE(e1 == e2); -} - -TEST_CASE("error: operator== with error_conditions", "[nihil]") -{ - using namespace nihil; - - auto e1 = error(std::errc::invalid_argument); - auto e2 = error(std::errc::invalid_argument); - auto e3 = error(std::errc::permission_denied); - - REQUIRE(e1 == e2); - REQUIRE(e1 != e3); -} - -TEST_CASE("error: std::format with string", "[nihil]") -{ - using namespace nihil; - - auto err = error("an error"); - REQUIRE(std::format("{}", err) == "an error"); -} - -TEST_CASE("error: std::format with std::errc", "[nihil]") -{ - using namespace nihil; - - auto err = error(std::errc::invalid_argument); - REQUIRE(std::format("{}", err) == std::strerror(EINVAL)); -} - -TEST_CASE("error: std::format with cause", "[nihil]") -{ - using namespace nihil; - - auto err = error("an error", std::errc::invalid_argument); - REQUIRE(std::format("{}", err) == "an error: Invalid argument"); -} - -TEST_CASE("error: throw and catch", "[nihil]") -{ - using namespace std::literals; - using namespace nihil; - - try { - throw error("oh no", error(std::errc::invalid_argument)); - } catch (std::exception const &exc) { - REQUIRE(exc.what() == "oh no: Invalid argument"s); - } -} |
