Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,9 @@
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# build directories
build/
cmake-*/
5 changes: 3 additions & 2 deletions CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Expand All@@ -31,7 +31,8 @@ else()
endif()

# glob src
file(GLOB_RECURSE DD_TARGET_FILES src/*.cpp)
file(GLOB_RECURSE DD_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/*.cpp")
include_directories("${CMAKE_SOURCE_DIR}")

# tests
if(BUILD_TESTS)
Expand Down
100 changes: 100 additions & 0 deletions src/logging.cpp
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
// class header include
#include "logging.h"

// system includes
#include <chrono>
#include <format>
#include <iostream>
#include <mutex>

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<std::underlying_type_t<log_level_e>>(log_level) };
const auto enabled_log_level_v { static_cast<std::underlying_type_t<log_level_e>>(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()) };
const auto now_ms { std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) };
const auto now_s { std::chrono::duration_cast<std::chrono::seconds>(now_ms) };

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

stream << std::format("[{:%Y-%m-%d %H:%M:%S}.{:0>3%Q}] ", now_local_seconds, now_decimal_part);

// 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::warning:
stream << "WARNING: ";
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
169 changes: 169 additions & 0 deletions src/logging.h
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
#pragma once

// system includes
#include <functional>
#include <sstream>
#include <string>

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,
warning,
error
};

/**
* @brief Defines the callback type for log data re-routing.
*/
using callback_t = std::function<void(const log_level_e, std::string)>;

/**
* @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 <class T>
log_writer_t &
operator<<(T &&value) {
m_buffer << std::forward<T>(value);
return *this;
}

private:
logger_t::log_level_e m_log_level; /**< Log level to be used. */
std::ostringstream m_buffer; /**< Buffer 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)
11 changes: 0 additions & 11 deletions src/main.cpp

This file was deleted.

7 changes: 2 additions & 5 deletions tests/CMakeLists.txt
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++17)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:${DD_COMPILE_OPTIONS}> -std=c++20)
target_link_options(${PROJECT_NAME} PRIVATE)

add_test(NAME ${PROJECT_NAME} COMMAND libdisplaydevice_test)
12 changes: 8 additions & 4 deletions tests/conftest.cpp
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
// system includes
#include <filesystem>
#include <gtest/gtest.h>

#include <tests/utils.h>
// local includes
#include "src/logging.h"
#include "tests/utils.h"

// Undefine the original TEST macro
#undef TEST
Expand DownExpand Up@@ -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
Expand All@@ -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
Expand All@@ -94,7 +99,6 @@ class BaseTest: public ::testing::Test {
std::vector<std::string> 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
Expand Down
Loading