diff --git a/.gitmodules b/.gitmodules index 69503ee..06c3765 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "cmake"] path = lib/cmake - url = https://github.com/OliverBenz/cmake_files.git + url = git@github.com:OliverBenz/cmake_files.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 9576e76..696c006 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,19 +1,77 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) + +project(Logger VERSION 1.0.0 LANGUAGES CXX) -project(LoggingProject) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(LOGGER_IS_TOP_LEVEL OFF) +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + set(LOGGER_IS_TOP_LEVEL ON) +endif() + +# Normalize install prefix for Unix-like systems (prefer /opt). +if(UNIX AND NOT WIN32) + if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT OR CMAKE_INSTALL_PREFIX MATCHES "^/var/empty") + set(CMAKE_INSTALL_PREFIX "/opt" CACHE PATH "Install path prefix" FORCE) + endif() +endif() + +# Project Options +option(LOGGER_BUILD_TESTS "Build Logger unit tests" ${LOGGER_IS_TOP_LEVEL}) +option(LOGGER_BUILD_EXAMPLES "Build Logger examples" ${LOGGER_IS_TOP_LEVEL}) +option(LOGGER_ENABLE_INSTALL "Enable install/export targets for Logger" ${LOGGER_IS_TOP_LEVEL}) -option(BUILD_TESTS "Create the unit tests for the project." True) -option(BUILD_EXAMPLES "Create examples for the project." True) +# Dependencies Options +set(FETCHCONTENT_UPDATES_DISCONNECTED ON CACHE BOOL "Use previously downloaded FetchContent sources" FORCE) +set(INSTALL_GTEST OFF CACHE BOOL "Do not install googletest" FORCE) +set(INSTALL_GMOCK OFF CACHE BOOL "Do not install googlemock" FORCE) +# Installation +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) # Setup project settings add_subdirectory(lib) +project_settings_configure_install_layout( + LOGGER_INSTALL_ARCH + LOGGER_INSTALL_CONFIG + LOGGER_INSTALL_BASE + PROJECT_NAME ${PROJECT_NAME} + INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX} +) add_subdirectory(src) -if(BUILD_TESTS) + +if(LOGGER_BUILD_TESTS) + enable_testing() add_subdirectory(tests) endif() -if(BUILD_EXAMPLES) +if(LOGGER_BUILD_EXAMPLES) add_subdirectory(examples) -endif() \ No newline at end of file +endif() + +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/LoggerConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/LoggerConfig.cmake + INSTALL_DESTINATION ${LOGGER_INSTALL_BASE}/lib/cmake +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/LoggerConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +export( + EXPORT LoggerTargets + FILE ${CMAKE_CURRENT_BINARY_DIR}/LoggerTargets.cmake + NAMESPACE Logger:: +) + +if(LOGGER_ENABLE_INSTALL) + install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/LoggerConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/LoggerConfigVersion.cmake + DESTINATION ${LOGGER_INSTALL_BASE}/lib/cmake + ) +endif() diff --git a/README.md b/README.md index 630b323..a4233b6 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# Template \ No newline at end of file +# Simple Logger +Just a simple logger. +Check examples for how to use. + +### Documentation +- [General Documentation](docs/Documentation.md) +- [Installation](docs/Installation.md) \ No newline at end of file diff --git a/cmake/LoggerConfig.cmake.in b/cmake/LoggerConfig.cmake.in new file mode 100644 index 0000000..59b23ab --- /dev/null +++ b/cmake/LoggerConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/LoggerTargets.cmake") diff --git a/docs/Documentation.md b/docs/Documentation.md index e69de29..7405b42 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -0,0 +1,2 @@ +Gotta do this. +Just check the examples for how to use. \ No newline at end of file diff --git a/docs/Installation.md b/docs/Installation.md new file mode 100644 index 0000000..81df4fe --- /dev/null +++ b/docs/Installation.md @@ -0,0 +1,54 @@ +# Logger installation & integration + +## CMake Options +- `LOGGER_BUILD_TESTS` (default: `ON` when top level) – build unit tests. +- `LOGGER_BUILD_EXAMPLES` (default: `ON` when top level) – build example apps. +- `LOGGER_ENABLE_INSTALL` (default: `ON` when top level) – emit install/export targets. + +## Build locally +```bash +cmake -S . -B build -DLOGGER_BUILD_TESTS=ON -DLOGGER_BUILD_EXAMPLES=ON +cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +## Use as a git submodule +1) `git submodule add external/logger` +2) In your root `CMakeLists.txt`: +```cmake +set(LOGGER_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(LOGGER_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +add_subdirectory(external/logger) +target_link_libraries( PRIVATE Logger::Logger) +``` +- Choose this when you want an auditable, locked revision in your tree. + +## Use via FetchContent +```cmake +include(FetchContent) +FetchContent_Declare( + Logger + GIT_REPOSITORY + GIT_TAG +) +set(LOGGER_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(LOGGER_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(Logger) +target_link_libraries( PRIVATE Logger::Logger) +``` +- Prefer this when you want simple, on-demand fetching without VCS plumbing. +- Pin `GIT_TAG` to a commit or release; mirror internally if external fetches are blocked. + +## Use an installed package +```bash +cmake --build build --target install --prefix +``` +```cmake +find_package(Logger CONFIG REQUIRED) +target_link_libraries( PRIVATE Logger::Logger) +``` + +## Integration notes +- Public include path is `Logger/` (e.g., `#include `). +- The library is C++20 and exports an ALIAS target `Logger::Logger`. +- Default output dirs: `${binary_dir}/out/bin` and `${binary_dir}/out/lib`. diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2e6f359..6899553 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) add_subdirectory(standard) add_subdirectory(globalConfig) diff --git a/examples/globalConfig/CMakeLists.txt b/examples/globalConfig/CMakeLists.txt index 911b33b..229377c 100644 --- a/examples/globalConfig/CMakeLists.txt +++ b/examples/globalConfig/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) set(targetName Example.GlobalConfig) @@ -9,7 +9,7 @@ add_executable(${targetName} ) # Link to required libraries -target_link_libraries(${targetName} PUBLIC Logging) +target_link_libraries(${targetName} PUBLIC Logger::Logger) set_target_properties(${targetName} PROPERTIES FOLDER "${ideFolderExamples}") # Setup project settings diff --git a/examples/globalConfig/LogModule.cpp b/examples/globalConfig/LogModule.cpp index 8ae5fe4..af09ac8 100644 --- a/examples/globalConfig/LogModule.cpp +++ b/examples/globalConfig/LogModule.cpp @@ -1,30 +1,30 @@ #include "LogModule.hpp" -#include "LogConfig.hpp" -#include "LogOutputFile.hpp" -#include "LogOutputConsole.hpp" +#include "Logger/LogConfig.hpp" +#include "Logger/LogOutputConsole.hpp" +#include "Logger/LogOutputFile.hpp" Logging::LogConfig config; //! Enable logging of any entries to an output file + console(for debug builds). static void InitializeLogger() { - config.SetLogEnabled(true); - config.SetMinLogLevel(Logging::LogLevel::Any); + config.SetLogEnabled(true); + config.SetMinLogLevel(Logging::LogLevel::Any); auto logFile = std::make_shared("Logfile.txt"); - config.AddLogOutput(logFile); + config.AddLogOutput(logFile); #ifdef _DEBUG - config.AddLogOutput(std::make_shared()); + config.AddLogOutput(std::make_shared()); #endif } Logging::Logger Logger() { - static bool initialized = false; - if (!initialized) { - InitializeLogger(); - initialized = true; - } + static bool initialized = false; + if (!initialized) { + InitializeLogger(); + initialized = true; + } - return Logging::Logger(config); + return Logging::Logger(config); } diff --git a/examples/globalConfig/LogModule.hpp b/examples/globalConfig/LogModule.hpp index ccd1435..9b8a47b 100644 --- a/examples/globalConfig/LogModule.hpp +++ b/examples/globalConfig/LogModule.hpp @@ -1,6 +1,6 @@ #pragma once -#include "Logger.hpp" +#include "Logger/Logger.hpp" //! Returns the logger instance based on the set up configuration. Logging::Logger Logger(); \ No newline at end of file diff --git a/examples/globalConfig/main.cpp b/examples/globalConfig/main.cpp index 810001d..eea2c22 100644 --- a/examples/globalConfig/main.cpp +++ b/examples/globalConfig/main.cpp @@ -2,32 +2,32 @@ #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) -#define VERSION_STRING "Version: " STR(LOGVERSION_MAJOR) "." STR(LOGVERSION_MINOR) "." STR(LOGVERSION_PATCH) +#define VERSION_STRING "Version: " STR(LOGVERSION_MAJOR) "." STR(LOGVERSION_MINOR) "." STR(LOGVERSION_PATCH) //! Example for logging. class Calculator { public: - Calculator(const int a, const int b) : m_a(a), m_b(b) { - Logger().Log(Logging::LogLevel::Debug, "Calculator constructor called."); - } + Calculator(const int a, const int b) : m_a(a), m_b(b) { + Logger().Log(Logging::LogLevel::Debug, "Calculator constructor called."); + } - int Add() const { - Logger().Log(Logging::LogLevel::Debug, "Add function called."); - return m_a + m_b; - } + int Add() const { + Logger().Log(Logging::LogLevel::Debug, "Add function called."); + return m_a + m_b; + } private: - int m_a; - int m_b; + int m_a; + int m_b; }; int main() { - auto logger = Logger(); - logger.Log(Logging::LogLevel::Info, "Starting application."); - logger.Log(Logging::LogLevel::Info, VERSION_STRING); - logger.Log(Logging::LogLevel::Info, "Author: Oliver Benz"); - logger.Flush(); + auto logger = Logger(); + logger.Log(Logging::LogLevel::Info, "Starting application."); + logger.Log(Logging::LogLevel::Info, VERSION_STRING); + logger.Log(Logging::LogLevel::Info, "Author: Oliver Benz"); + logger.Flush(); - Calculator calc(9, 10); - calc.Add(); + Calculator calc(9, 10); + calc.Add(); } \ No newline at end of file diff --git a/examples/standard/CMakeLists.txt b/examples/standard/CMakeLists.txt index f15faa0..b98a95c 100644 --- a/examples/standard/CMakeLists.txt +++ b/examples/standard/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) set(targetName Example.Standard) @@ -8,7 +8,7 @@ add_executable(${targetName} ) # Link to required libraries -target_link_libraries(${targetName} PUBLIC Logging) +target_link_libraries(${targetName} PUBLIC Logger::Logger) set_target_properties(${targetName} PROPERTIES FOLDER "${ideFolderExamples}") # Setup project settings diff --git a/examples/standard/main.cpp b/examples/standard/main.cpp index a358a90..92b3e06 100644 --- a/examples/standard/main.cpp +++ b/examples/standard/main.cpp @@ -1,13 +1,14 @@ +#include "Logger/LogLevel.hpp" +#include "Logger/LogOutputConsole.hpp" +#include "Logger/LogOutputFile.hpp" +#include "Logger/LogOutputMock.hpp" +#include "Logger/Logger.hpp" + #include -#include "Logger.hpp" -#include "LogLevel.hpp" -#include "LogOutputConsole.hpp" -#include "LogOutputMock.hpp" -#include "LogOutputFile.hpp" -int main(){ +int main() { std::cout << "Version: " << LOGVERSION_MAJOR << "." << LOGVERSION_MINOR << "." << LOGVERSION_PATCH << std::endl; - + static Logging::LogConfig config; auto mock = std::make_shared(); auto logFile = std::make_shared("Filename.txt"); @@ -24,7 +25,7 @@ int main(){ } std::cout << "\nMockData:\n"; - for (const auto& entry : mock->m_logEntries){ + for (const auto& entry: mock->m_logEntries) { std::cout << LevelToText(entry.m_level) + " " + entry.m_text + "\n"; } diff --git a/lib/FetchLibraries.cmake b/lib/FetchLibraries.cmake index dfe05b4..f6896bb 100644 --- a/lib/FetchLibraries.cmake +++ b/lib/FetchLibraries.cmake @@ -1,13 +1,12 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.16) include(FetchContent) # GTest -# Version 1.13.0 FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG b796f7d44681514f58a683a3a71ff17c94edb0c1 + GIT_TAG v1.17.0 ) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # For Windows: Prevent overriding the parent project's compiler/linker settings diff --git a/lib/cmake b/lib/cmake index ff32b73..129648a 160000 --- a/lib/cmake +++ b/lib/cmake @@ -1 +1 @@ -Subproject commit ff32b7392b154ceb80a0cc7d79006623811913a7 +Subproject commit 129648aa6dc1d4e5e94c4192d6e5147f9c76c13e diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e998730..ff12f3b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -set(targetName Logging) +set(targetName Logger) # Get files to build file(GLOB_RECURSE headers CONFIGURE_DEPENDS "*.hpp") @@ -9,11 +9,15 @@ add_library(${targetName} STATIC ${headers} ${sources} ) +add_library(Logger::Logger ALIAS ${targetName}) -# Link to thread module. -target_link_libraries(${targetName}) -target_include_directories(${targetName} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) -set_target_properties(${targetName} PROPERTIES FOLDER "${ideFolderSource}") +target_compile_features(${targetName} PUBLIC cxx_std_20) +target_include_directories(${targetName} + PUBLIC + $ + $ +) +set_target_properties(${targetName} PROPERTIES FOLDER "${IDE_FOLDER_SOURCE}") # Setup project settings set_project_warnings(${targetName}) # Which warnings to enable @@ -21,7 +25,7 @@ set_compile_options(${targetName}) # Which extra compiler flags to enable set_output_directory(${targetName}) # Set the output directory of the library # Copy header files to output after build -copy_headers_to_output("${targetName}" "${headers}" "") +copy_headers_to_output("${targetName}" "${headers}" "Logger") # Specify version target_compile_definitions(${targetName} PUBLIC LOGVERSION_MAJOR=1) @@ -29,5 +33,20 @@ target_compile_definitions(${targetName} PUBLIC LOGVERSION_MINOR=0) target_compile_definitions(${targetName} PUBLIC LOGVERSION_PATCH=0) # System installation -# install(TARGETS ${targetName} DESTINATION lib) -# install(FILES ${headers} DESTINATION include/Logger) \ No newline at end of file +if(LOGGER_ENABLE_INSTALL) + install(TARGETS ${targetName} + EXPORT LoggerTargets + ARCHIVE DESTINATION ${LOGGER_INSTALL_BASE}/lib/${LOGGER_INSTALL_ARCH}/${LOGGER_INSTALL_CONFIG} + LIBRARY DESTINATION ${LOGGER_INSTALL_BASE}/lib/${LOGGER_INSTALL_ARCH}/${LOGGER_INSTALL_CONFIG} + RUNTIME DESTINATION ${LOGGER_INSTALL_BASE}/bin/${LOGGER_INSTALL_ARCH}/${LOGGER_INSTALL_CONFIG} + ) + install( + DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/Logger + DESTINATION ${LOGGER_INSTALL_BASE}/include + ) + install(EXPORT LoggerTargets + FILE LoggerTargets.cmake + NAMESPACE Logger:: + DESTINATION ${LOGGER_INSTALL_BASE}/lib/cmake + ) +endif() diff --git a/src/LogConfig.cpp b/src/LogConfig.cpp index b2090a4..b77aeea 100644 --- a/src/LogConfig.cpp +++ b/src/LogConfig.cpp @@ -1,4 +1,4 @@ -#include "LogConfig.hpp" +#include "Logger/LogConfig.hpp" namespace Logging { diff --git a/src/LogEntry.cpp b/src/LogEntry.cpp index e334427..72e2da3 100644 --- a/src/LogEntry.cpp +++ b/src/LogEntry.cpp @@ -1,4 +1,4 @@ -#include "LogEntry.hpp" +#include "Logger/LogEntry.hpp" #include diff --git a/src/LogLevel.cpp b/src/LogLevel.cpp index d7eee1d..bac8a45 100644 --- a/src/LogLevel.cpp +++ b/src/LogLevel.cpp @@ -1,4 +1,4 @@ -#include "LogLevel.hpp" +#include "Logger/LogLevel.hpp" namespace Logging { diff --git a/src/LogOutputConsole.cpp b/src/LogOutputConsole.cpp index 383ee31..bd324ee 100644 --- a/src/LogOutputConsole.cpp +++ b/src/LogOutputConsole.cpp @@ -1,16 +1,17 @@ -#include "LogOutputConsole.hpp" +#include "Logger/LogOutputConsole.hpp" + +#include namespace Logging { void LogOutputConsole::Write(const std::vector& logEntries) { - // TODO: cout or cerr for (const auto& entry: logEntries) { - std::cout << entry.OutputText() << "\n"; + std::clog << entry.OutputText() << "\n"; } } void LogOutputConsole::Write(const LogEntry& entry) { - std::cout << entry.OutputText() << "\n"; + std::clog << entry.OutputText() << "\n"; } -} // namespace Logging \ No newline at end of file +} // namespace Logging diff --git a/src/LogOutputFile.cpp b/src/LogOutputFile.cpp index 066920d..a4e637b 100644 --- a/src/LogOutputFile.cpp +++ b/src/LogOutputFile.cpp @@ -1,7 +1,8 @@ -#include "LogOutputFile.hpp" +#include "Logger/LogOutputFile.hpp" #include #include +#include namespace Logging { @@ -10,67 +11,87 @@ LogOutputFile::LogOutputFile(const std::string& filePath, std::size_t maxFileSiz } void LogOutputFile::RotateFile() { - // TODO: Test this function - std::size_t pos = m_filePath.rfind('.'); - const std::string path = m_filePath.substr(0, pos); - const std::string ext = m_filePath.substr(pos + 1); - - // New filename of logFile has postfix [number]. - unsigned count = 1; - std::string newFilePath; + const std::filesystem::path originalPath(m_filePath); + const auto parent = originalPath.parent_path(); + const auto stem = originalPath.stem().string(); + const auto extension = originalPath.extension().string(); + + const auto baseName = extension.empty() ? originalPath.filename().string() : stem; + + std::error_code ec; + constexpr unsigned kMaxRotations = 10000; + unsigned count = 1u; + std::filesystem::path newFilePath; do { - newFilePath = path + "(" + std::to_string(count) + ")." + ext; - ++count; - } while (std::filesystem::exists(newFilePath)); + if (count > kMaxRotations) { + return; + } - // The current file is renamed with the postfix. - if (!std::rename(m_filePath.c_str(), newFilePath.c_str()) == 0) { - // throw std::ios_base::failure(std::stderror(-1)); - } + auto rotatedName = baseName + "(" + std::to_string(count) + ")" + extension; + newFilePath = parent.empty() ? std::filesystem::path(rotatedName) : parent / rotatedName; + ++count; + } while (std::filesystem::exists(newFilePath, ec)); + ec.clear(); // // Any errors during probing are ignored by design + + std::filesystem::rename(originalPath, newFilePath, ec); + if (ec == std::errc::cross_device_link) { + // Best-effort fallback + std::filesystem::copy_file( + originalPath, + newFilePath, + std::filesystem::copy_options::overwrite_existing, + ec); + + if (!ec) { + std::filesystem::remove(originalPath, ec); + } + } + + // Any failure beyond this point is intentionally ignored. } void LogOutputFile::Write(const std::vector& logEntries) { std::unique_lock lock(m_writeLock); - std::ofstream outfile; - outfile.open(m_filePath.c_str(), std::ios::out | std::ios::app); - if (outfile.fail()) { - // throw std::ios_base::failure(std::stderror(-1)); + std::ofstream outfile(m_filePath, std::ios::out | std::ios::app); + if (!outfile.is_open()) { + return; } for (const auto& entry: logEntries) { outfile << entry.OutputText() << "\n"; } - - // Check max filesize reached - const auto fileSize = outfile.tellp(); outfile.close(); - if (fileSize != std::ofstream::pos_type(-1) && static_cast(fileSize) >= m_maxFileSize) { - RotateFile(); - } + + // Check max file size reached + std::error_code ec; + const auto fileSize = std::filesystem::file_size(m_filePath, ec); + if (!ec && fileSize >= m_maxFileSize) { + RotateFile(); + } } void LogOutputFile::Write(const LogEntry& entry) { std::unique_lock lock(m_writeLock); - std::ofstream outfile; - outfile.open(m_filePath.c_str(), std::ios::out | std::ios::app); - if (outfile.fail()) { - // throw std::ios_base::failure(std::stderror(-1)); + std::ofstream outfile(m_filePath, std::ios::out | std::ios::app); + if (!outfile.is_open()) { + return; } outfile << entry.OutputText() << "\n"; + outfile.close(); // Check max filesize reached - const auto fileSize = outfile.tellp(); - outfile.close(); - if (fileSize != std::ofstream::pos_type(-1) && static_cast(fileSize) >= m_maxFileSize) { - RotateFile(); - } + std::error_code ec; + const auto fileSize = std::filesystem::file_size(m_filePath, ec); + if (!ec && fileSize >= m_maxFileSize) { + RotateFile(); + } } std::string LogOutputFile::FilePath() const { return m_filePath; } -} // namespace Logging \ No newline at end of file +} // namespace Logging diff --git a/src/LogOutputMock.cpp b/src/LogOutputMock.cpp index 1d4dc15..11e7ebf 100644 --- a/src/LogOutputMock.cpp +++ b/src/LogOutputMock.cpp @@ -1,12 +1,9 @@ -#include "LogOutputMock.hpp" +#include "Logger/LogOutputMock.hpp" namespace Logging { void LogOutputMock::Write(const std::vector& logEntries) { - for (std::size_t i = 0; i != logEntries.size(); ++i) { - // TODO: Properly copy all at once.. - m_logEntries.emplace_back(logEntries[i]); - } + m_logEntries.insert(m_logEntries.end(), logEntries.begin(), logEntries.end()); } void LogOutputMock::Write(const LogEntry& entry) { diff --git a/src/Logger.cpp b/src/Logger.cpp index f2300f7..569fa28 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -1,4 +1,4 @@ -#include "Logger.hpp" +#include "Logger/Logger.hpp" #include #include diff --git a/src/Profiler.cpp b/src/Profiler.cpp index 7e4eb0d..b841967 100644 --- a/src/Profiler.cpp +++ b/src/Profiler.cpp @@ -1,4 +1,5 @@ -#include "Profiler.hpp" +#include "Logger/Profiler.hpp" + #include namespace Logging { @@ -14,15 +15,17 @@ Profiler::Profiler(Logger logger, std::string identifier) Profiler::~Profiler() { const auto now = std::chrono::steady_clock::now(); const auto timeMs = std::chrono::duration_cast(now - m_startTime).count(); // t in ms - m_logger.Log(Logging::LogLevel::Info, std::format("<-- {} END: ", m_identifier, timeMs)); + m_logger.Log(Logging::LogLevel::Info, std::format("<-- {} END: {}ms", m_identifier, timeMs)); } void Profiler::LogStep(const std::string& stepName) { - const auto timeMs = - std::chrono::duration_cast(m_lastTime - m_startTime).count(); // t in ms - m_logger.Log(Logging::LogLevel::Info, std::format("--- {} STEP {}: ", m_identifier, stepName, timeMs)); - m_lastTime = std::chrono::steady_clock::now(); + const auto now = std::chrono::steady_clock::now(); + const auto stepMs = std::chrono::duration_cast(now - m_lastTime).count(); // t in ms + const auto totalMs = std::chrono::duration_cast(now - m_startTime).count(); // t in ms + m_logger.Log(Logging::LogLevel::Info, + std::format("--- {} STEP {}: +{}ms ({}ms total)", m_identifier, stepName, stepMs, totalMs)); + m_lastTime = now; } -} // namespace Logging \ No newline at end of file +} // namespace Logging diff --git a/src/include/ILogOutput.hpp b/src/include/Logger/ILogOutput.hpp similarity index 95% rename from src/include/ILogOutput.hpp rename to src/include/Logger/ILogOutput.hpp index f641813..267b553 100644 --- a/src/include/ILogOutput.hpp +++ b/src/include/Logger/ILogOutput.hpp @@ -1,6 +1,7 @@ #pragma once -#include "LogEntry.hpp" +#include "Logger/LogEntry.hpp" + #include namespace Logging { diff --git a/src/include/LogConfig.hpp b/src/include/Logger/LogConfig.hpp similarity index 93% rename from src/include/LogConfig.hpp rename to src/include/Logger/LogConfig.hpp index 08a5373..2dfc465 100644 --- a/src/include/LogConfig.hpp +++ b/src/include/Logger/LogConfig.hpp @@ -1,7 +1,7 @@ #pragma once -#include "ILogOutput.hpp" -#include "LogLevel.hpp" +#include "Logger/ILogOutput.hpp" +#include "Logger/LogLevel.hpp" #include #include diff --git a/src/include/LogEntry.hpp b/src/include/Logger/LogEntry.hpp similarity index 88% rename from src/include/LogEntry.hpp rename to src/include/Logger/LogEntry.hpp index 9a6a3cc..7914986 100644 --- a/src/include/LogEntry.hpp +++ b/src/include/Logger/LogEntry.hpp @@ -1,8 +1,7 @@ #pragma once -#include "LogLevel.hpp" +#include "Logger/LogLevel.hpp" -#include #include namespace Logging { diff --git a/src/include/LogLevel.hpp b/src/include/Logger/LogLevel.hpp similarity index 100% rename from src/include/LogLevel.hpp rename to src/include/Logger/LogLevel.hpp diff --git a/src/include/LogOutputConsole.hpp b/src/include/Logger/LogOutputConsole.hpp similarity index 72% rename from src/include/LogOutputConsole.hpp rename to src/include/Logger/LogOutputConsole.hpp index fec5717..0a713be 100644 --- a/src/include/LogOutputConsole.hpp +++ b/src/include/Logger/LogOutputConsole.hpp @@ -1,7 +1,9 @@ #pragma once -#include "ILogOutput.hpp" -#include +#include "Logger/ILogOutput.hpp" +#include "Logger/LogEntry.hpp" + +#include namespace Logging { diff --git a/src/include/LogOutputFile.hpp b/src/include/Logger/LogOutputFile.hpp similarity index 92% rename from src/include/LogOutputFile.hpp rename to src/include/Logger/LogOutputFile.hpp index b3ef7f0..8affb07 100644 --- a/src/include/LogOutputFile.hpp +++ b/src/include/Logger/LogOutputFile.hpp @@ -1,8 +1,11 @@ #pragma once -#include "ILogOutput.hpp" +#include "Logger/ILogOutput.hpp" +#include "Logger/LogEntry.hpp" + #include #include +#include namespace Logging { diff --git a/src/include/LogOutputMock.hpp b/src/include/Logger/LogOutputMock.hpp similarity index 79% rename from src/include/LogOutputMock.hpp rename to src/include/Logger/LogOutputMock.hpp index 02eaaeb..f6deef1 100644 --- a/src/include/LogOutputMock.hpp +++ b/src/include/Logger/LogOutputMock.hpp @@ -1,6 +1,9 @@ #pragma once -#include "ILogOutput.hpp" +#include "Logger/ILogOutput.hpp" +#include "Logger/LogEntry.hpp" + +#include namespace Logging { diff --git a/src/include/Logger.hpp b/src/include/Logger/Logger.hpp similarity index 85% rename from src/include/Logger.hpp rename to src/include/Logger/Logger.hpp index af3622d..5e32714 100644 --- a/src/include/Logger.hpp +++ b/src/include/Logger/Logger.hpp @@ -1,10 +1,9 @@ #pragma once -#include "LogConfig.hpp" -#include "LogEntry.hpp" -#include "LogLevel.hpp" +#include "Logger/LogConfig.hpp" +#include "Logger/LogEntry.hpp" +#include "Logger/LogLevel.hpp" -#include #include namespace Logging { diff --git a/src/include/Profiler.hpp b/src/include/Logger/Profiler.hpp similarity index 85% rename from src/include/Profiler.hpp rename to src/include/Logger/Profiler.hpp index 02fa7f8..13fb95f 100644 --- a/src/include/Profiler.hpp +++ b/src/include/Logger/Profiler.hpp @@ -1,6 +1,9 @@ #pragma once -#include "Logger.hpp" +#include "Logger/Logger.hpp" + +#include +#include namespace Logging { @@ -23,4 +26,4 @@ class Profiler { std::chrono::time_point m_lastTime; }; -} // namespace Logging \ No newline at end of file +} // namespace Logging diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 23ddb16..e8e7e27 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,13 +6,16 @@ add_executable(${targetName} ${CMAKE_CURRENT_LIST_DIR}/LogLevel.gtest.cpp ${CMAKE_CURRENT_LIST_DIR}/LogConfig.gtest.cpp ${CMAKE_CURRENT_LIST_DIR}/LogOutput.gtest.cpp + ${CMAKE_CURRENT_LIST_DIR}/Profiler.gtest.cpp ) # Link to required libraries -target_link_libraries(${targetName} PUBLIC Logging gtest_main) -set_target_properties(${targetName} PROPERTIES FOLDER "${ideFolderSource}") +target_link_libraries(${targetName} PUBLIC Logger::Logger gtest_main) +set_target_properties(${targetName} PROPERTIES FOLDER "${ideFolderTests}") # Setup project settings set_project_warnings(${targetName}) # Which warnings to enable set_compile_options(${targetName}) # Which extra compiler flags to enable set_output_directory(${targetName}) # Set the output directory of the library + +add_test(NAME Logger.UnitTests COMMAND ${targetName}) diff --git a/tests/LogConfig.gtest.cpp b/tests/LogConfig.gtest.cpp index dc1257f..32b72c5 100644 --- a/tests/LogConfig.gtest.cpp +++ b/tests/LogConfig.gtest.cpp @@ -1,10 +1,10 @@ #include "gtest/gtest.h" -#include "Logger.hpp" -#include "LogConfig.hpp" -#include "LogOutputFile.hpp" -#include "LogOutputMock.hpp" -#include "LogOutputConsole.hpp" +#include "Logger/LogConfig.hpp" +#include "Logger/LogOutputConsole.hpp" +#include "Logger/LogOutputFile.hpp" +#include "Logger/LogOutputMock.hpp" +#include "Logger/Logger.hpp" namespace Logging { namespace GTest { @@ -18,7 +18,7 @@ TEST(LogConfig, DisableLogging) { // Disbled logging config.SetLogEnabled(false); - logger.Log(LogLevel::Info, "Testing Entry 1"); + logger.Log(LogLevel::Info, "Testing Entry 1"); logger.Log(LogLevel::Debug, "Testing Entry 2"); logger.Log(LogLevel::Error, "Testing Entry 3"); logger.Flush(); @@ -27,7 +27,7 @@ TEST(LogConfig, DisableLogging) { // Enable logging config.SetLogEnabled(true); - logger.Log(LogLevel::Info, "Testing Entry 1"); + logger.Log(LogLevel::Info, "Testing Entry 1"); logger.Log(LogLevel::Debug, "Testing Entry 2"); logger.Flush(); @@ -35,11 +35,11 @@ TEST(LogConfig, DisableLogging) { // Disbled logging config.SetLogEnabled(false); - logger.Log(LogLevel::Info, "Testing Entry 1"); + logger.Log(LogLevel::Info, "Testing Entry 1"); logger.Log(LogLevel::Debug, "Testing Entry 2"); logger.Flush(); - EXPECT_EQ(mock->m_logEntries.size(), 2u); // Still 2 entries + EXPECT_EQ(mock->m_logEntries.size(), 2u); // Still 2 entries } TEST(LogConfig, MinLogLevel) { @@ -51,11 +51,11 @@ TEST(LogConfig, MinLogLevel) { //! Add 5 log entries and check the mock contains the expected amount of entries. const auto RunTest = [&](const std::size_t expectCount) { - logger.Log(LogLevel::Info, "This is info."); - logger.Log(LogLevel::Debug, "This is debug."); + logger.Log(LogLevel::Info, "This is info."); + logger.Log(LogLevel::Debug, "This is debug."); logger.Log(LogLevel::Warning, "This is warning."); - logger.Log(LogLevel::Error, "This is error."); - logger.Log(LogLevel::Critical,"This is critical."); + logger.Log(LogLevel::Error, "This is error."); + logger.Log(LogLevel::Critical, "This is critical."); logger.Flush(); EXPECT_EQ(mock->m_logEntries.size(), expectCount); @@ -98,5 +98,5 @@ TEST(LogConfig, LogOutputs) { EXPECT_EQ(config.LogOutputs().size(), 5u); } -} -} +} // namespace GTest +} // namespace Logging diff --git a/tests/LogEntry.gtest.cpp b/tests/LogEntry.gtest.cpp index ff8728a..7604141 100644 --- a/tests/LogEntry.gtest.cpp +++ b/tests/LogEntry.gtest.cpp @@ -1,27 +1,26 @@ #include "gtest/gtest.h" -#include "LogEntry.hpp" +#include "Logger/LogEntry.hpp" + #include namespace Logging { namespace GTest { TEST(LogEntry, OutputFormat) { - LogEntry logEntry{LogLevel::Info, "This is a test", "2023-02-03 15:47:00"}; - const std::string expected = "2023-02-03 15:47:00 [Info] This is a test"; + LogEntry logEntry{LogLevel::Info, "This is a test", "2023-02-03 15:47:00"}; + const std::string expected = "2023-02-03 15:47:00 [Info] This is a test"; - EXPECT_STREQ(logEntry.OutputText().c_str(), expected.c_str()); + EXPECT_STREQ(logEntry.OutputText().c_str(), expected.c_str()); +} -/* - std::regex r("%d%d.%d%d.%d%d %d%d-%d%d-%d%d%d%d [%s] %s"); +TEST(LogEntry, OutputTextPattern) { + LogEntry logEntry{LogLevel::Warning, "Pattern Check", "01.02.2023 03:04:05"}; + const auto output = logEntry.OutputText(); - std::smatch match; - std::regex_search(logEntry.OutputText(), match, r); -*/ - // TODO: - // - Check the output string has proper format. Should formatting be job of entry? For now ok. - // - Use all functions once so it's covered.. + std::regex pattern(R"(^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2} \[Warning\] Pattern Check$)"); + EXPECT_TRUE(std::regex_match(output, pattern)); } -} -} +} // namespace GTest +} // namespace Logging diff --git a/tests/LogLevel.gtest.cpp b/tests/LogLevel.gtest.cpp index bec0cb4..613962b 100644 --- a/tests/LogLevel.gtest.cpp +++ b/tests/LogLevel.gtest.cpp @@ -1,18 +1,18 @@ #include "gtest/gtest.h" -#include "LogLevel.hpp" +#include "Logger/LogLevel.hpp" namespace Logging { namespace GTest { TEST(LogLevel, LevelToText) { - EXPECT_STREQ(LevelToText(LogLevel::Any).c_str(), "[Any]"); - EXPECT_STREQ(LevelToText(LogLevel::Info).c_str(), "[Info]"); - EXPECT_STREQ(LevelToText(LogLevel::Debug).c_str(), "[Debug]"); - EXPECT_STREQ(LevelToText(LogLevel::Warning).c_str(), "[Warning]"); - EXPECT_STREQ(LevelToText(LogLevel::Error).c_str(), "[Error]"); - EXPECT_STREQ(LevelToText(LogLevel::Critical).c_str(), "[Critical]"); + EXPECT_STREQ(LevelToText(LogLevel::Any).c_str(), "[Any]"); + EXPECT_STREQ(LevelToText(LogLevel::Info).c_str(), "[Info]"); + EXPECT_STREQ(LevelToText(LogLevel::Debug).c_str(), "[Debug]"); + EXPECT_STREQ(LevelToText(LogLevel::Warning).c_str(), "[Warning]"); + EXPECT_STREQ(LevelToText(LogLevel::Error).c_str(), "[Error]"); + EXPECT_STREQ(LevelToText(LogLevel::Critical).c_str(), "[Critical]"); } -} -} +} // namespace GTest +} // namespace Logging diff --git a/tests/LogOutput.gtest.cpp b/tests/LogOutput.gtest.cpp index 8af1aae..65d3357 100644 --- a/tests/LogOutput.gtest.cpp +++ b/tests/LogOutput.gtest.cpp @@ -1,20 +1,23 @@ #include "gtest/gtest.h" -#include "Logger.hpp" -#include "LogLevel.hpp" -#include "LogOutputConsole.hpp" -#include "LogOutputMock.hpp" -#include "LogOutputFile.hpp" +#include "Logger/LogLevel.hpp" +#include "Logger/LogOutputConsole.hpp" +#include "Logger/LogOutputFile.hpp" +#include "Logger/LogOutputMock.hpp" +#include "Logger/Logger.hpp" + +#include #include #include +#include namespace Logging { namespace GTest { TEST(LogOutput, Mock) { - auto mock = std::make_shared(); + auto mock = std::make_shared(); - LogConfig config; + LogConfig config; config.AddLogOutput(mock); { @@ -24,31 +27,58 @@ TEST(LogOutput, Mock) { logger.Log(LogLevel::Error, "Error text"); } - EXPECT_EQ(mock->m_logEntries.size(), 3); - + EXPECT_EQ(mock->m_logEntries.size(), 3); + EXPECT_STREQ(mock->m_logEntries[0].m_text.c_str(), "This is a test."); EXPECT_EQ(mock->m_logEntries[0].m_level, LogLevel::Info); - EXPECT_STREQ(mock->m_logEntries[1].m_text.c_str(), "Debug Entry"); + EXPECT_STREQ(mock->m_logEntries[1].m_text.c_str(), "Debug Entry"); EXPECT_EQ(mock->m_logEntries[1].m_level, LogLevel::Debug); - EXPECT_STREQ(mock->m_logEntries[2].m_text.c_str(), "Error text"); + EXPECT_STREQ(mock->m_logEntries[2].m_text.c_str(), "Error text"); EXPECT_EQ(mock->m_logEntries[2].m_level, LogLevel::Error); } +TEST(LogOutput, MockCopiesVectorInOrder) { + LogOutputMock mock; + std::vector entries{ + {LogLevel::Info, "Vector entry 1", "time1"}, + {LogLevel::Debug, "Vector entry 2", "time2"}, + }; + + mock.Write(entries); + mock.Write(LogEntry{LogLevel::Error, "Single entry", "time3"}); + + ASSERT_EQ(mock.m_logEntries.size(), 3u); + EXPECT_EQ(mock.m_logEntries[0].m_text, "Vector entry 1"); + EXPECT_EQ(mock.m_logEntries[1].m_text, "Vector entry 2"); + EXPECT_EQ(mock.m_logEntries[2].m_text, "Single entry"); +} + +TEST(LogOutput, ConsoleUsesClog) { + LogOutputConsole console; + std::ostringstream capture; + auto* originalBuffer = std::clog.rdbuf(capture.rdbuf()); + + console.Write(LogEntry{LogLevel::Info, "Console entry", "2023-02-03 15:47:00"}); + console.Write(std::vector{{LogLevel::Warning, "Vector console entry", "2023-02-03 15:48:00"}}); + + std::clog.rdbuf(originalBuffer); + + const auto output = capture.str(); + EXPECT_NE(output.find("[Info] Console entry"), std::string::npos); + EXPECT_NE(output.find("[Warning] Vector console entry"), std::string::npos); +} + TEST(LogOutput, FileBasic) { - auto logFile1 = std::make_shared("Log1.txt"); + auto logFile1 = std::make_shared("Log1.txt"); auto logFile2 = std::make_shared("Log2.txt"); - LogConfig config; + LogConfig config; config.AddLogOutput(logFile1); config.AddLogOutput(logFile2); - std::array expectOutput { - "[Info] This is a test.", - "[Debug] Debug Entry", - "[Error] Error text" - }; + std::array expectOutput{"[Info] This is a test.", "[Debug] Debug Entry", "[Error] Error text"}; //! Lambda to check a files content is the expectedOutput. auto compareExpect = [&expectOutput](const std::string& fileName) { @@ -56,7 +86,7 @@ TEST(LogOutput, FileBasic) { std::ifstream file(fileName.c_str()); unsigned counter = 0; std::string text; - while(getline(file, text)) { + while (getline(file, text)) { // line contains the timestap so we use .find() EXPECT_TRUE(text.find(expectOutput[counter]) != std::string::npos); ++counter; @@ -67,7 +97,7 @@ TEST(LogOutput, FileBasic) { // Write data to file { Logger logger = Logger(config); - logger.Log(LogLevel::Info,"This is a test."); + logger.Log(LogLevel::Info, "This is a test."); logger.Log(LogLevel::Debug, "Debug Entry"); logger.Log(LogLevel::Error, "Error text"); } @@ -83,35 +113,29 @@ TEST(LogOutput, FileBasic) { TEST(LogOutput, FileMaxSize) { static constexpr std::uintmax_t maxSize = 50; - auto logFile = std::make_shared("Logfile.txt", maxSize); + auto logFile = std::make_shared("Logfile.txt", maxSize); - LogConfig config; + LogConfig config; config.AddLogOutput(logFile); // With timestamp/loglevel, this string produces output > maxSize Bytes const std::string testString('a', maxSize); // Write to output file - { - Logger(config).Log(LogLevel::Info, testString); - } - EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after next write + { Logger(config).Log(LogLevel::Info, testString); } + EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after next write EXPECT_TRUE(std::filesystem::exists("Logfile(1).txt")); // Wrapping happens after a write - EXPECT_FALSE(std::filesystem::exists("Logfile(2).txt")); // Does not exist yet + EXPECT_FALSE(std::filesystem::exists("Logfile(2).txt")); // Does not exist yet // Less than maxSize Bytes - { - Logger(config).Log(LogLevel::Info, "a"); - } - EXPECT_TRUE(std::filesystem::exists("Logfile.txt")); // New write -> Create file again + { Logger(config).Log(LogLevel::Info, "a"); } + EXPECT_TRUE(std::filesystem::exists("Logfile.txt")); // New write -> Create file again EXPECT_TRUE(std::filesystem::exists("Logfile(1).txt")); // Still exists - EXPECT_FALSE(std::filesystem::exists("Logfile(2).txt")); // Does not exist yet + EXPECT_FALSE(std::filesystem::exists("Logfile(2).txt")); // Does not exist yet // Fill the current log file - { - Logger(config).Log(LogLevel::Debug, testString); - } - EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after new write + { Logger(config).Log(LogLevel::Debug, testString); } + EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after new write EXPECT_TRUE(std::filesystem::exists("Logfile(1).txt")); // Still exists EXPECT_TRUE(std::filesystem::exists("Logfile(2).txt")); // Newly created from write EXPECT_FALSE(std::filesystem::exists("Logfile(3).txt")); // Does not exist yet @@ -122,19 +146,20 @@ TEST(LogOutput, FileMaxSize) { logger.Log(LogLevel::Debug, testString); logger.Log(LogLevel::Debug, testString); } - EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after new write + EXPECT_FALSE(std::filesystem::exists("Logfile.txt")); // New file only created after new write EXPECT_TRUE(std::filesystem::exists("Logfile(1).txt")); // Still exists EXPECT_TRUE(std::filesystem::exists("Logfile(2).txt")); // Still exists - EXPECT_TRUE(std::filesystem::exists("Logfile(3).txt")); // Newly created from write -> One log write always to one file + EXPECT_TRUE( + std::filesystem::exists("Logfile(3).txt")); // Newly created from write -> One log write always to one file EXPECT_FALSE(std::filesystem::exists("Logfile(4).txt")); // Does not exist // Cleanup - EXPECT_NE(std::remove("Logfile.txt"), 0); // Does not exist + EXPECT_NE(std::remove("Logfile.txt"), 0); // Does not exist EXPECT_EQ(std::remove("Logfile(1).txt"), 0); EXPECT_EQ(std::remove("Logfile(2).txt"), 0); EXPECT_EQ(std::remove("Logfile(3).txt"), 0); EXPECT_NE(std::remove("Logfile(4).txt"), 0); // Does not exist } -} -} \ No newline at end of file +} // namespace GTest +} // namespace Logging diff --git a/tests/Profiler.gtest.cpp b/tests/Profiler.gtest.cpp new file mode 100644 index 0000000..94e96c1 --- /dev/null +++ b/tests/Profiler.gtest.cpp @@ -0,0 +1,43 @@ +#include "gtest/gtest.h" + +#include "Logger/LogConfig.hpp" +#include "Logger/LogOutputMock.hpp" +#include "Logger/Profiler.hpp" + +#include +#include + +namespace Logging { +namespace GTest { + +TEST(Profiler, LogsStartStepsAndEnd) { + LogConfig config; + auto mock = std::make_shared(); + config.AddLogOutput(mock); + + { + Logger logger(config); + Profiler profiler(logger, "ProfileCase"); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + profiler.LogStep("First"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + profiler.LogStep("Second"); + } + + ASSERT_EQ(mock->m_logEntries.size(), 4u); + EXPECT_NE(mock->m_logEntries[0].m_text.find("ProfileCase"), std::string::npos); + EXPECT_NE(mock->m_logEntries[0].m_text.find("START"), std::string::npos); + + EXPECT_NE(mock->m_logEntries[1].m_text.find("STEP First"), std::string::npos); + EXPECT_NE(mock->m_logEntries[1].m_text.find("ms"), std::string::npos); + + EXPECT_NE(mock->m_logEntries[2].m_text.find("STEP Second"), std::string::npos); + EXPECT_NE(mock->m_logEntries[2].m_text.find("ms"), std::string::npos); + + EXPECT_NE(mock->m_logEntries[3].m_text.find("END"), std::string::npos); + EXPECT_NE(mock->m_logEntries[3].m_text.find("ProfileCase"), std::string::npos); +} + +} // namespace GTest +} // namespace Logging