Skip to content
Merged
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,23 @@ if(YSTDLIB_CPP_IS_TOP_LEVEL)
# installed, this file is not necessary.
include(build/deps/settings.cmake OPTIONAL)

# Use ystdlib-cpp's own path since there's no parent project.
# Note: the ending slash is needed, otherwise it gets appended to relative paths and make them
# into root paths, which is incorrect.
string(LENGTH "${CMAKE_CURRENT_LIST_DIR}/" YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE)

# If previously undefined, `BUILD_TESTING` will be set to ON.
include(CTest)
endif()

if(DEFINED YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE)
# Macro providing the length of the absolute source directory path of the project using
# ystdlib-cpp so we can output file name information with relative (rather than absolute) paths.
add_definitions(
"-DYSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE=${YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE}"
)
endif()

if(BUILD_TESTING AND YSTDLIB_CPP_BUILD_TESTING)
set(YSTDLIB_CPP_ENABLE_TESTS ON)
endif()
Expand Down
3 changes: 3 additions & 0 deletions src/ystdlib/error_handling/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ cpp_library(
NAMESPACE ystdlib
PUBLIC_HEADERS
ErrorCode.hpp
TraceableException.hpp
SourceLocation.hpp
TESTS_SOURCES
test/constants.hpp
test/test_ErrorCode.cpp
test/test_TraceableException.cpp
test/types.cpp
test/types.hpp
)
41 changes: 41 additions & 0 deletions src/ystdlib/error_handling/SourceLocation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP
#define YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP

#include <iostream>
#include <source_location>
#include <sstream>
#include <string>

namespace ystdlib::error_handling {
class SourceLocation : public std::source_location {
public:
// Constructor
explicit SourceLocation(std::source_location const& where)
: std::source_location{where},
m_formatted_location{format_location(*this)} {}

#ifdef YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE
[[nodiscard]] auto file_name() const noexcept -> char const* {
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
return std::source_location::file_name() + YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE;
}
#endif

[[nodiscard]] auto str() const noexcept -> std::string const& { return m_formatted_location; }

friend auto operator<<(std::ostream& os, SourceLocation const& where) -> std::ostream& {
return os << where.str();
}

private:
static auto format_location(SourceLocation const& where) -> std::string {
std::ostringstream oss;
oss << where.file_name() << "(" << where.line() << ":" << where.column() << "), function `"
<< where.function_name() << "`";
return oss.str();
}

std::string m_formatted_location;
};
} // namespace ystdlib::error_handling
#endif // YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP
84 changes: 84 additions & 0 deletions src/ystdlib/error_handling/TraceableException.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
#define YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP

#include <concepts>
#include <exception>
#include <source_location>
#include <string>
#include <system_error>
#include <utility>

#include "SourceLocation.hpp"

namespace ystdlib::error_handling {
/**
* Concept that defines a template parameter of an integer-based error code enumeration.
* @tparam Type
*/
template <typename Type>
concept ErrorCodeType
= std::same_as<Type, std::error_code> || std::convertible_to<Type, std::error_code>;

/**
* An exception class that is thrown with an `std::error_code`.
*
* This class extends `std::exception` and can be thrown with an `std::error_code` argument. It also
* provides additional information to aid in debugging by storing details in `std::source_location`,
* including the function name, file name, and line number of the throwing location.
*
* @see std::source_location::file_name()
* @see std::source_location::function_name()
* @see std::source_location::line()
*/
template <typename ErrorCodeType>
class TraceableException : public std::exception {
public:
// Constructors
explicit TraceableException(
ErrorCodeType error_code,
std::source_location const& where = std::source_location::current()
)
: m_error_code{std::move(error_code)},
m_where{where} {
m_what = m_where.str();
}

explicit TraceableException(
ErrorCodeType error_code,
std::string what,
std::source_location const& where = std::source_location::current()
)
: m_error_code{std::move(error_code)},
m_what{std::move(what)},
m_where{where} {}

// Methods implementing std::exception
[[nodiscard]] auto what() const noexcept -> char const* override { return m_what.c_str(); }

// Methods
[[nodiscard]] auto error_code() const -> ErrorCodeType { return m_error_code; }

[[nodiscard]] auto what() -> std::string& { return m_what; }

[[nodiscard]] auto where() const noexcept -> SourceLocation const& { return m_where; }

private:
// Variables
ErrorCodeType m_error_code;
std::string m_what;
SourceLocation m_where;
};

} // namespace ystdlib::error_handling

/**
* The macro to define a `TraceableException` class with the given class name T.
*/
// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
#define YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(T, E) \
class T : public ystdlib::error_handling::TraceableException<E> { \
using ystdlib::error_handling::TraceableException<E>::TraceableException; \
}
// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)

#endif // YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
68 changes: 68 additions & 0 deletions src/ystdlib/error_handling/test/test_TraceableException.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <cassert>
#include <cstring>

#include <ystdlib/error_handling/TraceableException.hpp>

#include <catch2/catch_test_macros.hpp>

#include "types.hpp"

using ystdlib::error_handling::ErrorCodeType;
using ystdlib::error_handling::TraceableException;

namespace {
constexpr auto cCustomFailureDescription{"This operation failed due to invalid args."};
constexpr auto cCurrentFileName{"src/ystdlib/error_handling/test/test_TraceableException.cpp"};
constexpr auto cSuccessFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_success()"
};
constexpr auto cFailureFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_failure()"
};
} // namespace

namespace ystdlib::error_handling::test {
class Worker {
public:
YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(OperationFailed, BinaryErrorCode);

static auto execute_with_success() -> void {
throw OperationFailed(BinaryErrorCode{BinaryErrorCodeEnum::Success});
}

static auto execute_with_failure() -> void {
throw OperationFailed(
BinaryErrorCode{BinaryErrorCodeEnum::Failure},
cCustomFailureDescription
);
}
};
} // namespace ystdlib::error_handling::test

namespace {
template <ErrorCodeType E, typename Callable>
[[nodiscard]] auto capture_exception(Callable&& f) -> TraceableException<E>;

template <ErrorCodeType E, typename Callable>
auto capture_exception(Callable&& f) -> TraceableException<E> {
try {
std::forward<Callable>(f)();
} catch (TraceableException<E>& e) {
return e;
}
assert(false && "The function is expected to throw.");
}
} // namespace

namespace ystdlib::error_handling::test {
TEST_CASE("test_traceable_exception", "[error_handling][TraceableException]") {
auto const ex_success{capture_exception<BinaryErrorCode>(Worker::execute_with_success)};
REQUIRE((0 == std::strcmp(ex_success.where().file_name(), cCurrentFileName)));
REQUIRE((0 == std::strcmp(ex_success.where().function_name(), cSuccessFuncName)));

auto const ex_failure{capture_exception<BinaryErrorCode>(Worker::execute_with_failure)};
REQUIRE((0 == std::strcmp(ex_failure.what(), cCustomFailureDescription)));
REQUIRE((0 == std::strcmp(ex_failure.where().file_name(), cCurrentFileName)));
REQUIRE((0 == std::strcmp(ex_failure.where().function_name(), cFailureFuncName)));
}
} // namespace ystdlib::error_handling::test