Skip to content

Commit

Permalink
feat(core): Add ErrorCode template to standardize conversion of use…
Browse files Browse the repository at this point in the history
…r-defined error code enums to `std::error_code`. (y-scope#486)
  • Loading branch information
LinZhihao-723 authored Dec 2, 2024
1 parent 9439d30 commit ec0821d
Show file tree
Hide file tree
Showing 3 changed files with 293 additions and 0 deletions.
2 changes: 2 additions & 0 deletions components/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
150 changes: 150 additions & 0 deletions components/core/src/clp/error_handling/ErrorCode.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#ifndef CLP_ERROR_HANDLING_ERRORCODE_HPP
#define CLP_ERROR_HANDLING_ERRORCODE_HPP

#include <concepts>
#include <string>
#include <system_error>
#include <type_traits>

namespace clp::error_handling {
/**
* Concept that defines a template parameter of an integer-based error code enumeration.
* @tparam Type
*/
template <typename Type>
concept ErrorCodeEnumType = std::is_enum_v<Type> && requires(Type type) {
{
static_cast<std::underlying_type_t<Type>>(type)
} -> std::convertible_to<int>;
};

/**
* Template that defines a `std::error_category` of the given set of error code enumeration.
* @tparam ErrorCodeEnum
*/
template <ErrorCodeEnumType ErrorCodeEnum>
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<ErrorCodeEnum>(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<ErrorCodeEnum>(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 <ErrorCodeEnumType ErrorCodeEnum>
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<int>(m_error); }

/**
* @return The reference to the singleton of the corresponded error category.
*/
[[nodiscard]] constexpr static auto get_category() -> ErrorCategory<ErrorCodeEnum> const& {
return cCategory;
}

private:
static inline ErrorCategory<ErrorCodeEnum> const cCategory;

ErrorCodeEnum m_error;
};

/**
* @tparam ErrorCodeEnum
* @param error
* @return Constructed `std::error_code` from the given `ErrorCode` instance.
*/
template <typename ErrorCodeEnum>
[[nodiscard]] auto make_error_code(ErrorCode<ErrorCodeEnum> error) -> std::error_code;

template <ErrorCodeEnumType ErrorCodeEnum>
auto ErrorCategory<ErrorCodeEnum>::equivalent(
ErrorCodeEnum error_enum,
std::error_condition const& condition
) const noexcept -> bool {
return std::error_category::default_error_condition(static_cast<int>(error_enum)) == condition;
}

template <typename ErrorCodeEnum>
auto make_error_code(ErrorCode<ErrorCodeEnum> error) -> std::error_code {
return {error.get_error_num(), ErrorCode<ErrorCodeEnum>::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<clp::error_handling::ErrorCode<T>> : std::true_type { \
static_assert(std::is_enum_v<T>); \
};
// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)

#endif // CLP_ERROR_HANDLING_ERRORCODE_HPP
141 changes: 141 additions & 0 deletions components/core/tests/test-error_handling.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <string_view>
#include <system_error>
#include <type_traits>

#include <Catch2/single_include/catch2/catch.hpp>

#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<AlwaysSuccessErrorCodeEnum>;
using AlwaysSuccessErrorCategory = ErrorCategory<AlwaysSuccessErrorCodeEnum>;
using BinaryErrorCode = ErrorCode<BinaryErrorCodeEnum>;
using BinaryErrorCategory = ErrorCategory<BinaryErrorCodeEnum>;

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));
}

0 comments on commit ec0821d

Please sign in to comment.