aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-07-02 02:17:11 +0100
committerLexi Winter <lexi@le-fay.org>2025-07-02 02:17:11 +0100
commit83eae6f3280b237ff4fb080947658cc91fde9532 (patch)
tree11330ff545d8505e9e68826878ebe6ec06d02d15
parentf52c343ab804b8469fda67d62383f84277577c93 (diff)
downloadnihil-83eae6f3280b237ff4fb080947658cc91fde9532.tar.gz
nihil-83eae6f3280b237ff4fb080947658cc91fde9532.tar.bz2
error: add more tests
-rw-r--r--nihil.error/CMakeLists.txt4
-rw-r--r--nihil.error/error.ccm150
-rw-r--r--nihil.error/error.test.cc273
-rw-r--r--nihil.error/test.cc164
-rw-r--r--nihil.std/nihil.std.ccm13
5 files changed, 367 insertions, 237 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);
- }
-}
diff --git a/nihil.std/nihil.std.ccm b/nihil.std/nihil.std.ccm
index effbf8a..349ab95 100644
--- a/nihil.std/nihil.std.ccm
+++ b/nihil.std/nihil.std.ccm
@@ -166,7 +166,6 @@ using std::uint64_t;
// <cstdlib>
using std::exit;
using std::quick_exit;
-using std::quick_exit;
// <cstdio>
using std::FILE;
@@ -208,28 +207,30 @@ using std::format_args;
using std::format_context;
using std::format_error;
using std::format_kind;
-using std::format;
using std::format_to;
using std::format_to_n;
+using std::format_to_n;
using std::format_to_n_result;
-using std::formattable;
using std::formatted_size;
using std::formatter;
-using std::make_format_args;
-using std::make_wformat_args;
+using std::formatter;
using std::range_format;
using std::range_formatter;
+using std::runtime_format;
using std::visit_format_arg;
using std::wformat_parse_context;
using std::format_error;
using std::runtime_format;
// <functional>
+using std::cref;
using std::function;
-using std::invoke;
using std::ref;
using std::cref;
using std::reference_wrapper;
+using std::invoke;
+using std::invoke_result;
+using std::invoke_result_t;
// <iostream>
using std::cerr;