From 28ff0e172a77c3a3fffd25382917adeee990fe9c Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Mon, 15 Apr 2024 22:37:28 +0300 Subject: [PATCH 1/7] feat: add basic re-routeable logger --- .gitignore | 3 + CMakeLists.txt | 4 +- src/logging.cpp | 88 +++++++++++++++++++ src/logging.h | 168 ++++++++++++++++++++++++++++++++++++ src/main.cpp | 11 --- tests/CMakeLists.txt | 7 +- tests/conftest.cpp | 12 ++- tests/unit/test_logging.cpp | 162 ++++++++++++++++++++++++++++++++++ 8 files changed, 433 insertions(+), 22 deletions(-) create mode 100644 src/logging.cpp create mode 100644 src/logging.h delete mode 100644 src/main.cpp create mode 100644 tests/unit/test_logging.cpp diff --git a/.gitignore b/.gitignore index cab683a..5073c82 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ # JetBrains IDE .idea/ +# VSCode IDE +.vscode/ + # build directories build/ cmake-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 75b18fa..0dc9f22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) # set the module path, used for includes set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") @@ -31,7 +31,7 @@ else() endif() # glob src -file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp) +file(GLOB_RECURSE DD_TARGET_FILES src/*.h src/*.cpp) # tests if(BUILD_TESTS) diff --git a/src/logging.cpp b/src/logging.cpp new file mode 100644 index 0000000..e3e0548 --- /dev/null +++ b/src/logging.cpp @@ -0,0 +1,88 @@ +// class header include +#include "logging.h" + +// system includes +#include +#include +#include +#include + +namespace display_device { + logger_t & + logger_t::get() { + static logger_t instance; + return instance; + } + + void + logger_t::set_log_level(const log_level_e log_level) { + m_enabled_log_level = log_level; + } + + bool + logger_t::is_log_level_enabled(const log_level_e log_level) const { + const auto log_level_v { static_cast>(log_level) }; + const auto enabled_log_level_v { static_cast>(m_enabled_log_level) }; + return log_level_v >= enabled_log_level_v; + } + + void + logger_t::set_custom_callback(callback_t callback) { + m_custom_callback = std::move(callback); + } + + void + logger_t::write(const log_level_e log_level, std::string value) { + if (!is_log_level_enabled(log_level)) { + return; + } + + if (m_custom_callback) { + m_custom_callback(log_level, std::move(value)); + return; + } + + std::stringstream stream; + { + // Time + const auto now { std::chrono::current_zone()->to_local(std::chrono::system_clock::now()) }; + stream << std::format("[{:%Y-%m-%d %X}] ", now); + + // Log level + switch (log_level) { + case log_level_e::verbose: + stream << "VERBOSE: "; + break; + case log_level_e::debug: + stream << "DEBUG: "; + break; + case log_level_e::info: + stream << "INFO: "; + break; + case log_level_e::error: + stream << "ERROR: "; + break; + default: + break; + } + + // Value + stream << value; + } + + static std::mutex log_mutex; + std::lock_guard lock { log_mutex }; + std::cout << stream.rdbuf() << std::endl; + } + + logger_t::logger_t(): + m_enabled_log_level { log_level_e::info } { + } + + log_writer_t::log_writer_t(const logger_t::log_level_e log_level): + m_log_level { log_level } {} + + log_writer_t::~log_writer_t() { + logger_t::get().write(m_log_level, m_buffer.str()); + } +} // namespace display_device diff --git a/src/logging.h b/src/logging.h new file mode 100644 index 0000000..23346f1 --- /dev/null +++ b/src/logging.h @@ -0,0 +1,168 @@ +#pragma once + +// system includes +#include +#include +#include + +namespace display_device { + /** + * @brief A singleton class for logging or re-routing logs. + * + * This class is not meant to be used directly (only for configuration). + * Instead, the MACRO below should be used throughout the code for logging. + * + * @note A lazy-evaluated, correctly-destroyed, thread-safe singleton pattern is used here (https://stackoverflow.com/a/1008289). + */ + class logger_t { + public: + /** + * @brief Defines the possible log levels. + * @note Level implicitly includes all other levels below it. + * @note All levels are in lower-case on purpose to fit the "BOOST_LOG(info)" style. + */ + enum class log_level_e { + verbose = 0, + debug, + info, + error + }; + + /** + * @brief Defines the callback type for log data re-routing. + */ + using callback_t = std::function; + + /** + * @brief Get the singleton instance. + * @returns Singleton instance for the class. + * + * EXAMPLES: + * ```cpp + * logger_t& logger { logger_t::get() }; + * ``` + */ + static logger_t & + get(); + + /** + * @brief Set the log level for the logger. + * @param log_level New level to be used. + * + * EXAMPLES: + * ```cpp + * logger_t::get().set_log_level(logger_t::log_level_e::Info); + * ``` + */ + void + set_log_level(const log_level_e log_level); + + /** + * @brief Check if log level is currently enabled. + * @param log_level Log level to check. + * @returns True if log level is enabled. + * + * EXAMPLES: + * ```cpp + * const bool is_enabled { logger_t::get().is_log_level_enabled(logger_t::log_level_e::Info) }; + * ``` + */ + [[nodiscard]] bool + is_log_level_enabled(const log_level_e log_level) const; + + /** + * @brief Set custom callback for writing the logs. + * @param callback New callback to be used or nullptr to reset to the default. + * + * EXAMPLES: + * ```cpp + * logger_t::get().set_custom_callback([](const log_level_e level, std::string value){ + * // write to file or something + * }); + * ``` + */ + void + set_custom_callback(callback_t callback); + + /** + * @brief Write the string to the output (via callback) if the log level is enabled. + * @param log_level Log level to be checked and (probably) written. + * @param value A copy of the string to be written. + * + * EXAMPLES: + * ```cpp + * logger_t::get().write(logger_t::log_level_e::Info, "Hello World!"); + * ``` + */ + void + write(const log_level_e log_level, std::string value); + + /** + * @brief A deleted copy constructor for singleton pattern. + * @note Public to ensure better compiler error message. + */ + logger_t(logger_t const &) = delete; + + /** + * @brief A deleted assignment operator for singleton pattern. + * @note Public to ensure better compiler error message. + */ + void + operator=(logger_t const &) = delete; + + private: + /** + * @brief A private constructor to ensure the singleton pattern. + */ + explicit logger_t(); + + log_level_e m_enabled_log_level; /**< The currently enabled log level. */ + callback_t m_custom_callback; /**< Custom callback to pass log data to. */ + }; + + /** + * @brief A helper class for accumulating output via the stream operator and then writing it out at once. + */ + class log_writer_t { + public: + /** + * @brief Constructor scoped writer utility. + * @param log_level Level to be used when writing out the output. + */ + explicit log_writer_t(const logger_t::log_level_e log_level); + + /** + * @brief Write out the accumulated output. + */ + virtual ~log_writer_t(); + + /** + * @brief Stream value to the buffer. + * @param value Arbitrary value to be written to the buffer. + * @returns Reference to the writer utility for chaining the operators. + */ + template + log_writer_t & + operator<<(T &&value) { + m_buffer << std::forward(value); + return *this; + } + + private: + logger_t::log_level_e m_log_level; /**< Log level to be used. */ + std::ostringstream m_buffer; /**< Buffet to hold all the output. */ + }; +} // namespace display_device + +/** + * @brief Helper MACRO that disables output string computation if log level is not enabled. + * + * EXAMPLES: + * ```cpp + * DD_LOG(info) << "Hello World!" << " " << 123; + * DD_LOG(error) << "OH MY GAWD!"; + * ``` + */ +#define DD_LOG(level) \ + for (bool is_enabled { display_device::logger_t::get().is_log_level_enabled(display_device::logger_t::log_level_e::level) }; is_enabled; is_enabled = false) \ + display_device::log_writer_t(display_device::logger_t::log_level_e::level) diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index c4c5a66..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/** - * @file main.cpp - */ - -#include - -int -main() { - std::cout << "Hello, World!" << std::endl; - return 0; -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61460b0..254c697 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,19 +33,16 @@ file(GLOB_RECURSE TEST_SOURCES set(DD_SOURCES ${DD_TARGET_FILES}) -# remove main.cpp from the list of sources -list(REMOVE_ITEM DD_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp) - add_executable(${PROJECT_NAME} ${TEST_SOURCES} ${DD_SOURCES}) -set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) +set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20) target_link_libraries(${PROJECT_NAME} ${DD_EXTERNAL_LIBRARIES} gtest gtest_main) # if we use this we don't need our own main function target_compile_definitions(${PROJECT_NAME} PUBLIC ${DD_DEFINITIONS} ${TEST_DEFINITIONS}) -target_compile_options(${PROJECT_NAME} PRIVATE $<$:${DD_COMPILE_OPTIONS}> -std=c++17) +target_compile_options(${PROJECT_NAME} PRIVATE $<$:${DD_COMPILE_OPTIONS}> -std=c++20) target_link_options(${PROJECT_NAME} PRIVATE) add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test) diff --git a/tests/conftest.cpp b/tests/conftest.cpp index 079cd7f..f06eca1 100644 --- a/tests/conftest.cpp +++ b/tests/conftest.cpp @@ -1,7 +1,10 @@ +// system includes #include #include -#include +// local includes +#include "src/logging.h" +#include "tests/utils.h" // Undefine the original TEST macro #undef TEST @@ -56,10 +59,14 @@ class BaseTest: public ::testing::Test { sbuf = std::cout.rdbuf(); // save cout buffer (std::cout) std::cout.rdbuf(cout_buffer.rdbuf()); // redirect cout to buffer (std::cout) + + // Default to the verbose level in case some test fails + display_device::logger_t::get().set_log_level(display_device::logger_t::log_level_e::verbose); } void TearDown() override { + display_device::logger_t::get().set_custom_callback(nullptr); // restore the default callback to avoid potential leaks std::cout.rdbuf(sbuf); // restore cout buffer // get test info @@ -69,8 +76,6 @@ class BaseTest: public ::testing::Test { std::cout << std::endl << "Test failed: " << test_info->name() << std::endl << std::endl - << "Captured boost log:" << std::endl - << boost_log_buffer.str() << std::endl << "Captured cout:" << std::endl << cout_buffer.str() << std::endl << "Captured stdout:" << std::endl @@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test { std::vector testArgs; // CLI arguments used std::filesystem::path testBinary; // full path of this binary std::filesystem::path testBinaryDir; // full directory of this binary - std::stringstream boost_log_buffer; // declare boost_log_buffer std::stringstream cout_buffer; // declare cout_buffer std::stringstream stdout_buffer; // declare stdout_buffer std::stringstream stderr_buffer; // declare stderr_buffer diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp new file mode 100644 index 0000000..f4d1aca --- /dev/null +++ b/tests/unit/test_logging.cpp @@ -0,0 +1,162 @@ +// system includes +#include + +// local includes +#include "tests/conftest.cpp" + +TEST(LoggingTest, LogLevelVerbose) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + logger.set_log_level(level::verbose); + + EXPECT_EQ(logger.is_log_level_enabled(level::verbose), true); + EXPECT_EQ(logger.is_log_level_enabled(level::debug), true); + EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::error), true); +} + +TEST(LoggingTest, LogLevelDebug) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + logger.set_log_level(level::debug); + + EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); + EXPECT_EQ(logger.is_log_level_enabled(level::debug), true); + EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::error), true); +} + +TEST(LoggingTest, LogLevelInfo) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + logger.set_log_level(level::info); + + EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); + EXPECT_EQ(logger.is_log_level_enabled(level::debug), false); + EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::error), true); +} + +TEST(LoggingTest, LogLevelError) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + logger.set_log_level(level::error); + + EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); + EXPECT_EQ(logger.is_log_level_enabled(level::debug), false); + EXPECT_EQ(logger.is_log_level_enabled(level::info), false); + EXPECT_EQ(logger.is_log_level_enabled(level::error), true); +} + +TEST(LoggingTest, DefaultLogger) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + const auto write_and_get_cout { [this, &logger](level level, std::string value) -> std::string { + this->cout_buffer.str(std::string {}); // reset the buffer + logger.write(level, std::move(value)); + return this->cout_buffer.str(); + } }; + const auto test_regex { [](const std::string &test_patten, const std::string ®ex_pattern) -> bool { + std::regex regex(regex_pattern); + std::smatch match; + return std::regex_match(test_patten, match, regex); + } }; + + logger.set_log_level(level::verbose); + EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] VERBOSE: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] DEBUG: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] INFO: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] ERROR: Hello World!\n)")); +} + +TEST(LoggingTest, CustomCallback) { + using level = display_device::logger_t::log_level_e; + using level_t = std::underlying_type_t; + auto &logger { display_device::logger_t::get() }; + + std::string output; + logger.set_log_level(level::verbose); + logger.set_custom_callback([&output](const level level, std::string value) { + output = std::to_string(static_cast(level)) + " " + value; + }); + + logger.write(level::verbose, "Hello World!"); + EXPECT_EQ(output, "0 Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.write(level::debug, "Hello World!"); + EXPECT_EQ(output, "1 Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.write(level::info, "Hello World!"); + EXPECT_EQ(output, "2 Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.write(level::error, "Hello World!"); + EXPECT_EQ(output, "3 Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); +} + +TEST(LoggingTest, WriteMethodRespectsLogLevelWhenUsingDefaultLogger) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.set_log_level(level::error); + logger.write(level::info, "Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.set_log_level(level::info); + logger.write(level::info, "Hello World!"); + EXPECT_FALSE(this->cout_buffer.str().empty()); +} + +TEST(LoggingTest, WriteMethodRespectsLogLevelWhenUsingCustomCallback) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + bool callback_invoked { false }; + logger.set_custom_callback([&callback_invoked](const level, std::string) { + callback_invoked = true; + }); + + logger.set_log_level(level::error); + logger.write(level::info, "Hello World!"); + EXPECT_EQ(callback_invoked, false); + + logger.set_log_level(level::info); + logger.write(level::info, "Hello World!"); + EXPECT_EQ(callback_invoked, true); +} + +TEST(LoggingTest, LogMacroDisablesStreamChain) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + bool output_logged { false }; + logger.set_custom_callback([&output_logged](const level, std::string) { + output_logged = true; + }); + + bool some_function_invoked { false }; + const auto some_function { [&some_function_invoked]() { + some_function_invoked = true; + return "some string"; + } }; + + logger.set_log_level(level::error); + DD_LOG(info) << some_function(); + EXPECT_EQ(output_logged, false); + EXPECT_EQ(some_function_invoked, false); + + logger.set_log_level(level::info); + DD_LOG(info) << some_function(); + EXPECT_EQ(output_logged, true); + EXPECT_EQ(some_function_invoked, true); +} From a4a3e16de2456d84aa1bb65d41cfad5fe91a74ee Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Mon, 15 Apr 2024 23:30:06 +0300 Subject: [PATCH 2/7] Fix a typo --- src/logging.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logging.h b/src/logging.h index 23346f1..a92597a 100644 --- a/src/logging.h +++ b/src/logging.h @@ -150,7 +150,7 @@ namespace display_device { private: logger_t::log_level_e m_log_level; /**< Log level to be used. */ - std::ostringstream m_buffer; /**< Buffet to hold all the output. */ + std::ostringstream m_buffer; /**< Buffer to hold all the output. */ }; } // namespace display_device From 615808f1dc5e0dcd31f8a1bf77405c37254aa893 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Mon, 15 Apr 2024 23:30:19 +0300 Subject: [PATCH 3/7] Fix a test for Windows --- tests/unit/test_logging.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index f4d1aca..44dfcb8 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -66,12 +66,20 @@ TEST(LoggingTest, DefaultLogger) { std::smatch match; return std::regex_match(test_patten, match, regex); } }; + const auto add_os_newline { [](std::string value) { +#ifdef _WIN32 + value += "\r\n"; +#else + value += "\n"; +#endif + return value; + } }; logger.set_log_level(level::verbose); - EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] VERBOSE: Hello World!\n)")); - EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] DEBUG: Hello World!\n)")); - EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] INFO: Hello World!\n)")); - EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] ERROR: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] VERBOSE: Hello World!)"))); + EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] DEBUG: Hello World!)"))); + EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] INFO: Hello World!)"))); + EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] ERROR: Hello World!)"))); } TEST(LoggingTest, CustomCallback) { From 4ad721b99e4b733569ba38d5cf387f41c55d073e Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Tue, 16 Apr 2024 10:49:25 +0300 Subject: [PATCH 4/7] Fix cross-platf timestamp logging --- src/logging.cpp | 11 ++++++++++- tests/unit/test_logging.cpp | 22 ++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/logging.cpp b/src/logging.cpp index e3e0548..0611c43 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -46,7 +46,16 @@ namespace display_device { { // Time const auto now { std::chrono::current_zone()->to_local(std::chrono::system_clock::now()) }; - stream << std::format("[{:%Y-%m-%d %X}] ", now); + const auto now_ms { std::chrono::duration_cast(now.time_since_epoch()) }; + const auto now_s { std::chrono::duration_cast(now_ms) }; + + // we need to ensure that the time formatter does not print decimal numbers for seconds as they as + // it currently has inconsistent logic between platforms... + // Instead we will do it manually. + const auto now_local_seconds { std::chrono::local_seconds(now_s) }; + const auto now_decimal_part { now_ms - now_s }; + + stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part); // Log level switch (log_level) { diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index 44dfcb8..168a894 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -61,25 +61,19 @@ TEST(LoggingTest, DefaultLogger) { logger.write(level, std::move(value)); return this->cout_buffer.str(); } }; - const auto test_regex { [](const std::string &test_patten, const std::string ®ex_pattern) -> bool { + const auto test_regex { [](const std::string &test_pattern, const std::string ®ex_pattern) -> bool { std::regex regex(regex_pattern); std::smatch match; - return std::regex_match(test_patten, match, regex); - } }; - const auto add_os_newline { [](std::string value) { -#ifdef _WIN32 - value += "\r\n"; -#else - value += "\n"; -#endif - return value; + return std::regex_match(test_pattern, match, regex); } }; logger.set_log_level(level::verbose); - EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] VERBOSE: Hello World!)"))); - EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] DEBUG: Hello World!)"))); - EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] INFO: Hello World!)"))); - EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), add_os_newline(R"(\[\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\] ERROR: Hello World!)"))); + // clang-format off + EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] VERBOSE: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] DEBUG: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] INFO: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] ERROR: Hello World!\n)")); + // clang-format on } TEST(LoggingTest, CustomCallback) { From a5b1ad231cf282a8d4fe38b70752e9ec131b627c Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Tue, 16 Apr 2024 10:55:04 +0300 Subject: [PATCH 5/7] Fix typo --- src/logging.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logging.cpp b/src/logging.cpp index 0611c43..ffd7a3e 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -49,8 +49,8 @@ namespace display_device { const auto now_ms { std::chrono::duration_cast(now.time_since_epoch()) }; const auto now_s { std::chrono::duration_cast(now_ms) }; - // we need to ensure that the time formatter does not print decimal numbers for seconds as they as - // it currently has inconsistent logic between platforms... + // we need to ensure that the time formatter does not print decimal numbers for seconds as + // it currently has inconsistent formatting logic between platforms... // Instead we will do it manually. const auto now_local_seconds { std::chrono::local_seconds(now_s) }; const auto now_decimal_part { now_ms - now_s }; From 5a69c4537ff1f9c009a169c28a72ad73c814e9b5 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Tue, 16 Apr 2024 15:53:07 +0300 Subject: [PATCH 6/7] Add warning level --- src/logging.cpp | 3 +++ src/logging.h | 1 + tests/unit/test_logging.cpp | 24 +++++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/logging.cpp b/src/logging.cpp index ffd7a3e..5d67e53 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -68,6 +68,9 @@ namespace display_device { case log_level_e::info: stream << "INFO: "; break; + case log_level_e::warning: + stream << "WARNING: "; + break; case log_level_e::error: stream << "ERROR: "; break; diff --git a/src/logging.h b/src/logging.h index a92597a..0b8b035 100644 --- a/src/logging.h +++ b/src/logging.h @@ -25,6 +25,7 @@ namespace display_device { verbose = 0, debug, info, + warning, error }; diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index 168a894..e6be086 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -13,6 +13,7 @@ TEST(LoggingTest, LogLevelVerbose) { EXPECT_EQ(logger.is_log_level_enabled(level::verbose), true); EXPECT_EQ(logger.is_log_level_enabled(level::debug), true); EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::warning), true); EXPECT_EQ(logger.is_log_level_enabled(level::error), true); } @@ -25,6 +26,7 @@ TEST(LoggingTest, LogLevelDebug) { EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); EXPECT_EQ(logger.is_log_level_enabled(level::debug), true); EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::warning), true); EXPECT_EQ(logger.is_log_level_enabled(level::error), true); } @@ -37,6 +39,20 @@ TEST(LoggingTest, LogLevelInfo) { EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); EXPECT_EQ(logger.is_log_level_enabled(level::debug), false); EXPECT_EQ(logger.is_log_level_enabled(level::info), true); + EXPECT_EQ(logger.is_log_level_enabled(level::warning), true); + EXPECT_EQ(logger.is_log_level_enabled(level::error), true); +} + +TEST(LoggingTest, LogLevelWarning) { + using level = display_device::logger_t::log_level_e; + auto &logger { display_device::logger_t::get() }; + + logger.set_log_level(level::warning); + + EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); + EXPECT_EQ(logger.is_log_level_enabled(level::debug), false); + EXPECT_EQ(logger.is_log_level_enabled(level::info), false); + EXPECT_EQ(logger.is_log_level_enabled(level::warning), true); EXPECT_EQ(logger.is_log_level_enabled(level::error), true); } @@ -49,6 +65,7 @@ TEST(LoggingTest, LogLevelError) { EXPECT_EQ(logger.is_log_level_enabled(level::verbose), false); EXPECT_EQ(logger.is_log_level_enabled(level::debug), false); EXPECT_EQ(logger.is_log_level_enabled(level::info), false); + EXPECT_EQ(logger.is_log_level_enabled(level::warning), false); EXPECT_EQ(logger.is_log_level_enabled(level::error), true); } @@ -72,6 +89,7 @@ TEST(LoggingTest, DefaultLogger) { EXPECT_TRUE(test_regex(write_and_get_cout(level::verbose, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] VERBOSE: Hello World!\n)")); EXPECT_TRUE(test_regex(write_and_get_cout(level::debug, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] DEBUG: Hello World!\n)")); EXPECT_TRUE(test_regex(write_and_get_cout(level::info, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] INFO: Hello World!\n)")); + EXPECT_TRUE(test_regex(write_and_get_cout(level::warning, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] WARNING: Hello World!\n)")); EXPECT_TRUE(test_regex(write_and_get_cout(level::error, "Hello World!"), R"(\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}\] ERROR: Hello World!\n)")); // clang-format on } @@ -99,9 +117,13 @@ TEST(LoggingTest, CustomCallback) { EXPECT_EQ(output, "2 Hello World!"); EXPECT_TRUE(this->cout_buffer.str().empty()); - logger.write(level::error, "Hello World!"); + logger.write(level::warning, "Hello World!"); EXPECT_EQ(output, "3 Hello World!"); EXPECT_TRUE(this->cout_buffer.str().empty()); + + logger.write(level::error, "Hello World!"); + EXPECT_EQ(output, "4 Hello World!"); + EXPECT_TRUE(this->cout_buffer.str().empty()); } TEST(LoggingTest, WriteMethodRespectsLogLevelWhenUsingDefaultLogger) { From b9743a405070837d0c075418a4d7f0f070252a10 Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Tue, 16 Apr 2024 16:40:39 +0300 Subject: [PATCH 7/7] Update CMakeLists.txt Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dc9f22..3e0e44d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,8 @@ else() endif() # glob src -file(GLOB_RECURSE DD_TARGET_FILES src/*.h src/*.cpp) +file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp") +include_directories("${CMAKE_SOURCE_DIR}") # tests if(BUILD_TESTS)