From ec0821d95c2fec653041c8b4b7af8eaac6cfb111 Mon Sep 17 00:00:00 2001 From: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:00:18 -0500 Subject: [PATCH] feat(core): Add `ErrorCode` template to standardize conversion of user-defined error code enums to `std::error_code`. (#486) --- components/core/CMakeLists.txt | 2 + .../core/src/clp/error_handling/ErrorCode.hpp | 150 ++++++++++++++++++ components/core/tests/test-error_handling.cpp | 141 ++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 components/core/src/clp/error_handling/ErrorCode.hpp create mode 100644 components/core/tests/test-error_handling.cpp diff --git a/components/core/CMakeLists.txt b/components/core/CMakeLists.txt index 1b4fdb1be..193d167d8 100644 --- a/components/core/CMakeLists.txt +++ b/components/core/CMakeLists.txt @@ -368,6 +368,7 @@ set(SOURCE_FILES_unitTest src/clp/DictionaryEntry.hpp src/clp/DictionaryReader.hpp src/clp/DictionaryWriter.hpp + src/clp/error_handling/ErrorCode.hpp src/clp/EncodedVariableInterpreter.cpp src/clp/EncodedVariableInterpreter.hpp src/clp/ErrorCode.hpp @@ -551,6 +552,7 @@ set(SOURCE_FILES_unitTest tests/test-clp_s-end_to_end.cpp tests/test-EncodedVariableInterpreter.cpp tests/test-encoding_methods.cpp + tests/test-error_handling.cpp tests/test-ffi_IrUnitHandlerInterface.cpp tests/test-ffi_KeyValuePairLogEvent.cpp tests/test-ffi_SchemaTree.cpp diff --git a/components/core/src/clp/error_handling/ErrorCode.hpp b/components/core/src/clp/error_handling/ErrorCode.hpp new file mode 100644 index 000000000..2612e7768 --- /dev/null +++ b/components/core/src/clp/error_handling/ErrorCode.hpp @@ -0,0 +1,150 @@ +#ifndef CLP_ERROR_HANDLING_ERRORCODE_HPP +#define CLP_ERROR_HANDLING_ERRORCODE_HPP + +#include +#include +#include +#include + +namespace clp::error_handling { +/** + * Concept that defines a template parameter of an integer-based error code enumeration. + * @tparam Type + */ +template +concept ErrorCodeEnumType = std::is_enum_v && requires(Type type) { + { + static_cast>(type) + } -> std::convertible_to; +}; + +/** + * Template that defines a `std::error_category` of the given set of error code enumeration. + * @tparam ErrorCodeEnum + */ +template +class ErrorCategory : public std::error_category { +public: + // Methods implementing `std::error_category` + /** + * Gets the error category name. + * Note: A specialization must be explicitly implemented for each valid `ErrorCodeEnum`. + * @return The name of the error category. + */ + [[nodiscard]] auto name() const noexcept -> char const* override; + + /** + * Gets the descriptive message associated with the given error. + * @param error_num + * @return The descriptive message for the error. + */ + [[nodiscard]] auto message(int error_num) const -> std::string override { + return message(static_cast(error_num)); + } + + /** + * @param error_num + * @param condition + * @return Whether the error condition of the given error matches the given condition. + */ + [[nodiscard]] auto equivalent( + int error_num, + std::error_condition const& condition + ) const noexcept -> bool override { + return equivalent(static_cast(error_num), condition); + } + + // Methods + /** + * Gets the descriptive message associated with the given error. + * Note: A specialization must be explicitly implemented for each valid `ErrorCodeEnum`. + * @param error_enum. + * @return The descriptive message for the error. + */ + [[nodiscard]] auto message(ErrorCodeEnum error_enum) const -> std::string; + + /** + * Note: A specialization can be implemented to create error enum to error condition mappings. + * @param error_num + * @param condition + * @return Whether the error condition of the given error matches the given condition. + */ + [[nodiscard]] auto equivalent( + ErrorCodeEnum error_enum, + std::error_condition const& condition + ) const noexcept -> bool; +}; + +/** + * Template class that defines an error code. An error code is represented by a error enum value and + * the associated error category. This template class is designed to be `std::error_code` + * compatible, meaning that every instance of this class can be used to construct a corresponded + * `std::error_code` instance, or compare with a `std::error_code` instance to inspect a specific + * error. + * @tparam ErrorCodeEnum + */ +template +class ErrorCode { +public: + // Constructor + ErrorCode(ErrorCodeEnum error) : m_error{error} {} + + /** + * @return The underlying error code enum. + */ + [[nodiscard]] auto get_error() const -> ErrorCodeEnum { return m_error; } + + /** + * @return The error code as an error number. + */ + [[nodiscard]] auto get_error_num() const -> int { return static_cast(m_error); } + + /** + * @return The reference to the singleton of the corresponded error category. + */ + [[nodiscard]] constexpr static auto get_category() -> ErrorCategory const& { + return cCategory; + } + +private: + static inline ErrorCategory const cCategory; + + ErrorCodeEnum m_error; +}; + +/** + * @tparam ErrorCodeEnum + * @param error + * @return Constructed `std::error_code` from the given `ErrorCode` instance. + */ +template +[[nodiscard]] auto make_error_code(ErrorCode error) -> std::error_code; + +template +auto ErrorCategory::equivalent( + ErrorCodeEnum error_enum, + std::error_condition const& condition +) const noexcept -> bool { + return std::error_category::default_error_condition(static_cast(error_enum)) == condition; +} + +template +auto make_error_code(ErrorCode error) -> std::error_code { + return {error.get_error_num(), ErrorCode::get_category()}; +} +} // namespace clp::error_handling + +/** + * The macro to create a specialization of `std::is_error_code_enum` for a given type T. Only types + * that are marked with this macro will be considered as a valid CLP error code enum, and thus used + * to specialize `ErrorCode` and `ErrorCategory` templates. + */ +// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage) +#define CLP_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(T) \ + template <> \ + struct std::is_error_code_enum> : std::true_type { \ + static_assert(std::is_enum_v); \ + }; +// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage) + +#endif // CLP_ERROR_HANDLING_ERRORCODE_HPP diff --git a/components/core/tests/test-error_handling.cpp b/components/core/tests/test-error_handling.cpp new file mode 100644 index 000000000..2d640ed57 --- /dev/null +++ b/components/core/tests/test-error_handling.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../src/clp/error_handling/ErrorCode.hpp" + +using clp::error_handling::ErrorCategory; +using clp::error_handling::ErrorCode; +using std::string; +using std::string_view; + +namespace { +enum class AlwaysSuccessErrorCodeEnum : uint8_t { + Success = 0 +}; + +enum class BinaryErrorCodeEnum : uint8_t { + Success = 0, + Failure +}; + +using AlwaysSuccessErrorCode = ErrorCode; +using AlwaysSuccessErrorCategory = ErrorCategory; +using BinaryErrorCode = ErrorCode; +using BinaryErrorCategory = ErrorCategory; + +constexpr string_view cAlwaysSuccessErrorCategoryName{"Always Success Error Code"}; +constexpr string_view cBinaryTestErrorCategoryName{"Binary Error Code"}; +constexpr string_view cSuccessErrorMsg{"Success"}; +constexpr string_view cFailureErrorMsg{"Failure"}; +constexpr string_view cUnrecognizedErrorCode{"Unrecognized Error Code"}; +constexpr std::array cFailureConditions{std::errc::not_connected, std::errc::timed_out}; +constexpr std::array cNoneFailureConditions{std::errc::broken_pipe, std::errc::address_in_use}; +} // namespace + +CLP_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(AlwaysSuccessErrorCodeEnum); +CLP_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(BinaryErrorCodeEnum); + +template <> +auto AlwaysSuccessErrorCategory::name() const noexcept -> char const* { + return cAlwaysSuccessErrorCategoryName.data(); +} + +template <> +auto AlwaysSuccessErrorCategory::message(AlwaysSuccessErrorCodeEnum error_enum) const -> string { + switch (error_enum) { + case AlwaysSuccessErrorCodeEnum::Success: + return string{cSuccessErrorMsg}; + default: + return string{cUnrecognizedErrorCode}; + } +} + +template <> +auto BinaryErrorCategory::name() const noexcept -> char const* { + return cBinaryTestErrorCategoryName.data(); +} + +template <> +auto BinaryErrorCategory::message(BinaryErrorCodeEnum error_enum) const -> string { + switch (error_enum) { + case BinaryErrorCodeEnum::Success: + return string{cSuccessErrorMsg}; + case BinaryErrorCodeEnum::Failure: + return string{cFailureErrorMsg}; + default: + return string{cUnrecognizedErrorCode}; + } +} + +template <> +auto BinaryErrorCategory::equivalent( + BinaryErrorCodeEnum error_enum, + std::error_condition const& condition +) const noexcept -> bool { + switch (error_enum) { + case BinaryErrorCodeEnum::Failure: + return std::any_of( + cFailureConditions.cbegin(), + cFailureConditions.cend(), + [&](auto failure_condition) -> bool { return condition == failure_condition; } + ); + default: + return false; + } +} + +TEST_CASE("test_error_code_implementation", "[error_handling][ErrorCode]") { + // Test error codes within the same error category + BinaryErrorCode const success{BinaryErrorCodeEnum::Success}; + std::error_code const success_error_code{success}; + REQUIRE((success == success_error_code)); + REQUIRE((cSuccessErrorMsg == success_error_code.message())); + REQUIRE((BinaryErrorCode::get_category() == success_error_code.category())); + REQUIRE((cBinaryTestErrorCategoryName == success_error_code.category().name())); + + BinaryErrorCode const failure{BinaryErrorCodeEnum::Failure}; + std::error_code const failure_error_code{failure}; + REQUIRE((failure == failure_error_code)); + REQUIRE((cFailureErrorMsg == failure_error_code.message())); + REQUIRE((BinaryErrorCode::get_category() == failure_error_code.category())); + REQUIRE((cBinaryTestErrorCategoryName == failure_error_code.category().name())); + std::for_each( + cFailureConditions.cbegin(), + cFailureConditions.cend(), + [&](auto failure_condition) { REQUIRE((failure_error_code == failure_condition)); } + ); + std::for_each( + cNoneFailureConditions.cbegin(), + cNoneFailureConditions.cend(), + [&](auto none_failure_condition) { + REQUIRE((failure_error_code != none_failure_condition)); + } + ); + + REQUIRE((success_error_code != failure_error_code)); + REQUIRE((success_error_code.category() == failure_error_code.category())); + + AlwaysSuccessErrorCode const always_success{AlwaysSuccessErrorCodeEnum::Success}; + std::error_code const always_success_error_code{always_success}; + REQUIRE((always_success_error_code == always_success)); + REQUIRE((cSuccessErrorMsg == always_success_error_code.message())); + REQUIRE((AlwaysSuccessErrorCode::get_category() == always_success_error_code.category())); + REQUIRE((cAlwaysSuccessErrorCategoryName == always_success_error_code.category().name())); + + // Compare error codes from different error category + // Error codes that have the same value or message won't be the same with each other if they are + // from different error categories. + REQUIRE((success_error_code.value() == always_success_error_code.value())); + REQUIRE((success_error_code.message() == always_success_error_code.message())); + REQUIRE((success_error_code.category() != always_success_error_code.category())); + REQUIRE((success_error_code != always_success_error_code)); + REQUIRE((AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} != success_error_code)); + REQUIRE((BinaryErrorCode{BinaryErrorCodeEnum::Success} != always_success_error_code)); +}