Skip to content

Commit d8b72c4

Browse files
committed
Add ErrorCode and Update Traceable Exception. Add unit tests.
1 parent 518f018 commit d8b72c4

File tree

12 files changed

+414
-34
lines changed

12 files changed

+414
-34
lines changed

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS
1313

1414
option(BUILD_TESTING "If ON, unit tests will be built." ON)
1515

16+
# Macro providing the length of the absolute source directory path so we can output file name
17+
# information with relative (rather than absolute) paths.
18+
string(LENGTH "${CMAKE_SOURCE_DIR}/" SOURCE_PATH_SIZE)
19+
add_definitions("-DSOURCE_PATH_SIZE=${SOURCE_PATH_SIZE}")
20+
1621
# Import CMake helper functions
1722
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/CMake)
1823

src/ystdlib/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
add_subdirectory(testlib)
1+
add_subdirectory(error_handling)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cpp_library(
2+
NAME error_handling
3+
NAMESPACE ystdlib
4+
TESTS_SOURCES
5+
test-Defs.hpp
6+
test-Defs.cpp
7+
test-ErrorCode.cpp
8+
test-TraceableException.cpp
9+
)
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#ifndef YSTDLIB_ERROR_HANDLING_ERRORCODE_HPP
2+
#define YSTDLIB_ERROR_HANDLING_ERRORCODE_HPP
3+
4+
#include <concepts>
5+
#include <string>
6+
#include <system_error>
7+
#include <type_traits>
8+
9+
namespace ystdlib::error_handling {
10+
/**
11+
* Concept that defines a template parameter of an integer-based error code enumeration.
12+
* @tparam Type
13+
*/
14+
template <typename Type>
15+
concept ErrorCodeEnumType = std::is_enum_v<Type> && requires(Type type) {
16+
{
17+
static_cast<std::underlying_type_t<Type>>(type)
18+
} -> std::convertible_to<int>;
19+
};
20+
21+
/**
22+
* Template that defines a `std::error_category` of the given set of error code enumeration.
23+
* @tparam ErrorCodeEnum
24+
*/
25+
template <ErrorCodeEnumType ErrorCodeEnum>
26+
class ErrorCategory : public std::error_category {
27+
public:
28+
// Methods implementing `std::error_category`
29+
/**
30+
* Gets the error category name.
31+
* Note: A specialization must be explicitly implemented for each valid `ErrorCodeEnum`.
32+
* @return The name of the error category.
33+
*/
34+
[[nodiscard]] auto name() const noexcept -> char const* override;
35+
36+
/**
37+
* Gets the descriptive message associated with the given error.
38+
* @param error_num
39+
* @return The descriptive message for the error.
40+
*/
41+
[[nodiscard]] auto message(int error_num) const -> std::string override {
42+
return message(static_cast<ErrorCodeEnum>(error_num));
43+
}
44+
45+
/**
46+
* @param error_num
47+
* @param condition
48+
* @return Whether the error condition of the given error matches the given condition.
49+
*/
50+
[[nodiscard]] auto
51+
equivalent(int error_num, std::error_condition const& condition) const noexcept
52+
-> bool override {
53+
return equivalent(static_cast<ErrorCodeEnum>(error_num), condition);
54+
}
55+
56+
// Methods
57+
/**
58+
* Gets the descriptive message associated with the given error.
59+
* Note: A specialization must be explicitly implemented for each valid `ErrorCodeEnum`.
60+
* @param error_enum.
61+
* @return The descriptive message for the error.
62+
*/
63+
[[nodiscard]] auto message(ErrorCodeEnum error_enum) const -> std::string;
64+
65+
/**
66+
* Note: A specialization can be implemented to create error enum to error condition mappings.
67+
* @param error_num
68+
* @param condition
69+
* @return Whether the error condition of the given error matches the given condition.
70+
*/
71+
[[nodiscard]] auto
72+
equivalent(ErrorCodeEnum error_enum, std::error_condition const& condition) const noexcept
73+
-> bool;
74+
};
75+
76+
/**
77+
* Template class that defines an error code. An error code is represented by a error enum value and
78+
* the associated error category. This template class is designed to be `std::error_code`
79+
* compatible, meaning that every instance of this class can be used to construct a corresponded
80+
* `std::error_code` instance, or compare with a `std::error_code` instance to inspect a specific
81+
* error.
82+
* @tparam ErrorCodeEnum
83+
*/
84+
template <ErrorCodeEnumType ErrorCodeEnum>
85+
class ErrorCode {
86+
public:
87+
// Constructor
88+
ErrorCode(ErrorCodeEnum error) : m_error{error} {}
89+
90+
/**
91+
* @return The underlying error code enum.
92+
*/
93+
[[nodiscard]] auto get_error() const -> ErrorCodeEnum { return m_error; }
94+
95+
/**
96+
* @return The error code as an error number.
97+
*/
98+
[[nodiscard]] auto get_error_num() const -> int { return static_cast<int>(m_error); }
99+
100+
/**
101+
* @return The reference to the singleton of the corresponded error category.
102+
*/
103+
[[nodiscard]] constexpr static auto get_category() -> ErrorCategory<ErrorCodeEnum> const& {
104+
return cCategory;
105+
}
106+
107+
private:
108+
static inline ErrorCategory<ErrorCodeEnum> const cCategory;
109+
110+
ErrorCodeEnum m_error;
111+
};
112+
113+
/**
114+
* @tparam ErrorCodeEnum
115+
* @param error
116+
* @return Constructed `std::error_code` from the given `ErrorCode` instance.
117+
*/
118+
template <typename ErrorCodeEnum>
119+
[[nodiscard]] auto make_error_code(ErrorCode<ErrorCodeEnum> error) -> std::error_code;
120+
121+
template <ErrorCodeEnumType ErrorCodeEnum>
122+
auto ErrorCategory<ErrorCodeEnum>::equivalent(
123+
ErrorCodeEnum error_enum,
124+
std::error_condition const& condition
125+
) const noexcept -> bool {
126+
return std::error_category::default_error_condition(static_cast<int>(error_enum)) == condition;
127+
}
128+
129+
template <typename ErrorCodeEnum>
130+
auto make_error_code(ErrorCode<ErrorCodeEnum> error) -> std::error_code {
131+
return {error.get_error_num(), ErrorCode<ErrorCodeEnum>::get_category()};
132+
}
133+
} // namespace ystdlib::error_handling
134+
135+
/**
136+
* The macro to create a specialization of `std::is_error_code_enum` for a given type T. Only types
137+
* that are marked with this macro will be considered as a valid YStdlib error code enum, and thus
138+
* used to specialize `ErrorCode` and `ErrorCategory` templates.
139+
*/
140+
// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
141+
#define YSTDLIB_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(T) \
142+
template <> \
143+
struct std::is_error_code_enum<ystdlib::error_handling::ErrorCode<T>> : std::true_type { \
144+
static_assert(std::is_enum_v<T>); \
145+
};
146+
// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
147+
148+
#endif // YSTDLIB_ERROR_HANDLING_ERRORCODE_HPP

src/ystdlib/error_handling/TraceableException.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ class TraceableException : public std::exception, public std::source_location {
2525
std::source_location const& location = std::source_location::current()
2626
)
2727
: std::source_location{location},
28-
m_error_code(error_code) {
29-
m_what = std::string{function_name()} + " operation failed.";
28+
m_error_code{error_code} {
29+
m_what = std::string{function_name()} + " failed.";
3030
}
3131

3232
// Methods
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#include "test-Defs.hpp"
2+
3+
#include <algorithm>
4+
#include <array>
5+
#include <string>
6+
#include <string_view>
7+
#include <system_error>
8+
9+
template <>
10+
auto AlwaysSuccessErrorCategory::name() const noexcept -> char const* {
11+
return cAlwaysSuccessErrorCategoryName.data();
12+
}
13+
14+
template <>
15+
auto AlwaysSuccessErrorCategory::message(AlwaysSuccessErrorCodeEnum error_enum) const
16+
-> std::string {
17+
switch (error_enum) {
18+
case AlwaysSuccessErrorCodeEnum::Success:
19+
return std::string{cSuccessErrorMsg};
20+
default:
21+
return std::string{cUnrecognizedErrorCode};
22+
}
23+
}
24+
25+
template <>
26+
auto BinaryErrorCategory::name() const noexcept -> char const* {
27+
return cBinaryTestErrorCategoryName.data();
28+
}
29+
30+
template <>
31+
auto BinaryErrorCategory::message(BinaryErrorCodeEnum error_enum) const -> std::string {
32+
switch (error_enum) {
33+
case BinaryErrorCodeEnum::Success:
34+
return std::string{cSuccessErrorMsg};
35+
case BinaryErrorCodeEnum::Failure:
36+
return std::string{cFailureErrorMsg};
37+
default:
38+
return std::string{cUnrecognizedErrorCode};
39+
}
40+
}
41+
42+
template <>
43+
auto BinaryErrorCategory::equivalent(
44+
BinaryErrorCodeEnum error_enum,
45+
std::error_condition const& condition
46+
) const noexcept -> bool {
47+
switch (error_enum) {
48+
case BinaryErrorCodeEnum::Failure:
49+
return std::ranges::any_of(
50+
cFailureConditions.cbegin(),
51+
cFailureConditions.cend(),
52+
[&](auto failure_condition) -> bool { return condition == failure_condition; }
53+
);
54+
default:
55+
return false;
56+
}
57+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include <array>
2+
#include <cstdint>
3+
#include <string_view>
4+
#include <system_error>
5+
6+
#include <ystdlib/error_handling/ErrorCode.hpp>
7+
8+
constexpr std::string_view cAlwaysSuccessErrorCategoryName{"Always Success Error Code"};
9+
constexpr std::string_view cBinaryTestErrorCategoryName{"Binary Error Code"};
10+
constexpr std::string_view cSuccessErrorMsg{"Success"};
11+
constexpr std::string_view cFailureErrorMsg{"Failure"};
12+
constexpr std::string_view cUnrecognizedErrorCode{"Unrecognized Error Code"};
13+
constexpr std::array cFailureConditions{std::errc::not_connected, std::errc::timed_out};
14+
constexpr std::array cNoneFailureConditions{std::errc::broken_pipe, std::errc::address_in_use};
15+
16+
enum class AlwaysSuccessErrorCodeEnum : uint8_t {
17+
Success = 0
18+
};
19+
20+
enum class BinaryErrorCodeEnum : uint8_t {
21+
Success = 0,
22+
Failure
23+
};
24+
25+
using AlwaysSuccessErrorCode = ystdlib::error_handling::ErrorCode<AlwaysSuccessErrorCodeEnum>;
26+
using AlwaysSuccessErrorCategory
27+
= ystdlib::error_handling::ErrorCategory<AlwaysSuccessErrorCodeEnum>;
28+
using BinaryErrorCode = ystdlib::error_handling::ErrorCode<BinaryErrorCodeEnum>;
29+
using BinaryErrorCategory = ystdlib::error_handling::ErrorCategory<BinaryErrorCodeEnum>;
30+
31+
YSTDLIB_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(AlwaysSuccessErrorCodeEnum);
32+
YSTDLIB_ERROR_HANDLING_MARK_AS_ERROR_CODE_ENUM(BinaryErrorCodeEnum);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#include <algorithm>
2+
#include <array>
3+
#include <string_view>
4+
#include <system_error>
5+
#include <type_traits>
6+
7+
#include <ystdlib/error_handling/ErrorCode.hpp>
8+
9+
#include <catch2/catch_test_macros.hpp>
10+
11+
#include "test-Defs.hpp"
12+
13+
TEST_CASE("test_error_code", "[error_handling][ErrorCode]") {
14+
// Test error codes within the same error category
15+
BinaryErrorCode const success{BinaryErrorCodeEnum::Success};
16+
std::error_code const success_error_code{success};
17+
REQUIRE((success == success_error_code));
18+
REQUIRE((cSuccessErrorMsg == success_error_code.message()));
19+
REQUIRE((BinaryErrorCode::get_category() == success_error_code.category()));
20+
REQUIRE((cBinaryTestErrorCategoryName == success_error_code.category().name()));
21+
22+
BinaryErrorCode const failure{BinaryErrorCodeEnum::Failure};
23+
std::error_code const failure_error_code{failure};
24+
REQUIRE((failure == failure_error_code));
25+
REQUIRE((cFailureErrorMsg == failure_error_code.message()));
26+
REQUIRE((BinaryErrorCode::get_category() == failure_error_code.category()));
27+
REQUIRE((cBinaryTestErrorCategoryName == failure_error_code.category().name()));
28+
29+
REQUIRE((success_error_code != failure_error_code));
30+
REQUIRE((success_error_code.category() == failure_error_code.category()));
31+
32+
AlwaysSuccessErrorCode const always_success{AlwaysSuccessErrorCodeEnum::Success};
33+
std::error_code const always_success_error_code{always_success};
34+
REQUIRE((always_success_error_code == always_success));
35+
REQUIRE((cSuccessErrorMsg == always_success_error_code.message()));
36+
REQUIRE((AlwaysSuccessErrorCode::get_category() == always_success_error_code.category()));
37+
REQUIRE((cAlwaysSuccessErrorCategoryName == always_success_error_code.category().name()));
38+
39+
// Compare error codes from different error category
40+
// Error codes that have the same value or message won't be the same with each other if they are
41+
// from different error categories.
42+
REQUIRE((success_error_code.value() == always_success_error_code.value()));
43+
REQUIRE((success_error_code.message() == always_success_error_code.message()));
44+
REQUIRE((success_error_code.category() != always_success_error_code.category()));
45+
REQUIRE((success_error_code != always_success_error_code));
46+
REQUIRE((AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} != success_error_code));
47+
REQUIRE((BinaryErrorCode{BinaryErrorCodeEnum::Success} != always_success_error_code));
48+
}
49+
50+
TEST_CASE("test_error_code_failure_condition", "[error_handling][ErrorCode]") {
51+
std::error_code const failure_error_code{BinaryErrorCode{BinaryErrorCodeEnum::Failure}};
52+
std::ranges::for_each(
53+
cFailureConditions.cbegin(),
54+
cFailureConditions.cend(),
55+
[&](auto failure_condition) { REQUIRE((failure_error_code == failure_condition)); }
56+
);
57+
std::ranges::for_each(
58+
cNoneFailureConditions.cbegin(),
59+
cNoneFailureConditions.cend(),
60+
[&](auto none_failure_condition) {
61+
REQUIRE((failure_error_code != none_failure_condition));
62+
}
63+
);
64+
}

0 commit comments

Comments
 (0)