From 8ae3df0bb2bafeae3bba65ef34cb052978e74154 Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Tue, 16 Sep 2025 17:58:32 +0300 Subject: [PATCH] test(os-error): add coverage and docs for system error macros - document LOGIT_SYSERR_* usage and customization options in both READMEs\n- extend Doxygen groups with system error macro guidance\n- add os_error_macros_test.cpp to verify logged errno/GetLastError details --- README-RU.md | 31 +++++ README.md | 31 +++++ docs/groups.dox | 14 +++ include/logit_cpp/logit/LogMacros.hpp | 112 ++++++++++++++++++ include/logit_cpp/logit/config.hpp | 30 +++++ .../logit/detail/SystemErrorMacros.hpp | 85 +++++++++++++ tests/os_error_macros_test.cpp | 91 ++++++++++++++ 7 files changed, 394 insertions(+) create mode 100644 include/logit_cpp/logit/detail/SystemErrorMacros.hpp create mode 100644 tests/os_error_macros_test.cpp diff --git a/README-RU.md b/README-RU.md index e4e8acb..40b567d 100644 --- a/README-RU.md +++ b/README-RU.md @@ -67,6 +67,37 @@ void short_names_demo() { Самодостаточный пример, который объединяет настройки и намеренно завершает работу после fatal-сообщения, расположен в `examples/example_logit_minimal_crash.cpp`. +### Макросы системных ошибок + +`LOGIT_SYSERR_` захватывает текущее значение `errno` (или `GetLastError()` в Windows) и добавляет расшифровку ошибки к исходному сообщению, чтобы в логе оставался как контекст, так и код сбоя. При необходимости можно явно выбрать платформу через `LOGIT_PERROR_` или `LOGIT_WINERR_`. + +```cpp +#include +#include + +int main() { + LOGIT_ADD_CONSOLE_DEFAULT(); + + if (::open("missing.cfg", O_RDONLY) == -1) { + LOGIT_SYSERR_ERROR("Не удалось открыть конфигурацию"); + } + + LOGIT_WAIT(); +} +``` + +Формат суффикса можно изменить на этапе компиляции с помощью макросов из `config.hpp`: + +```cpp +#define LOGIT_OS_ERROR_JOIN " <- " +#define LOGIT_POSIX_ERROR_PATTERN "[%s] errno=%d (%s)" +#define LOGIT_WINDOWS_ERROR_PATTERN "[%s] GetLastError=%lu (%s)" +#include + +// ... далее в коде ... +LOGIT_SYSERR_ERROR("Ошибка удаления временного каталога"); +``` + ## Возможности - **Гибкое форматирование логов**: diff --git a/README.md b/README.md index 1c81ae5..ba989e3 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,37 @@ void short_names_demo() { For a standalone program that brings everything together and intentionally aborts after logging a fatal message, check `examples/example_logit_minimal_crash.cpp`. +### System error helpers + +`LOGIT_SYSERR_` captures the current `errno` (or `GetLastError()` on Windows) and appends the decoded information to the message, so failure details stay attached to the original context. The lower-level `LOGIT_PERROR_` and `LOGIT_WINERR_` families are also available if you want to explicitly choose the platform macro. + +```cpp +#include +#include + +int main() { + LOGIT_ADD_CONSOLE_DEFAULT(); + + if (::open("missing.cfg", O_RDONLY) == -1) { + LOGIT_SYSERR_ERROR("Failed to open configuration"); + } + + LOGIT_WAIT(); +} +``` + +The error suffix can be customised at compile time via `config.hpp` macros: + +```cpp +#define LOGIT_OS_ERROR_JOIN " <- " +#define LOGIT_POSIX_ERROR_PATTERN "[%s] errno=%d (%s)" +#define LOGIT_WINDOWS_ERROR_PATTERN "[%s] GetLastError=%lu (%s)" +#include + +// ... later in the code ... +LOGIT_SYSERR_ERROR("Deleting temp directory failed"); +``` + --- ## Features diff --git a/docs/groups.dox b/docs/groups.dox index d7e9aa1..f5464a8 100644 --- a/docs/groups.dox +++ b/docs/groups.dox @@ -26,6 +26,20 @@ int x = 42; LOGIT_DEBUG_IF(x > 0, "x is positive: ", x); \endcode +\par System Error Logging +Use `LOGIT_PERROR_` on POSIX and `LOGIT_WINERR_` on Windows to automatically +attach decoded system error codes to your message. The cross-platform +`LOGIT_SYSERR_` macro aliases to the appropriate variant. +\code +int fd = ::open("missing.cfg", O_RDONLY); +if (fd == -1) { + LOGIT_SYSERR_ERROR("Failed to open configuration"); +} +\endcode +Customise the suffix by overriding configuration macros such as +`LOGIT_OS_ERROR_JOIN`, `LOGIT_POSIX_ERROR_PATTERN`, or `LOGIT_WINDOWS_ERROR_PATTERN` +before including ``. + \par Logger Management \code LOGIT_SET_LOGGER_ENABLED(1, false); // Disable logger at index 1 diff --git a/include/logit_cpp/logit/LogMacros.hpp b/include/logit_cpp/logit/LogMacros.hpp index c824163..002a0df 100644 --- a/include/logit_cpp/logit/LogMacros.hpp +++ b/include/logit_cpp/logit/LogMacros.hpp @@ -6,6 +6,9 @@ #include #endif +#include "config.hpp" +#include "utils/format.hpp" + /// \file LogMacros.hpp /// \brief Provides various logging macros for different log levels and options. @@ -65,6 +68,115 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT # define LOGIT_IF_COMPILED_LEVEL(level) if constexpr (LOGIT_COMPILED_LEVEL <= static_cast(level)) #endif +//------------------------------------------------------------------------------ +// System error logging helpers + +#include "detail/SystemErrorMacros.hpp" + +//------------------------------------------------------------------------------ +// Platform-specific error logging macros + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_TRACE +#define LOGIT_PERROR_TRACE(message) LOGIT_DETAIL_PERROR(TRACE, message) +#else +#define LOGIT_PERROR_TRACE(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_DEBUG +#define LOGIT_PERROR_DEBUG(message) LOGIT_DETAIL_PERROR(DEBUG, message) +#else +#define LOGIT_PERROR_DEBUG(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_INFO +#define LOGIT_PERROR_INFO(message) LOGIT_DETAIL_PERROR(INFO, message) +#else +#define LOGIT_PERROR_INFO(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_WARN +#define LOGIT_PERROR_WARN(message) LOGIT_DETAIL_PERROR(WARN, message) +#else +#define LOGIT_PERROR_WARN(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_ERROR +#define LOGIT_PERROR_ERROR(message) LOGIT_DETAIL_PERROR(ERROR, message) +#else +#define LOGIT_PERROR_ERROR(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_FATAL +#define LOGIT_PERROR_FATAL(message) LOGIT_DETAIL_PERROR(FATAL, message) +#else +#define LOGIT_PERROR_FATAL(message) do { } while (0) +#endif + +#if defined(_WIN32) + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_TRACE +#define LOGIT_WINERR_TRACE(message) LOGIT_DETAIL_WINERR(TRACE, message) +#else +#define LOGIT_WINERR_TRACE(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_DEBUG +#define LOGIT_WINERR_DEBUG(message) LOGIT_DETAIL_WINERR(DEBUG, message) +#else +#define LOGIT_WINERR_DEBUG(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_INFO +#define LOGIT_WINERR_INFO(message) LOGIT_DETAIL_WINERR(INFO, message) +#else +#define LOGIT_WINERR_INFO(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_WARN +#define LOGIT_WINERR_WARN(message) LOGIT_DETAIL_WINERR(WARN, message) +#else +#define LOGIT_WINERR_WARN(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_ERROR +#define LOGIT_WINERR_ERROR(message) LOGIT_DETAIL_WINERR(ERROR, message) +#else +#define LOGIT_WINERR_ERROR(message) do { } while (0) +#endif + +#if LOGIT_COMPILED_LEVEL <= LOGIT_LEVEL_FATAL +#define LOGIT_WINERR_FATAL(message) LOGIT_DETAIL_WINERR(FATAL, message) +#else +#define LOGIT_WINERR_FATAL(message) do { } while (0) +#endif + +#else // defined(_WIN32) + +#define LOGIT_WINERR_TRACE(message) do { } while (0) +#define LOGIT_WINERR_DEBUG(message) do { } while (0) +#define LOGIT_WINERR_INFO(message) do { } while (0) +#define LOGIT_WINERR_WARN(message) do { } while (0) +#define LOGIT_WINERR_ERROR(message) do { } while (0) +#define LOGIT_WINERR_FATAL(message) do { } while (0) + +#endif // defined(_WIN32) + +#if defined(_WIN32) +#define LOGIT_SYSERR_TRACE(message) LOGIT_WINERR_TRACE(message) +#define LOGIT_SYSERR_DEBUG(message) LOGIT_WINERR_DEBUG(message) +#define LOGIT_SYSERR_INFO(message) LOGIT_WINERR_INFO(message) +#define LOGIT_SYSERR_WARN(message) LOGIT_WINERR_WARN(message) +#define LOGIT_SYSERR_ERROR(message) LOGIT_WINERR_ERROR(message) +#define LOGIT_SYSERR_FATAL(message) LOGIT_WINERR_FATAL(message) +#else +#define LOGIT_SYSERR_TRACE(message) LOGIT_PERROR_TRACE(message) +#define LOGIT_SYSERR_DEBUG(message) LOGIT_PERROR_DEBUG(message) +#define LOGIT_SYSERR_INFO(message) LOGIT_PERROR_INFO(message) +#define LOGIT_SYSERR_WARN(message) LOGIT_PERROR_WARN(message) +#define LOGIT_SYSERR_ERROR(message) LOGIT_PERROR_ERROR(message) +#define LOGIT_SYSERR_FATAL(message) LOGIT_PERROR_FATAL(message) +#endif + //------------------------------------------------------------------------------ // Stream-based logging macros for various levels diff --git a/include/logit_cpp/logit/config.hpp b/include/logit_cpp/logit/config.hpp index 5c2acf1..758a4d1 100644 --- a/include/logit_cpp/logit/config.hpp +++ b/include/logit_cpp/logit/config.hpp @@ -151,6 +151,36 @@ #define LOGIT_TAGS_JOIN " | " #endif +/// \brief Separator appended before platform error metadata when formatting system error messages. +/// Override to customize how human-readable messages and error codes are joined in log output. +#ifndef LOGIT_OS_ERROR_JOIN +#define LOGIT_OS_ERROR_JOIN " | " +#endif + +/// \brief Format string used when appending POSIX `errno` information to a log message. +/// This macro can be overridden to change how the user message, errno value, and description are rendered. +#ifndef LOGIT_POSIX_ERROR_PATTERN +#define LOGIT_POSIX_ERROR_PATTERN "%s" LOGIT_OS_ERROR_JOIN "errno=%d (%s)" +#endif + +/// \brief Format string used when appending Windows `GetLastError` details to a log message. +/// This macro can be overridden to adjust how the original text, numeric code, and decoded message are displayed. +#ifndef LOGIT_WINDOWS_ERROR_PATTERN +#define LOGIT_WINDOWS_ERROR_PATTERN "%s" LOGIT_OS_ERROR_JOIN "GetLastError=%lu (%s)" +#endif + +/// \brief Selects the default system error formatting macro for the current platform. +/// Users may redefine this alias to integrate custom error formatting logic. +#if defined(_WIN32) +#ifndef LOGIT_SYSTEM_ERROR_PATTERN +#define LOGIT_SYSTEM_ERROR_PATTERN LOGIT_WINDOWS_ERROR_PATTERN +#endif +#else +#ifndef LOGIT_SYSTEM_ERROR_PATTERN +#define LOGIT_SYSTEM_ERROR_PATTERN LOGIT_POSIX_ERROR_PATTERN +#endif +#endif + /// \brief Separator between individual tag pairs (e.g., " " or "; "). #ifndef LOGIT_TAG_PAIR_SEP #define LOGIT_TAG_PAIR_SEP " " diff --git a/include/logit_cpp/logit/detail/SystemErrorMacros.hpp b/include/logit_cpp/logit/detail/SystemErrorMacros.hpp new file mode 100644 index 0000000..6f73af9 --- /dev/null +++ b/include/logit_cpp/logit/detail/SystemErrorMacros.hpp @@ -0,0 +1,85 @@ +#pragma once +#ifndef _LOGIT_DETAIL_SYSTEM_ERROR_MACROS_HPP_INCLUDED +#define _LOGIT_DETAIL_SYSTEM_ERROR_MACROS_HPP_INCLUDED + +#include +#include +#include + +/// \file SystemErrorMacros.hpp +/// \brief Internal helpers shared by the system error logging macros. + +#include "../config.hpp" +#include "../utils/format.hpp" + +#if defined(_WIN32) +#include +#endif + +namespace logit { +namespace detail { + +#if defined(_WIN32) +inline std::string _logit_format_winerr(DWORD code) { + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD length = ::FormatMessageA(flags, + nullptr, + code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), + 0, + nullptr); + + if (length == 0 || buffer == nullptr) { + return std::string(); + } + + std::string message(buffer, length); + ::LocalFree(buffer); + + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) { + message.pop_back(); + } + + return message; +} +#endif // defined(_WIN32) + +} // namespace detail +} // namespace logit + +#define LOGIT_DETAIL_PERROR(level_macro, message) \ + do { \ + const int _logit_errno_code = (errno); \ + const char *_logit_errno_desc = ::strerror(_logit_errno_code); \ + if (_logit_errno_desc == nullptr) { \ + _logit_errno_desc = ""; \ + } \ + const std::string _logit_errno_text(message); \ + LOGIT_PRINTF_##level_macro(LOGIT_POSIX_ERROR_PATTERN, \ + _logit_errno_text.c_str(), \ + _logit_errno_code, \ + _logit_errno_desc); \ + } while (0) + +#if defined(_WIN32) + +#define LOGIT_DETAIL_WINERR(level_macro, message) \ + do { \ + const DWORD _logit_winerr_code = ::GetLastError(); \ + const std::string _logit_winerr_text(message); \ + const std::string _logit_winerr_desc = ::logit::detail::_logit_format_winerr(_logit_winerr_code); \ + LOGIT_PRINTF_##level_macro(LOGIT_WINDOWS_ERROR_PATTERN, \ + _logit_winerr_text.c_str(), \ + static_cast(_logit_winerr_code), \ + _logit_winerr_desc.c_str()); \ + } while (0) + +#else // defined(_WIN32) + +#define LOGIT_DETAIL_WINERR(level_macro, message) do { } while (0) + +#endif // defined(_WIN32) + +#endif // _LOGIT_DETAIL_SYSTEM_ERROR_MACROS_HPP_INCLUDED diff --git a/tests/os_error_macros_test.cpp b/tests/os_error_macros_test.cpp new file mode 100644 index 0000000..a796fe0 --- /dev/null +++ b/tests/os_error_macros_test.cpp @@ -0,0 +1,91 @@ +#define LOGIT_FILE_LOGGER_PATH "." +#include + +#include +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#include +#include +#include +#endif + +namespace { +#if defined(_WIN32) +std::string format_windows_error(DWORD code) { + LPSTR buffer = nullptr; + const DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; + const DWORD length = ::FormatMessageA(flags, + nullptr, + code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), + 0, + nullptr); + std::string result; + if (length != 0 && buffer != nullptr) { + result.assign(buffer, length); + ::LocalFree(buffer); + while (!result.empty() && (result.back() == '\r' || result.back() == '\n')) { + result.pop_back(); + } + } + return result; +} +#endif +} // namespace + +int main() { + LOGIT_ADD_FILE_LOGGER_DEFAULT(); + + const std::string message_token = "Failed to open configuration"; + std::string code_token; + std::string expected_desc; + +#if defined(_WIN32) + ::SetLastError(ERROR_FILE_NOT_FOUND); + const DWORD captured_code = ::GetLastError(); + LOGIT_SYSERR_ERROR(message_token.c_str()); + code_token = "GetLastError=" + std::to_string(static_cast(captured_code)); + expected_desc = format_windows_error(captured_code); +#else + errno = 0; + int fd = ::open("/this/path/should/not/exist/logit.test", O_RDONLY); + if (fd >= 0) { + ::close(fd); + return 1; + } + const int captured_errno = errno; + expected_desc = std::strerror(captured_errno); + errno = captured_errno; + LOGIT_SYSERR_ERROR(message_token.c_str()); + code_token = "errno=" + std::to_string(captured_errno); +#endif + + LOGIT_WAIT(); + + std::ifstream in(LOGIT_GET_LAST_FILE_PATH(0)); + if (!in.is_open()) { + LOGIT_SHUTDOWN(); + return 1; + } + + bool found = false; + std::string line; + while (std::getline(in, line)) { + if (line.find(message_token) != std::string::npos && + line.find(code_token) != std::string::npos && + (expected_desc.empty() || line.find(expected_desc) != std::string::npos)) { + found = true; + break; + } + } + + LOGIT_SHUTDOWN(); + return found ? 0 : 1; +}