From 5f5f0bf9925618040d84ae6dc947650507df9383 Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Wed, 30 Dec 2020 11:58:37 -0800 Subject: [PATCH 1/3] Add ErrorCode The ErrorCode class implements type erasure for a range of error code enumeration types, allowing for interoperability between various domain-specific and third-party error codes that the library may need to support. Unlike std::error_code, ErrorCode uses a variant/visitor idiom rather than dynamic polymorphism to implement type erasure. This makes it more intrusive to extend - you need to modify the class definition in order to support additional error codes. However, it allows ErrorCode to be constructed on the device and safely passed between host and device code. In addition, there's significantly less boilerplate code to maintain for each specific error code enumeration type. This commit also adds two specific error code enumeration types - DomainError and OutOfRange - as examples and for testing. --- error/v2/CMakeLists.txt | 63 ++++++++++++++++ error/v2/caesar/error.hpp | 5 ++ error/v2/caesar/error/domain_error.cpp | 17 +++++ error/v2/caesar/error/domain_error.hpp | 23 ++++++ error/v2/caesar/error/error_code.cpp | 1 + error/v2/caesar/error/error_code.hpp | 100 +++++++++++++++++++++++++ error/v2/caesar/error/out_of_range.cpp | 17 +++++ error/v2/caesar/error/out_of_range.hpp | 23 ++++++ error/v2/test/error_code_test.cpp | 50 +++++++++++++ 9 files changed, 299 insertions(+) create mode 100644 error/v2/CMakeLists.txt create mode 100644 error/v2/caesar/error.hpp create mode 100644 error/v2/caesar/error/domain_error.cpp create mode 100644 error/v2/caesar/error/domain_error.hpp create mode 100644 error/v2/caesar/error/error_code.cpp create mode 100644 error/v2/caesar/error/error_code.hpp create mode 100644 error/v2/caesar/error/out_of_range.cpp create mode 100644 error/v2/caesar/error/out_of_range.hpp create mode 100644 error/v2/test/error_code_test.cpp diff --git a/error/v2/CMakeLists.txt b/error/v2/CMakeLists.txt new file mode 100644 index 0000000..d272af5 --- /dev/null +++ b/error/v2/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.18) + +project(error LANGUAGES CXX) + +# Import third-party dependencies. +find_package(GTest REQUIRED CONFIG) + +# Enable some compiler warnings (supported by gcc & clang). +set(warnings + -Wall + -Wconversion + -Werror + -Wextra + -Wformat=2 + -Wold-style-cast + -Woverloaded-virtual + -Wshadow + -Wsign-conversion + -Wuninitialized + -Wunused + ) +string(REPLACE ";" " " warnings "${warnings}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${warnings}") + +add_library(error SHARED) +add_library(caesar::error ALIAS error) + +# Require C++17. +target_compile_features(error PUBLIC cxx_std_17) + +# Add sources. +set(sources + caesar/error/domain_error.cpp + caesar/error/error_code.cpp + caesar/error/out_of_range.cpp + ) +target_sources(error PRIVATE ${sources}) + +# Add include dirs. +target_include_directories( + error + PUBLIC + $ + ) + +# Add test sources. +set(tests + test/error_code_test.cpp + ) + +add_executable(error-test ${tests}) + +target_link_libraries( + error-test + PRIVATE + caesar::error + GTest::gmock_main + ) + +# Register tests with CTest. +enable_testing() +include(GoogleTest) +gtest_discover_tests(error-test) diff --git a/error/v2/caesar/error.hpp b/error/v2/caesar/error.hpp new file mode 100644 index 0000000..325990f --- /dev/null +++ b/error/v2/caesar/error.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "error/domain_error.hpp" +#include "error/error_code.hpp" +#include "error/out_of_range.hpp" diff --git a/error/v2/caesar/error/domain_error.cpp b/error/v2/caesar/error/domain_error.cpp new file mode 100644 index 0000000..eb23195 --- /dev/null +++ b/error/v2/caesar/error/domain_error.cpp @@ -0,0 +1,17 @@ +#include "domain_error.hpp" + +namespace caesar { + +const char* get_error_category(DomainError) noexcept { return "DomainError"; } + +const char* +get_error_string(DomainError e) noexcept +{ + switch (e) { + case DomainError::DivisionByZero: return "Division by zero"; + } + + return ""; +} + +} // namespace caesar diff --git a/error/v2/caesar/error/domain_error.hpp b/error/v2/caesar/error/domain_error.hpp new file mode 100644 index 0000000..d401ef2 --- /dev/null +++ b/error/v2/caesar/error/domain_error.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace caesar { + +/** + * Error code used to indicate domain errors, i.e. situations where the inputs + * are outside of the domain on which an operation is defined. + * + * \see ErrorCode + */ +enum class DomainError { + DivisionByZero = 1, +}; + +/** \private implements ErrorCode::category() */ +const char* +get_error_category(DomainError e) noexcept; + +/** \private implements ErrorCode::description() */ +const char* +get_error_string(DomainError e) noexcept; + +} // namespace caesar diff --git a/error/v2/caesar/error/error_code.cpp b/error/v2/caesar/error/error_code.cpp new file mode 100644 index 0000000..c645fc2 --- /dev/null +++ b/error/v2/caesar/error/error_code.cpp @@ -0,0 +1 @@ +#include "error_code.hpp" diff --git a/error/v2/caesar/error/error_code.hpp b/error/v2/caesar/error/error_code.hpp new file mode 100644 index 0000000..7b8c01b --- /dev/null +++ b/error/v2/caesar/error/error_code.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include "domain_error.hpp" +#include "out_of_range.hpp" + +#include + +namespace caesar { + +/** + * A type-erased error code + * + * ErrorCode facilitates interoperability between any number of domain-specific + * error codes that an application and its third-party dependencies may + * need to support. It does so by providing a common type that can be used to + * represent error code enumerations of different types. + * + * Unlike + * [`std::error_code`](https://en.cppreference.com/w/cpp/error/error_code), + * ErrorCode uses a variant/visitor idiom rather than dynamic polymorphism to + * implement type erasure. This makes extending ErrorCode more intrusive (you + * need to actually modify the class definition in order to support additional + * error code enum types). However, ErrorCode boasts a number of beneficial + * properties compared to `std::error_code`: + * + * - ErrorCode objects can be constructed in device code and can be safely + * passed between the host and device + * - Supporting custom error codes requires significantly less boilerplate + * - ErrorCode is a *LiteralType* and most operations are `constexpr` + * + * ### Example + * + * ```c++ + * ErrorCode ec = DomainError::DivisionByZero; + * + * std::cout << ec.category() << "\n"; // "DomainError" + * std::cout << ec.description() << "\n"; // "Division by zero" + * ``` + * + * \see Error + */ +class ErrorCode : private std::variant { + using Base = std::variant; + +public: + /** Integral type that can be used to represent error code values */ + using value_type = int; + + using Base::Base; + using Base::operator=; + + ErrorCode() = delete; + + /** Return the error code value. */ + constexpr value_type + value() const noexcept + { + return std::visit([](auto arg) { return static_cast(arg); }, + base()); + } + + /** Return the associated error category name. */ + const char* + category() const noexcept + { + return std::visit([](auto arg) { return get_error_category(arg); }, + base()); + } + + /** Return a message describing the error code. */ + const char* + description() const noexcept + { + return std::visit([](auto arg) { return get_error_string(arg); }, + base()); + } + + /** Compare two ErrorCode objects */ + friend constexpr bool + operator==(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return lhs.base() == rhs.base(); + } + + /** \copydoc operator==(const ErrorCode&, const ErrorCode&) */ + friend constexpr bool + operator!=(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return not(lhs == rhs); + } + +private: + constexpr const Base& + base() const noexcept + { + return static_cast(*this); + } +}; + +} // namespace caesar diff --git a/error/v2/caesar/error/out_of_range.cpp b/error/v2/caesar/error/out_of_range.cpp new file mode 100644 index 0000000..8a3ddf8 --- /dev/null +++ b/error/v2/caesar/error/out_of_range.cpp @@ -0,0 +1,17 @@ +#include "out_of_range.hpp" + +namespace caesar { + +const char* get_error_category(OutOfRange) noexcept { return "OutOfRange"; } + +const char* +get_error_string(OutOfRange e) noexcept +{ + switch (e) { + case OutOfRange::OutOfBoundsAccess: return "Out of bounds access attempted"; + } + + return ""; +} + +} // namespace caesar diff --git a/error/v2/caesar/error/out_of_range.hpp b/error/v2/caesar/error/out_of_range.hpp new file mode 100644 index 0000000..bbf7a4b --- /dev/null +++ b/error/v2/caesar/error/out_of_range.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace caesar { + +/** + * Error code used to indicate errors that are consequence of attempt to access + * elements out of a defined range. + * + * \see ErrorCode + */ +enum class OutOfRange { + OutOfBoundsAccess = 1, +}; + +/** \private implements ErrorCode::category() */ +const char* +get_error_category(OutOfRange e) noexcept; + +/** \private implements ErrorCode::description() */ +const char* +get_error_string(OutOfRange e) noexcept; + +} // namespace caesar diff --git a/error/v2/test/error_code_test.cpp b/error/v2/test/error_code_test.cpp new file mode 100644 index 0000000..719a94e --- /dev/null +++ b/error/v2/test/error_code_test.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include + +namespace cs = caesar; + +TEST(ErrorCodeTest, FromEnum) +{ + const auto e = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code = e; + + const auto value = static_cast(e); + EXPECT_EQ(error_code.value(), value); + + const std::string category = "DomainError"; + EXPECT_EQ(error_code.category(), category); +} + +TEST(ErrorCodeTest, AssignEnum) +{ + cs::ErrorCode error_code = cs::DomainError::DivisionByZero; + + const auto e = cs::OutOfRange::OutOfBoundsAccess; + error_code = e; + + const auto value = static_cast(e); + EXPECT_EQ(error_code.value(), value); + + const std::string category = "OutOfRange"; + EXPECT_EQ(error_code.category(), category); +} + +TEST(ErrorCodeTest, Description) +{ + const cs::ErrorCode error_code = cs::DomainError::DivisionByZero; + + const std::string description = "Division by zero"; + EXPECT_EQ(error_code.description(), description); +} + +TEST(ErrorCodeTest, Compare) +{ + const cs::ErrorCode error_code1 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code2 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code3 = cs::OutOfRange::OutOfBoundsAccess; + + EXPECT_TRUE(error_code1 == error_code2); + EXPECT_TRUE(error_code1 != error_code3); +} From 2061b99bb96f079ac617cc7af0a470ce69314e08 Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Wed, 30 Dec 2020 15:43:28 -0800 Subject: [PATCH 2/3] Add Error The Error class pairs an error code with information about the source location where the error occurred (filename and line number). It is implicitly constructible from an ErrorCode or any supported error code enumeration type. By default, the source location info is populated based on the call site where the Error object was constructed. This implementation relies on std::experimental::source_location (from ), which is supported in gcc >= 7 and clang >= 9, to transparently capture source code location info. This class is standardized in C++20 as std::source_location. --- error/v2/CMakeLists.txt | 2 + error/v2/caesar/error.hpp | 1 + error/v2/caesar/error/error.cpp | 1 + error/v2/caesar/error/error.hpp | 105 ++++++++++++++++++++++++++++++++ error/v2/test/error_test.cpp | 27 ++++++++ 5 files changed, 136 insertions(+) create mode 100644 error/v2/caesar/error/error.cpp create mode 100644 error/v2/caesar/error/error.hpp create mode 100644 error/v2/test/error_test.cpp diff --git a/error/v2/CMakeLists.txt b/error/v2/CMakeLists.txt index d272af5..e94b3f8 100644 --- a/error/v2/CMakeLists.txt +++ b/error/v2/CMakeLists.txt @@ -32,6 +32,7 @@ target_compile_features(error PUBLIC cxx_std_17) set(sources caesar/error/domain_error.cpp caesar/error/error_code.cpp + caesar/error/error.cpp caesar/error/out_of_range.cpp ) target_sources(error PRIVATE ${sources}) @@ -46,6 +47,7 @@ target_include_directories( # Add test sources. set(tests test/error_code_test.cpp + test/error_test.cpp ) add_executable(error-test ${tests}) diff --git a/error/v2/caesar/error.hpp b/error/v2/caesar/error.hpp index 325990f..daf8a9c 100644 --- a/error/v2/caesar/error.hpp +++ b/error/v2/caesar/error.hpp @@ -2,4 +2,5 @@ #include "error/domain_error.hpp" #include "error/error_code.hpp" +#include "error/error.hpp" #include "error/out_of_range.hpp" diff --git a/error/v2/caesar/error/error.cpp b/error/v2/caesar/error/error.cpp new file mode 100644 index 0000000..ad01126 --- /dev/null +++ b/error/v2/caesar/error/error.cpp @@ -0,0 +1 @@ +#include "error.hpp" diff --git a/error/v2/caesar/error/error.hpp b/error/v2/caesar/error/error.hpp new file mode 100644 index 0000000..144c162 --- /dev/null +++ b/error/v2/caesar/error/error.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include "error_code.hpp" + +#include +#include + +namespace caesar { + +/** + * Describes an error encountered during processing + * + * The Error class stores an error code along with contextual information about + * where the error originated from in the source code (filename and line + * number). When constructed from an ErrorCode (or any supported error code + * enumeration type), by default, the source location information is populated + * based on the call site where the Error object was constructed. + * + * \see ErrorCode + * \see Expected + */ +class Error { + using source_location = std::experimental::source_location; + +public: + /** + * Construct a new Error object. + * + * \param[in] error_code error code + * \param[in] file source code filename where the error occurred + * \param[in] line source code line number where the error occurred + */ + constexpr Error(const ErrorCode& error_code, + const char* file, + int line) noexcept + : error_code_(error_code), file_(file), line_(line) + {} + + /** + * Construct a new Error object. + * + * The source location defaults to the call site where the Error object was + * constructed. + * + * \param[in] error_code error code + * \param[in] origin source code location where the error occurred + */ + constexpr Error( + const ErrorCode& error_code, + const source_location& origin = source_location::current()) noexcept + : Error(error_code, origin.file_name(), static_cast(origin.line())) + {} + + /** + * Construct a new Error object. + * + * The source location defaults to the call site where the Error object was + * constructed. + * + * This overload participates in overload resolution only if + * `std::is_constructible_v == true`. + * + * \tparam ErrorCodeEnum + * an error code enumeration type supported by ErrorCode + * + * \param[in] e error code enumeration object + * \param[in] origin source code location where the error occurred + */ + template>> + constexpr Error( + ErrorCodeEnum e, + const source_location& origin = source_location::current()) noexcept + : Error(ErrorCode(e), origin) + {} + + /** Return the error code. */ + constexpr const ErrorCode& + error_code() const noexcept + { + return error_code_; + } + + /** Return the source code filename where the error occurred. */ + constexpr const char* + file() const noexcept + { + return file_; + } + + /** Return the source code line number where the error occurred. */ + constexpr int + line() const noexcept + { + return line_; + } + +private: + ErrorCode error_code_; + const char* file_; + int line_; +}; + +} // namespace caesar diff --git a/error/v2/test/error_test.cpp b/error/v2/test/error_test.cpp new file mode 100644 index 0000000..27d3ae2 --- /dev/null +++ b/error/v2/test/error_test.cpp @@ -0,0 +1,27 @@ +#include + +#include + +namespace cs = caesar; + +TEST(ErrorTest, FromErrorCode) +{ + const auto error_code = cs::DomainError::DivisionByZero; + const cs::Error error = error_code; + + EXPECT_EQ(error.error_code(), error_code); + EXPECT_EQ(error.line(), 10); + EXPECT_THAT(error.file(), testing::EndsWith("error_test.cpp")); +} + +TEST(ErrorTest, AssignErrorCode) +{ + cs::Error error = cs::DomainError::DivisionByZero; + + const auto error_code = cs::OutOfRange::OutOfBoundsAccess; + error = error_code; + + EXPECT_EQ(error.error_code(), error_code); + EXPECT_EQ(error.line(), 22); + EXPECT_THAT(error.file(), testing::EndsWith("error_test.cpp")); +} From 83cf407ae72653257f387fd0fd6264ad4cc91968 Mon Sep 17 00:00:00 2001 From: Geoffrey M Gunter Date: Wed, 30 Dec 2020 21:49:54 -0800 Subject: [PATCH 3/3] Add Expected The Expected template class is a wrapper that may contain an object of the template type T or an error. It is intended to be used as a return type for operations which may fail. On success, the returned object contains the expected result. In case of failure, it instead contains an object that describes the error encountered. The expected/unexpected idiom provides a useful alternative to traditional error handling mechanisms such as exceptions and error codes. Compared to the use of exceptions, the Expected approach - can be used in device code and can be safely and easily propagated across thread boundaries - allows for inexpensive local handling of the "bad path" when an operation fails to return the expected value - improves error visibility in code review by making the possibility of error explicit in a function's return type Compared to error codes, Expected objects - do not monopolize the return channel - are not easily ignored (if the user wants to retrieve the contained value) --- error/v2/CMakeLists.txt | 12 ++- error/v2/caesar/error.hpp | 1 + error/v2/caesar/error/expected.cpp | 1 + error/v2/caesar/error/expected.hpp | 139 +++++++++++++++++++++++++++++ error/v2/test/expected_test.cpp | 80 +++++++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 error/v2/caesar/error/expected.cpp create mode 100644 error/v2/caesar/error/expected.hpp create mode 100644 error/v2/test/expected_test.cpp diff --git a/error/v2/CMakeLists.txt b/error/v2/CMakeLists.txt index e94b3f8..4680c38 100644 --- a/error/v2/CMakeLists.txt +++ b/error/v2/CMakeLists.txt @@ -3,7 +3,8 @@ cmake_minimum_required(VERSION 3.18) project(error LANGUAGES CXX) # Import third-party dependencies. -find_package(GTest REQUIRED CONFIG) +find_package(GTest REQUIRED CONFIG) +find_package(tl-expected REQUIRED CONFIG) # Enable some compiler warnings (supported by gcc & clang). set(warnings @@ -33,6 +34,7 @@ set(sources caesar/error/domain_error.cpp caesar/error/error_code.cpp caesar/error/error.cpp + caesar/error/expected.cpp caesar/error/out_of_range.cpp ) target_sources(error PRIVATE ${sources}) @@ -44,10 +46,18 @@ target_include_directories( $ ) +# Link to imported targets. +target_link_libraries( + error + PUBLIC + tl::expected + ) + # Add test sources. set(tests test/error_code_test.cpp test/error_test.cpp + test/expected_test.cpp ) add_executable(error-test ${tests}) diff --git a/error/v2/caesar/error.hpp b/error/v2/caesar/error.hpp index daf8a9c..39da6e4 100644 --- a/error/v2/caesar/error.hpp +++ b/error/v2/caesar/error.hpp @@ -3,4 +3,5 @@ #include "error/domain_error.hpp" #include "error/error_code.hpp" #include "error/error.hpp" +#include "error/expected.hpp" #include "error/out_of_range.hpp" diff --git a/error/v2/caesar/error/expected.cpp b/error/v2/caesar/error/expected.cpp new file mode 100644 index 0000000..869d274 --- /dev/null +++ b/error/v2/caesar/error/expected.cpp @@ -0,0 +1 @@ +#include "expected.hpp" diff --git a/error/v2/caesar/error/expected.hpp b/error/v2/caesar/error/expected.hpp new file mode 100644 index 0000000..47705ed --- /dev/null +++ b/error/v2/caesar/error/expected.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include "error.hpp" + +#include +#include + +namespace caesar { + +/** + * A wrapper that may contain an object of type T or an error + * + * The Expected class is intended to be used as a return type for operations + * that may fail. On success, the returned object contains the expected result. + * In case of failure, it instead contains an object that describes the error + * encountered. + * + * Compared to the use of exceptions, the Expected approach + * + * - can be used in device code and can be safely and easily propagated across + * thread boundaries + * - allows for inexpensive local handling of the "bad path" when an operation + * fails to return the expected value + * - improves error visibility in code review by making the possibility of error + * explicit in a function's return type + * + * Compared to error codes, Expected objects + * + * - do not monopolize the return channel + * - are not easily ignored (if the user wants to retrieve the contained value) + * + * The Expected class provides methods for safe and unsafe access to the + * underlying value. If an instance of the expected type was not stored, + * attempting to access it via the `Expected::value()` method causes an + * exception to be thrown. The indirection operators (`Expected::operator*()` + * and `Expected::operator->()`), however, provide unchecked access to the + * stored value. The behavior of these methods is undefined if the expected + * value is not present. (Similarly, attempting to access the error object via + * `Expected::error()` has undefined behavior if a value was stored instead.) + * + * When an Expected object is contextually converted to `bool`, the conversion + * returns `true` if the object contains a value and `false` if the object + * contains an error. + * + * Expected may not store a reference type. + * + * \tparam the expected value type + * + * ### Example + * + * ```c++ + * Expected safe_divide(int x, int y) + * { + * if (y == 0) { + * return DomainError::DivisionByZero; + * } + * return x / y; + * } + * + * auto res1 = safe_divide(6, 3); + * auto res2 = safe_divide(6, 0); + * + * std::cout << std::boolalpha << bool(res1) << "\n"; + * std::cout << std::boolalpha << bool(res2) << "\n"; + * + * std::cout << res1.value() << "\n"; + * std::cout << res2.error().error_code().description() << "\n"; + * + * // std::cout << res2.value() << "\n"; // causes an exception to be thrown + * ``` + * + * Possible output: + * + * ``` + * true + * false + * 2 + * Division by zero + * ``` + * + * \see Error + */ +template +class Expected : public tl::expected { + using Base = tl::expected; + using source_location = std::experimental::source_location; + +public: + using Base::Base; + using Base::operator=; + + Expected() = delete; + + /** Construct a new Expected object containing an error. */ + constexpr Expected(const Error& error) noexcept + : Base(tl::unexpected(error)) + {} + + /** + * Construct a new Expected object containing an error. + * + * The source location defaults to the call site where the Expected object + * was constructed. + * + * \param[in] error_code error code + * \param[in] origin source code location where the error occurred + */ + constexpr Expected( + const ErrorCode& error_code, + const source_location& origin = source_location::current()) noexcept + : Expected(Error(error_code, origin)) + {} + + /** + * Construct a new Expected object containing an error. + * + * The source location defaults to the call site where the Expected object + * was constructed. + * + * This overload participates in overload resolution only if + * `std::is_constructible_v == true`. + * + * \tparam ErrorCodeEnum + * an error code enumeration type supported by ErrorCode + * + * \param[in] e error code enumeration object + * \param[in] origin source code location where the error occurred + */ + template>> + constexpr Expected( + ErrorCodeEnum e, + const source_location& origin = source_location::current()) noexcept + : Expected(Error(e, origin)) + {} +}; + +} // namespace caesar diff --git a/error/v2/test/expected_test.cpp b/error/v2/test/expected_test.cpp new file mode 100644 index 0000000..64484df --- /dev/null +++ b/error/v2/test/expected_test.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include + +namespace cs = caesar; + +static cs::Expected +safe_divide(int x, int y) +{ + if (y == 0) { + return cs::DomainError::DivisionByZero; + } + return x / y; +} + +TEST(ExpectedTest, HasValue) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result.has_value()); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(ExpectedTest, Truthiness) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result); + } +} + +TEST(ExpectedTest, Value) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_EQ(result.value(), 2); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_THROW({ result.value(); }, std::exception); + } +} + +TEST(ExpectedTest, Dereference) +{ + const auto result = safe_divide(6, 3); + EXPECT_EQ(*result, 2); +} + +TEST(ExpectedTest, DereferenceMember) +{ + struct Foo { + int bar; + }; + + const cs::Expected foo = Foo{123}; + EXPECT_EQ(foo->bar, 123); +} + +TEST(ExpectedTest, Error) +{ + const auto result = safe_divide(1, 0); + const auto error = result.error(); + + EXPECT_EQ(error.error_code(), cs::DomainError::DivisionByZero); + EXPECT_EQ(error.line(), 11); + EXPECT_THAT(error.file(), testing::EndsWith("expected_test.cpp")); +}