diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000..e65112130 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,53 @@ +--- +name: HEP-CE Code Coverage +# Schema: https://json.schemastore.org/github-action.json + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + name: Calculate Code Coverage + runs-on: ubuntu-latest + steps: + - name: Checkout Respond Repository + uses: actions/checkout@v6 + + - name: Install Boost + uses: MarkusJx/install-boost@v2 + id: install-boost + with: + boost_version: 1.84.0 + toolset: gcc + + - name: Install gcovr + run: pip install gcovr + + - name: Build and Test Project + run: > + cmake --preset coverage && cmake --build --preset coverage && ctest --preset coverage + env: + BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} + + - name: Generate gcovr report HTML + run: | + gcovr -r . --filter src --html coverage.html + + - name: Generate gcovr report XML + run: | + gcovr -r . --filter src --xml-pretty -o coverage.xml + + - name: Upload Code Coverage Report + uses: actions/upload-artifact@v7 + with: + name: code-coverage-report + path: coverage.* + + - name: Generate Coverage Badge + uses: GaelGirodon/ci-badges-action@v1 + with: + gist-id: 10e746e28df5d23e91689b01493435a0 + token: ${{secrets.GIST_BADGE_TOKEN}} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f8086a906..fa35f00be 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -3,45 +3,55 @@ name: Docs # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: - push: - branches: ["main"] - workflow_dispatch: + push: + branches: ["main"] + workflow_dispatch: + permissions: - contents: write - + contents: write + jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: "true" - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y wget graphviz rsync - shell: bash - - - name: Install Doxygen v1.9.6 - run: | - transformed_version=$(echo "1.9.6" | tr '.' '_') - wget https://github.com/doxygen/doxygen/releases/download/Release_${transformed_version}/doxygen-1.9.6.linux.bin.tar.gz - tar -xzf doxygen-1.9.6.linux.bin.tar.gz - sudo mv doxygen-1.9.6/bin/doxygen /usr/local/bin/doxygen - shell: bash - - - name: Generate Doxygen Documentation - run: doxygen Doxyfile - shell: bash - - - name: Create .nojekyll (ensures pages with underscores work on gh pages) - run: touch docs/doxygen/html/.nojekyll - shell: bash - - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4 - with: - repository-name: SyndemicsLab/hep-ce - branch: gh-pages - folder: docs/doxygen/html - target-folder: ./ + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: "true" + + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y wget graphviz rsync + shell: bash + + - name: Install Doxygen v1.9.6 + run: | + transformed_version=$(echo "1.9.6" | tr '.' '_') + wget https://github.com/doxygen/doxygen/releases/download/Release_${transformed_version}/doxygen-1.9.6.linux.bin.tar.gz + tar -xzf doxygen-1.9.6.linux.bin.tar.gz + sudo mv doxygen-1.9.6/bin/doxygen /usr/local/bin/doxygen + shell: bash + + - name: Configure Project + run: cmake --preset docs + + - name: Generate Doxygen Documentation + run: doxygen Doxyfile + shell: bash + + - name: Remove polyfill references from generated HTML + run: | + find docs/doxygen/html -type f -name '*.html' -print0 | + xargs -0 sed -i '/polyfill\.io\/v3\/polyfill\.min\.js/d' + shell: bash + + - name: Create .nojekyll (ensures pages with underscores work on gh pages) + run: touch docs/doxygen/html/.nojekyll + shell: bash + + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + repository-name: SyndemicsLab/hep-ce + branch: gh-pages + folder: docs/doxygen/html + target-folder: ./ diff --git a/.github/workflows/linux-unit-testing.yml b/.github/workflows/linux-unit-testing.yml new file mode 100644 index 000000000..c3691be4c --- /dev/null +++ b/.github/workflows/linux-unit-testing.yml @@ -0,0 +1,38 @@ +name: Linux HEP-CE Unit Tests +# Schema: https://json.schemastore.org/github-action.json + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + unit-testing: + name: Testing ${{ matrix.os }} ${{matrix.build-variant}} build + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + + strategy: + matrix: + os: [ubuntu-latest] # macos-latest + build-variant: [shared, static] + steps: + - name: Checkout HEP-CE + uses: actions/checkout@v6 + + - name: Install Boost + uses: MarkusJx/install-boost@v2 + id: install-boost + with: + boost_version: 1.84.0 + toolset: gcc + + - name: Run Testing Workflow + continue-on-error: true + run: cmake --workflow --preset test-debug-${{matrix.build-variant}} + env: + BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }} + timeout-minutes: 10 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml deleted file mode 100644 index 0e006a058..000000000 --- a/.github/workflows/unit-tests.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Unit Tests - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -permissions: - contents: write - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Debug - -jobs: - build: - name: Build (${{ matrix.os_name }}) - runs-on: ${{ matrix.runs_on }} - strategy: - fail-fast: false - matrix: - include: - - os_name: ubuntu - runs_on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Boost, SQLite, and gcovr (Ubuntu) - if: matrix.os_name == 'ubuntu' - run: sudo apt-get update && sudo apt-get install -y libboost-dev libsqlitecpp-dev gcovr - - - name: Build and Test (Ubuntu) - if: matrix.os_name == 'ubuntu' - working-directory: ${{ github.workspace }} - run: cmake --workflow --preset gcc-debug - - - name: Generate Coverage Badge JSON - if: matrix.os_name == 'ubuntu' - shell: bash - run: | - set -euo pipefail - mkdir -p .github/badges - - gcovr --txt-summary | tee coverage-summary.txt - coverage_pct=$(awk '/^lines:/{print $2}' coverage-summary.txt) - coverage_num=${coverage_pct%%%} - - color=$(awk -v n="$coverage_num" 'BEGIN { - if (n >= 90) print "brightgreen"; - else if (n >= 80) print "green"; - else if (n >= 70) print "yellow"; - else if (n >= 60) print "orange"; - else print "red"; - }') - - cat > .github/badges/coverage.json <") -set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Syndemics Modeling Library") -set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) -set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) -set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) -set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}) -if(PROJECT_VERSION_TWEAK) - set(CPACK_PACKAGE_VERSION ${CPACK_PACKAGE_VERSION}.${PROJECT_VERSION_TWEAK}) -endif() -set(CPACK_PACKAGE_RELOCATABLE ON CACHE BOOL "Build relocatable package") - -set(CPACK_RPM_PACKAGE_LICENSE "AGPLv3") -set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries") -set(CPACK_DEBIAN_PACKAGE_SECTION "libs") -set(CPACK_RPM_PACKAGE_URL ${CPACK_PROJECT_URL}) -set(CPACK_DEBIAN_PACKAGE_HOMEPAGE ${CPACK_PROJECT_URL}) -set(CPACK_RPM_PACKAGE_DESCRIPTION "Syndemics Modeling Library.") -set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "Syndemics Modeling Library.") - -if(CPACK_PACKAGE_NAME) - set(CPACK_RPM_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") - set(CPACK_DEBIAN_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") -else() - set(CPACK_RPM_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}") - set(CPACK_DEBIAN_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}") - set(CPACK_RPM_PACKAGE_NAME "${PROJECT_NAME}") - set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}") -endif() - -if(CPACK_RPM_PACKAGE_RELEASE) - set(CPACK_RPM_FILE_NAME "${CPACK_RPM_FILE_NAME}-${CPACK_RPM_PACKAGE_RELEASE}") -endif() -if(CPACK_DEBIAN_PACKAGE_RELEASE) - set(CPACK_DEBIAN_FILE_NAME "${CPACK_DEBIAN_FILE_NAME}-${CPACK_DEBIAN_PACKAGE_RELEASE}") -endif() - -if(CPACK_RPM_PACKAGE_ARCHITECTURE) - set(CPACK_RPM_FILE_NAME "${CPACK_RPM_FILE_NAME}.${CPACK_RPM_PACKAGE_ARCHITECTURE}") -endif() -if(CPACK_DEBIAN_PACKAGE_ARCHITECTURE) - set(CPACK_DEBIAN_FILE_NAME "${CPACK_DEBIAN_FILE_NAME}.${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}") -endif() -set(CPACK_RPM_FILE_NAME "${CPACK_RPM_FILE_NAME}.rpm") -set(CPACK_DEBIAN_FILE_NAME "${CPACK_DEBIAN_FILE_NAME}.deb") - -if(NOT CPACK_PACKAGE_RELOCATABLE) - # Depend on pkgconfig rpm to create the system pkgconfig folder - set(CPACK_RPM_PACKAGE_REQUIRES pkgconfig) - set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION - "${CPACK_PACKAGING_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/pkgconfig") -endif() - -include(CPack) \ No newline at end of file diff --git a/cmake/hepceConfig.cmake.in b/cmake/hepceConfig.cmake.in index a429689bc..8780aa632 100644 --- a/cmake/hepceConfig.cmake.in +++ b/cmake/hepceConfig.cmake.in @@ -2,9 +2,9 @@ include(CMakeFindDependencyMacro) -find_dependency(spdlog REQUIRED) -find_dependency(SQLiteCpp REQUIRED) find_dependency(Boost REQUIRED) +find_dependency(SQLiteCpp REQUIRED) +find_dependency(spdlog REQUIRED) if(@HEPCE_BUILD_TESTS@) find_depenency(GTest REQUIRED) diff --git a/cmake/options.cmake b/cmake/options.cmake index bdeb5d62d..d1221432c 100644 --- a/cmake/options.cmake +++ b/cmake/options.cmake @@ -3,6 +3,12 @@ option(HEPCE_BUILD_ALL "Build all artifacts" OFF) # build position independent code option(HEPCE_BUILD_PIC "Build position independent code (-fPIC)" OFF) +# shared_libs options +option(BUILD_SHARED_LIBS "Enable shared libraries" ON) + +# build documentation +option(HEPCE_BUILD_DOCS "Build Documentation" ON) + option(HEPCE_BUILD_EXECUTABLE "Build Executable for HEPCE" ON) # testing options @@ -11,17 +17,12 @@ option(HEPCE_BUILD_TESTS "Build tests" OFF) # coverage options (only valid if HEPCE_BUILD_TESTS is ON) option(HEPCE_CALCULATE_COVERAGE "Calculate Code Coverage" OFF) -# pybind options -option(HEPCE_BUILD_PYBINDINGS "Build python bindings" OFF) - # bench options -option(HEPCE_BUILD_BENCH "Build benchmarks (Requires https://github.com/google/benchmark.git to be installed)" OFF) - -# shared_libs options -option(HEPCE_BUILD_SHARED_LIBS "Enable shared libraries" ${BUILD_SHARED_LIBS}) +option(HEPCE_BUILD_BENCH "Build benchmarks" OFF) # warning options option(HEPCE_BUILD_WARNINGS "Enable compiler warnings" OFF) +option(HEPCE_NO_EXCEPTIONS "Compile with -fno-exceptions. Call abort() on any simdemics exceptions" OFF) # stop on warnings option(HEPCE_STOP_ON_WARNINGS "Stop Execution if a warning is occurred" OFF) @@ -32,4 +33,4 @@ option(HEPCE_RUN_OMP "Enable omp runtime" OFF) # install options option(HEPCE_SYSTEM_INCLUDES "Include as system headers (skip for clang-tidy)." OFF) option(HEPCE_INSTALL "Generate the install target" ${HEPCE_MASTER_PROJECT}) -option(HEPCE_NO_EXCEPTIONS "Compile with -fno-exceptions. Call abort() on any simdemics exceptions" OFF) + diff --git a/include/hepce/utils/logging.hpp b/include/hepce/utils/logging.hpp index 21f517e21..965091bd6 100644 --- a/include/hepce/utils/logging.hpp +++ b/include/hepce/utils/logging.hpp @@ -4,10 +4,10 @@ // Created Date: 2025-04-17 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2025-09-23 // -// Modified By: Dimitri Baptiste // +// Last Modified: 2026-06-22 // +// Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #ifndef HEPCE_UTILS_LOGGING_HPP_ #define HEPCE_UTILS_LOGGING_HPP_ @@ -17,22 +17,150 @@ namespace hepce { namespace utils { -enum class LogType : int { kInfo, kWarn, kError, kDebug, kCount }; +/// @brief Logging levels for the logger. +enum class LogType : int { + kInfo = 0, // Information Messages + kWarn = 1, // Warning Messages + kError = 2, // Error Messages + kDebug = 3, // Debug Messages + kCount = 4 // Enum Counter +}; + +/// @brief Creation status of the logger. enum class CreationStatus : int { - kError = -1, - kSuccess = 0, - kExists = 1, - kNotCreated = 2, - kCount = 4 + kError = -1, // Error Creating Logger + kSuccess = 0, // Logger Created Successfully + kExists = 1, // Logger Already Exists + kNotCreated = 2, // Logger Not Created + kCount = 4 // Enum Counter +}; + +/// @brief Log pattern templates for different output styles. +enum class LogPattern : int { + kSimple = 0, // Minimal: [%n] %v + kStandard = 1, // Default: [%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v + kDetailed = 2, // Full: [%Y-%m-%d %H:%M:%S.%e] [%n] [%^%L%$] [thread %t] %v + kThreadSafe = + 3 // With sequence number: [%H:%M:%S] [seq %i] [%n] [%^---%L---%$] %v }; +// ============================================================================ +// Core Logger Creation and Management +// ============================================================================ + +/// @brief Create a logger with the specified name and file path. +/// Thread-safe for use in parallel execution contexts. +/// @param logger_name Unique identifier for this logger. +/// @param filepath File path where the logger will write logs. +/// @return CreationStatus indicating the result of logger creation. +/// @note If a logger with the same name already exists, kExists is returned. CreationStatus CreateFileLogger(const std::string &logger_name, const std::string &filepath); -void DropLogger(const std::string &logger_name); + +// ============================================================================ +// Parallel Execution Support: Shared File Sink +// ============================================================================ + +/// @brief Create a shared file sink that multiple loggers can write to. +/// This enables thread-safe concurrent logging from multiple models/threads +/// to the same output file. Use with CreateSharedLogger(). +/// @param filepath Path to the shared log file. +/// @return CreationStatus indicating success or failure. +/// @note Thread-safe: Can be called from multiple threads simultaneously. +/// @note Creates the file sink once; subsequent calls return kExists. +CreationStatus CreateSharedFileSink(const std::string &filepath); + +/// @brief Create a logger that writes to the shared file sink. +/// Multiple loggers can be created with different names but write to the +/// same file through the shared sink. Essential for parallel execution. +/// @param logger_name Unique identifier for this logger. +/// @return CreationStatus indicating the result of logger creation. +/// @note Thread-safe: Can be called concurrently from multiple threads. +/// @note Requires CreateSharedFileSink() to be called first with a file path. +/// @note If CreateSharedFileSink() wasn't called, creates a default sink to +/// "respond.log". +CreationStatus CreateSharedLogger(const std::string &logger_name); + +/// @brief Sets the logging pattern template for all subsequent logger +/// creations. +/// @param pattern The LogPattern enum value to use. +/// @note Affects CreateFileLogger() and CreateSharedLogger() calls made after +/// this. +void SetLogPattern(LogPattern pattern); + +/// @brief Gets the current logging pattern template. +/// @return The active LogPattern enum value. +LogPattern GetLogPattern(); + +/// @brief Sets the global flush interval for automatic buffer flushing. +/// @param seconds Interval in seconds for automatic flush (0 to disable +/// auto-flush). +/// @note Thread-safe configuration change. +void SetFlushInterval(int seconds); + +/// @brief Flushes all active loggers, ensuring buffered output is written. +/// Useful when terminating parallel execution or before critical operations. +/// @note Thread-safe operation. +void FlushAllLoggers(); + +// ============================================================================ +// Logging Functions +// ============================================================================ + +/// @brief Log a message as information level. +/// Thread-safe for concurrent calls from multiple threads. +/// @param logger_name Logger identifier (created via CreateFileLogger or +/// CreateSharedLogger). +/// @param message Message to log. void LogInfo(const std::string &logger_name, const std::string &message); + +/// @brief Log a message as warning level. +/// Thread-safe for concurrent calls from multiple threads. +/// @param logger_name Logger identifier. +/// @param message Message to log. void LogWarning(const std::string &logger_name, const std::string &message); + +/// @brief Log a message as error level. +/// Thread-safe for concurrent calls from multiple threads. +/// @param logger_name Logger identifier. +/// @param message Message to log. void LogError(const std::string &logger_name, const std::string &message); + +/// @brief Log a message as debug level. +/// Thread-safe for concurrent calls from multiple threads. +/// @param logger_name Logger identifier. +/// @param message Message to log. void LogDebug(const std::string &logger_name, const std::string &message); + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/// @brief Check if a logger with the given name exists. +/// @param logger_name Logger identifier to check. +/// @return CreationStatus::kExists if logger exists, kNotCreated otherwise. +/// @note Thread-safe query. +CreationStatus CheckLoggerExists(const std::string &logger_name); + +/// @brief Retrieve detailed information about a logger. +/// @param logger_name Logger identifier to query. +/// @return String containing logger name, file path, level, and thread info. +/// @note Thread-safe operation. +std::string GetLoggerInfo(const std::string &logger_name); + +/// @brief Set the logging level for a specific logger. +/// @param logger_name Logger identifier to configure. +/// @param level Log level: 0=trace, 1=debug, 2=info, 3=warn, 4=error, +/// 5=critical. +/// @return CreationStatus::kSuccess if level was set, kNotCreated if logger +/// doesn't exist. +void SetLoggerLevel(const std::string &logger_name, int level); + +/// @brief Utility function to build a string message corresponding to the +/// exception. +/// @param error The exception object to extract the message from. +/// @param message The message object to append to the exception message. +/// @return A string containing the combined message and exception details. std::string ConstructMessage(const std::exception &error, std::string message); } // namespace utils } // namespace hepce diff --git a/src/utils/internals/logging_internals.hpp b/src/utils/internals/logging_internals.hpp index 8c4ffd60d..c09ce78ce 100644 --- a/src/utils/internals/logging_internals.hpp +++ b/src/utils/internals/logging_internals.hpp @@ -4,10 +4,10 @@ // Created Date: 2025-04-18 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2025-05-02 // +// Last Modified: 2026-06-22 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #ifndef HEPCE_UTILS_LOGGINGINTERNALS_HPP_ #define HEPCE_UTILS_LOGGINGINTERNALS_HPP_ @@ -19,11 +19,91 @@ #include #include +#include #include namespace hepce { namespace utils { -void DropLogger(const std::string &logger_name) { spdlog::drop(logger_name); } +class LoggingConfig { +public: + static LoggingConfig &GetInstance() { + static LoggingConfig instance; + return instance; + } + + static std::shared_ptr + GetSharedSink(const std::string &filepath) { + std::lock_guard lock(GetInstance().sink_mutex_); + auto key = filepath; + if (GetInstance().shared_sinks_.find(key) == + GetInstance().shared_sinks_.end()) { + try { + GetInstance().shared_sinks_[key] = + std::make_shared( + filepath, false); + } catch (const spdlog::spdlog_ex &ex) { + std::cerr << "Failed to create shared sink: " << ex.what() + << std::endl; + return nullptr; + } + } + return GetInstance().shared_sinks_[key]; + } + + static LogPattern GetPattern() { return GetInstance().current_pattern_; } + + static void SetPattern(LogPattern pattern) { + GetInstance().current_pattern_ = pattern; + } + + static std::string GetPatternString(LogPattern pattern) { + switch (pattern) { + case LogPattern::kSimple: + return "[%n] %v"; + case LogPattern::kStandard: + return "[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v"; + case LogPattern::kDetailed: + return "[%Y-%m-%d %H:%M:%S.%e] [%n] [%^%L%$] [thread %t] %v"; + case LogPattern::kThreadSafe: + return "[%H:%M:%S] [seq %i] [%n] [%^---%L---%$] %v"; + default: + return "[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v"; + } + } + + static int GetFlushInterval() { return GetInstance().flush_interval_; } + + static void SetFlushInterval(int seconds) { + GetInstance().flush_interval_ = seconds; + } + + static void SetDefaultSinkPath(const std::string &path) { + std::lock_guard lock(GetInstance().config_mutex_); + GetInstance().default_sink_path_ = path; + } + + static std::string GetDefaultSinkPath() { + std::lock_guard lock(GetInstance().config_mutex_); + return GetInstance().default_sink_path_; + } + +private: + LoggingConfig() + : current_pattern_(LogPattern::kStandard), flush_interval_(3), + default_sink_path_("hepce.log") { + spdlog::cfg::load_env_levels(); + } + + std::unordered_map> + shared_sinks_; + std::mutex sink_mutex_; + std::mutex config_mutex_; + LogPattern current_pattern_; + int flush_interval_; + std::string default_sink_path_; +}; + CreationStatus CheckIfExists(const std::string &logger_name) { return (spdlog::get(logger_name) != nullptr) ? CreationStatus::kExists : CreationStatus::kNotCreated; @@ -60,7 +140,9 @@ void log(const std::string &logger_name, const std::string &message, logger->info(message); break; } - logger->flush(); + if (LoggingConfig::GetFlushInterval() == 0) { + logger->flush(); + } } else { spdlog::error("Logger {} not found", logger_name); } diff --git a/src/utils/logging.cpp b/src/utils/logging.cpp index f77aa044e..b49fe8cf6 100644 --- a/src/utils/logging.cpp +++ b/src/utils/logging.cpp @@ -4,10 +4,10 @@ // Created Date: 2025-04-18 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2025-05-12 // +// Last Modified: 2026-06-22 // // Modified By: Matthew Carroll // // ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // //////////////////////////////////////////////////////////////////////////////// #include @@ -24,9 +24,10 @@ CreationStatus CreateFileLogger(const std::string &logger_name, } try { spdlog::cfg::load_env_levels(); - spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v"); - spdlog::flush_every(std::chrono::seconds(3)); - spdlog::basic_logger_mt(logger_name, filepath, true); + std::string pattern = + LoggingConfig::GetPatternString(LoggingConfig::GetPattern()); + spdlog::set_pattern(pattern); + spdlog::basic_logger_mt(logger_name, filepath); } catch (const spdlog::spdlog_ex &ex) { std::cout << "Log init failed: " << ex.what() << std::endl; return CreationStatus::kError; @@ -34,6 +35,73 @@ CreationStatus CreateFileLogger(const std::string &logger_name, return CreationStatus::kSuccess; } +CreationStatus CreateSharedFileSink(const std::string &filepath) { + try { + auto sink = LoggingConfig::GetSharedSink(filepath); + if (sink) { + LoggingConfig::SetDefaultSinkPath(filepath); + return CreationStatus::kSuccess; + } + std::string error_msg = + "Failed to create shared file sink: sink is null"; + std::cerr << error_msg << std::endl; + return CreationStatus::kError; + } catch (const spdlog::spdlog_ex &ex) { + std::string error_msg = "Failed to create shared file sink at '" + + filepath + "': " + ex.what(); + std::cerr << error_msg << std::endl; + return CreationStatus::kError; + } +} + +CreationStatus CreateSharedLogger(const std::string &logger_name) { + if (CheckIfExists(logger_name) == CreationStatus::kExists) { + std::cout << "Shared logger " << logger_name << " already exists" + << std::endl; + return CreationStatus::kExists; + } + + try { + std::string filepath = LoggingConfig::GetDefaultSinkPath(); + auto sink = LoggingConfig::GetSharedSink(filepath); + if (!sink) { + std::string error_msg = + "Failed to create shared logger '" + logger_name + + "': could not get or create shared sink at '" + filepath + "'"; + std::cerr << error_msg << std::endl; + return CreationStatus::kError; + } + + spdlog::cfg::load_env_levels(); + std::string pattern = + LoggingConfig::GetPatternString(LoggingConfig::GetPattern()); + + auto logger = std::make_shared(logger_name, sink); + logger->set_pattern(pattern); + logger->set_level(spdlog::level::trace); + + spdlog::register_logger(logger); + + return CreationStatus::kSuccess; + } catch (const spdlog::spdlog_ex &ex) { + std::string error_msg = "Failed to create shared logger '" + + logger_name + "': " + ex.what(); + std::cerr << error_msg << std::endl; + return CreationStatus::kError; + } +} + +void SetLogPattern(LogPattern pattern) { LoggingConfig::SetPattern(pattern); } + +LogPattern GetLogPattern() { return LoggingConfig::GetPattern(); } + +void SetFlushInterval(int seconds) { LoggingConfig::SetFlushInterval(seconds); } + +void FlushAllLoggers() { + spdlog::apply_all( + [](std::shared_ptr log) { log->flush(); }); +} + void LogInfo(const std::string &logger_name, const std::string &message) { log(logger_name, message, LogType::kInfo); } @@ -50,6 +118,33 @@ void LogDebug(const std::string &logger_name, const std::string &message) { log(logger_name, message, LogType::kDebug); } +CreationStatus CheckLoggerExists(const std::string &logger_name) { + return CheckIfExists(logger_name); +} + +std::string GetLoggerInfo(const std::string &logger_name) { + auto logger = spdlog::get(logger_name); + if (!logger) { + return "Logger \"" + logger_name + "\" not found"; + } + + auto level_view = spdlog::level::to_string_view(logger->level()); + std::string info = "Logger: " + logger_name + "\n"; + info += + " Level: " + std::string(level_view.data(), level_view.size()) + "\n"; + info += " Sinks: " + std::to_string(logger->sinks().size()); + return info; +} + +void SetLoggerLevel(const std::string &logger_name, int level) { + auto logger = spdlog::get(logger_name); + if (logger) { + int clamped_level = (level < 0) ? 0 : (level > 5) ? 5 : level; + logger->set_level( + static_cast(clamped_level)); + } +} + std::string ConstructMessage(const std::exception &error, std::string message) { std::stringstream msg; msg << message << ": " << error.what(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cf75d6fa2..58928f83a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,44 +1,57 @@ cmake_minimum_required(VERSION 3.27) -project(hepce_tests VERSION ${HEPCE_VERSION} LANGUAGES CXX) +project(hepce_tests LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard to compile with, 17") +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +if(CMAKE_SYSTEM_NAME MATCHES "CYGWIN" OR CMAKE_SYSTEM_NAME MATCHES "MSYS" OR CMAKE_SYSTEM_NAME MATCHES "MINGW") + set(CMAKE_CXX_EXTENSIONS ON) +endif() + +if($ENV{FORCE_COLOR}) + if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") + add_compile_options(-fdiagnostics-color=always) + elseif("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + add_compile_options(-fcolor-diagnostics) + endif() +endif() if(PROJECT_IS_TOP_LEVEL OR NOT HEPCE_BUILD_TESTS) message(STATUS "Attempted to Build tests, but HEPCE_BUILD_TESTS is OFF!") return() endif() +find_package(Boost REQUIRED) -enable_testing() - -add_executable(hepce_tests) - -file(GLOB_RECURSE HEPCE_TEST_FILES CONFIGURE_DEPENDS src/*.cpp) file(GLOB HEPCE_TEST_MOCKS CONFIGURE_DEPENDS mocks/*.hpp) file(GLOB HEPCE_TEST_CONSTANTS CONFIGURE_DEPENDS constants/*.hpp) +file(GLOB_RECURSE HEPCE_UNIT_TESTS CONFIGURE_DEPENDS unit/*.cpp) + +add_executable(hepce_tests) +# Not sure why, but CMake is not inferring the linker language correctly +set_target_properties(hepce_tests PROPERTIES LINKER_LANGUAGE CXX) target_sources(hepce_tests PRIVATE ${HEPCE_TEST_MOCKS} ${HEPCE_TEST_CONSTANTS} - ${HEPCE_TEST_FILES} + ${HEPCE_UNIT_TESTS} ) -target_link_libraries(hepce_tests - PUBLIC - hepce_model - Boost::property_tree - GTest::gmock_main - spdlog::spdlog -) - target_include_directories(hepce_tests PRIVATE "mocks/" "constants/" ) -set(HEPCE_TEST_ARTIFACT_DIR ${CMAKE_BINARY_DIR}/test_artifacts) -file(MAKE_DIRECTORY ${HEPCE_TEST_ARTIFACT_DIR}) +target_link_libraries(hepce_tests + PUBLIC + hepce_model + Boost::headers + GTest::gmock_main + spdlog::spdlog +) -gtest_discover_tests(${PROJECT_NAME} - WORKING_DIRECTORY ${HEPCE_TEST_ARTIFACT_DIR} +gtest_discover_tests(hepce_tests + TEST_PREFIX "hepce_" ) diff --git a/tests/constants/utility.hpp b/tests/constants/utility.hpp index ee7f9d167..655e6b61e 100644 --- a/tests/constants/utility.hpp +++ b/tests/constants/utility.hpp @@ -13,11 +13,17 @@ #define HEPCE_TESTS_CONSTANTS_UTILITY_HPP_ // STL Includes +#include #include +#include +#include // Library Headers #include +// Third-party Includes +#include + namespace hepce { namespace testing { inline void CreateTestLog(std::string log_name) { @@ -26,7 +32,25 @@ inline void CreateTestLog(std::string log_name) { } inline void RemoveTestLog(std::string log_name) { - std::filesystem::remove(log_name + ".log"); + const std::string log_file = log_name + ".log"; + + // Release any open file handles held by the logger before deleting. + if (auto logger = spdlog::get(log_name); logger) { + logger->flush(); + spdlog::drop(log_name); + } + + std::error_code ec; + for (int attempt = 0; attempt < 10; ++attempt) { + std::filesystem::remove(log_file, ec); + if (!ec || !std::filesystem::exists(log_file)) { + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + + throw std::runtime_error("Failed to remove test log file '" + log_file + + "': " + ec.message()); } } // namespace testing } // namespace hepce diff --git a/tests/src/utils/logging_test.cpp b/tests/src/utils/logging_test.cpp deleted file mode 100644 index abe8605f7..000000000 --- a/tests/src/utils/logging_test.cpp +++ /dev/null @@ -1,157 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// File: test_logging.cpp // -// Project: hep-ce // -// Created Date: 2025-05-01 // -// Author: Matthew Carroll // -// ----- // -// Last Modified: 2025-07-23 // -// Modified By: Dimitri Baptiste // -// ----- // -// Copyright (c) 2025 Syndemics Lab at Boston Medical Center // -//////////////////////////////////////////////////////////////////////////////// - -#include - -#include -#include -#include -#include -#include - -#include - -class LoggingTest : public ::testing::Test { -protected: - void SetUp() override {} - void TearDown() override {} -}; - -TEST_F(LoggingTest, LogInfo) { - const std::string LOG_NAME = "info"; - const std::string LOG_FILE = "info.log"; - std::string expected = "Testing"; - std::string line; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - hepce::utils::LogInfo(LOG_NAME, expected); - - std::ifstream f(LOG_FILE); - std::getline(f, line); - f.close(); - - ASSERT_TRUE(line.find(expected) != std::string::npos); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, LogWarning) { - const std::string LOG_NAME = "warn"; - const std::string LOG_FILE = "warn.log"; - std::string expected = "Testing"; - std::string line; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - hepce::utils::LogWarning(LOG_NAME, expected); - - std::ifstream f(LOG_FILE); - std::getline(f, line); - f.close(); - - ASSERT_TRUE(line.find(expected) != std::string::npos); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, LogError) { - const std::string LOG_NAME = "error"; - const std::string LOG_FILE = "error.log"; - std::string expected = "Testing"; - std::string line; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - hepce::utils::LogError(LOG_NAME, expected); - - std::ifstream f(LOG_FILE); - std::getline(f, line); - f.close(); - - ASSERT_TRUE(line.find(expected) != std::string::npos); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, LogDebug) { - const std::string LOG_NAME = "debug"; - const std::string LOG_FILE = "debug.log"; - std::string expected = "Testing"; - std::string line; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - hepce::utils::LogDebug(LOG_NAME, expected); - - std::ifstream f(LOG_FILE); - std::getline(f, line); - f.close(); - - ASSERT_TRUE(line.find(expected) != std::string::npos); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, CreateFileLoggerReturnsExistsWhenLoggerAlreadyRegistered) { - const std::string LOG_NAME = "exists_case"; - const std::string LOG_FILE = "exists_case.log"; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kExists); - - hepce::utils::DropLogger(LOG_NAME); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, DropLoggerAllowsRecreateWithSameName) { - const std::string LOG_NAME = "drop_recreate"; - const std::string LOG_FILE = "drop_recreate.log"; - - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - hepce::utils::DropLogger(LOG_NAME); - ASSERT_EQ(hepce::utils::CreateFileLogger(LOG_NAME, LOG_FILE), - hepce::utils::CreationStatus::kSuccess); - - hepce::utils::DropLogger(LOG_NAME); - std::filesystem::remove(LOG_FILE); -} - -TEST_F(LoggingTest, LogInfoCreatesDefaultLogFileWhenLoggerMissing) { - const std::string LOG_NAME = "implicit_logger_creation"; - const std::string DEFAULT_LOG_FILE = "log.txt"; - const std::string expected = "auto-created message"; - std::string line; - - std::filesystem::remove(DEFAULT_LOG_FILE); - hepce::utils::DropLogger(LOG_NAME); - - hepce::utils::LogInfo(LOG_NAME, expected); - - std::ifstream f(DEFAULT_LOG_FILE); - ASSERT_TRUE(f.is_open()); - std::getline(f, line); - f.close(); - - ASSERT_TRUE(line.find(expected) != std::string::npos); - - hepce::utils::DropLogger(LOG_NAME); - std::filesystem::remove(DEFAULT_LOG_FILE); -} - -TEST_F(LoggingTest, ConstructMessageConcatenatesPrefixAndExceptionText) { - const std::runtime_error err("boom"); - - const std::string msg = - hepce::utils::ConstructMessage(err, "while loading inputs"); - - ASSERT_EQ(msg, "while loading inputs: boom"); -} diff --git a/tests/src/data/types_test.cpp b/tests/unit/data/types_test.cpp similarity index 100% rename from tests/src/data/types_test.cpp rename to tests/unit/data/types_test.cpp diff --git a/tests/src/data/writer_test.cpp b/tests/unit/data/writer_test.cpp similarity index 100% rename from tests/src/data/writer_test.cpp rename to tests/unit/data/writer_test.cpp diff --git a/tests/src/event/base/death_test.cpp b/tests/unit/event/base/death_test.cpp similarity index 100% rename from tests/src/event/base/death_test.cpp rename to tests/unit/event/base/death_test.cpp diff --git a/tests/src/event/base/event_factory_test.cpp b/tests/unit/event/base/event_factory_test.cpp similarity index 100% rename from tests/src/event/base/event_factory_test.cpp rename to tests/unit/event/base/event_factory_test.cpp diff --git a/tests/src/event/behavior/behavior_changes_test.cpp b/tests/unit/event/behavior/behavior_changes_test.cpp similarity index 100% rename from tests/src/event/behavior/behavior_changes_test.cpp rename to tests/unit/event/behavior/behavior_changes_test.cpp diff --git a/tests/src/event/behavior/moud_test.cpp b/tests/unit/event/behavior/moud_test.cpp similarity index 100% rename from tests/src/event/behavior/moud_test.cpp rename to tests/unit/event/behavior/moud_test.cpp diff --git a/tests/src/event/behavior/overdose_test.cpp b/tests/unit/event/behavior/overdose_test.cpp similarity index 100% rename from tests/src/event/behavior/overdose_test.cpp rename to tests/unit/event/behavior/overdose_test.cpp diff --git a/tests/src/event/behavior/pregnancy_test.cpp b/tests/unit/event/behavior/pregnancy_test.cpp similarity index 100% rename from tests/src/event/behavior/pregnancy_test.cpp rename to tests/unit/event/behavior/pregnancy_test.cpp diff --git a/tests/src/event/fibrosis/progression_test.cpp b/tests/unit/event/fibrosis/progression_test.cpp similarity index 100% rename from tests/src/event/fibrosis/progression_test.cpp rename to tests/unit/event/fibrosis/progression_test.cpp diff --git a/tests/src/event/fibrosis/staging_test.cpp b/tests/unit/event/fibrosis/staging_test.cpp similarity index 100% rename from tests/src/event/fibrosis/staging_test.cpp rename to tests/unit/event/fibrosis/staging_test.cpp diff --git a/tests/src/event/hcv/clearance_test.cpp b/tests/unit/event/hcv/clearance_test.cpp similarity index 100% rename from tests/src/event/hcv/clearance_test.cpp rename to tests/unit/event/hcv/clearance_test.cpp diff --git a/tests/src/event/hcv/infection_test.cpp b/tests/unit/event/hcv/infection_test.cpp similarity index 100% rename from tests/src/event/hcv/infection_test.cpp rename to tests/unit/event/hcv/infection_test.cpp diff --git a/tests/src/event/hcv/linking_test.cpp b/tests/unit/event/hcv/linking_test.cpp similarity index 100% rename from tests/src/event/hcv/linking_test.cpp rename to tests/unit/event/hcv/linking_test.cpp diff --git a/tests/src/event/hcv/screening_test.cpp b/tests/unit/event/hcv/screening_test.cpp similarity index 100% rename from tests/src/event/hcv/screening_test.cpp rename to tests/unit/event/hcv/screening_test.cpp diff --git a/tests/src/event/hcv/treatment_test.cpp b/tests/unit/event/hcv/treatment_test.cpp similarity index 100% rename from tests/src/event/hcv/treatment_test.cpp rename to tests/unit/event/hcv/treatment_test.cpp diff --git a/tests/src/event/hcv/voluntary_relink_test.cpp b/tests/unit/event/hcv/voluntary_relink_test.cpp similarity index 100% rename from tests/src/event/hcv/voluntary_relink_test.cpp rename to tests/unit/event/hcv/voluntary_relink_test.cpp diff --git a/tests/src/event/hiv/infection_test.cpp b/tests/unit/event/hiv/infection_test.cpp similarity index 100% rename from tests/src/event/hiv/infection_test.cpp rename to tests/unit/event/hiv/infection_test.cpp diff --git a/tests/src/event/hiv/linking_test.cpp b/tests/unit/event/hiv/linking_test.cpp similarity index 100% rename from tests/src/event/hiv/linking_test.cpp rename to tests/unit/event/hiv/linking_test.cpp diff --git a/tests/src/event/hiv/screening_test.cpp b/tests/unit/event/hiv/screening_test.cpp similarity index 100% rename from tests/src/event/hiv/screening_test.cpp rename to tests/unit/event/hiv/screening_test.cpp diff --git a/tests/src/event/hiv/treatment_test.cpp b/tests/unit/event/hiv/treatment_test.cpp similarity index 100% rename from tests/src/event/hiv/treatment_test.cpp rename to tests/unit/event/hiv/treatment_test.cpp diff --git a/tests/src/model/costing_test.cpp b/tests/unit/model/costing_test.cpp similarity index 100% rename from tests/src/model/costing_test.cpp rename to tests/unit/model/costing_test.cpp diff --git a/tests/src/model/person_test.cpp b/tests/unit/model/person_test.cpp similarity index 95% rename from tests/src/model/person_test.cpp rename to tests/unit/model/person_test.cpp index 738355149..64da070dd 100644 --- a/tests/src/model/person_test.cpp +++ b/tests/unit/model/person_test.cpp @@ -4,7 +4,7 @@ // Created Date: 2025-05-09 // // Author: Matthew Carroll // // ----- // -// Last Modified: 2026-03-20 // +// Last Modified: 2026-06-23 // // Modified By: Matthew Carroll // // ----- // // Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // @@ -14,10 +14,14 @@ #include // STL Libraries +#include #include +#include +#include // 3rd Party Dependencies #include +#include // Library Includes #include @@ -43,7 +47,23 @@ class PersonTest : public ::testing::Test { } static void TearDownTestSuite() { - std::filesystem::remove("PersonTest.log"); + const std::string log_name = "PersonTest"; + const std::string log_file = log_name + ".log"; + + // Release any open file handles held by the logger before deleting. + if (auto logger = spdlog::get(log_name); logger) { + logger->flush(); + spdlog::drop(log_name); + } + + std::error_code ec; + for (int attempt = 0; attempt < 10; ++attempt) { + std::filesystem::remove(log_file, ec); + if (!ec || !std::filesystem::exists(log_file)) { + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } } // Creating the Person object and growing to set the Current Time to 3 diff --git a/tests/src/model/sampler_test.cpp b/tests/unit/model/sampler_test.cpp similarity index 100% rename from tests/src/model/sampler_test.cpp rename to tests/unit/model/sampler_test.cpp diff --git a/tests/src/model/simulation_test.cpp b/tests/unit/model/simulation_test.cpp similarity index 100% rename from tests/src/model/simulation_test.cpp rename to tests/unit/model/simulation_test.cpp diff --git a/tests/src/model/utility_test.cpp b/tests/unit/model/utility_test.cpp similarity index 100% rename from tests/src/model/utility_test.cpp rename to tests/unit/model/utility_test.cpp diff --git a/tests/unit/utils/logging_test.cpp b/tests/unit/utils/logging_test.cpp new file mode 100644 index 000000000..78aa1922c --- /dev/null +++ b/tests/unit/utils/logging_test.cpp @@ -0,0 +1,481 @@ +//////////////////////////////////////////////////////////////////////////////// +// File: logging_test.cpp // +// Project: hep-ce // +// Created Date: 2025-05-01 // +// Author: Matthew Carroll // +// ----- // +// Last Modified: 2026-06-22 // +// Modified By: Matthew Carroll // +// ----- // +// Copyright (c) 2025-2026 Syndemics Lab at Boston Medical Center // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace hepce::utils::testing { + +class LoggingTest : public ::testing::Test { +protected: + void SetUp() override { + // Clear any existing loggers from previous tests + spdlog::drop_all(); + + // Create temporary log files for testing + test_log_file_ = "/tmp/hepce_test.log"; + shared_log_file_ = "/tmp/hepce_shared.log"; + + // Remove test files if they exist + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + void TearDown() override { + // Clean up loggers + spdlog::drop_all(); + + // Remove test files + std::remove(test_log_file_.c_str()); + std::remove(shared_log_file_.c_str()); + } + + std::string test_log_file_; + std::string shared_log_file_; + + // Helper to check if file contains a string + bool FileContains(const std::string &filepath, const std::string &search) { + std::ifstream file(filepath); + if (!file.is_open()) + return false; + + std::string line; + while (std::getline(file, line)) { + if (line.find(search) != std::string::npos) { + return true; + } + } + return false; + } + + // Helper to count lines in file + int CountLinesInFile(const std::string &filepath) { + std::ifstream file(filepath); + if (!file.is_open()) + return 0; + + int count = 0; + std::string line; + while (std::getline(file, line)) { + count++; + } + return count; + } +}; + +// ============================================================================ +// Test: CreateFileLogger Basic Functionality +// ============================================================================ + +TEST_F(LoggingTest, CreateFileLoggerSuccess) { + CreationStatus status = CreateFileLogger("test_logger", test_log_file_); + EXPECT_EQ(status, CreationStatus::kSuccess); + + // Verify logger exists + auto logger = spdlog::get("test_logger"); + EXPECT_NE(logger, nullptr); +} + +TEST_F(LoggingTest, CreateFileLoggerAlreadyExists) { + CreateFileLogger("test_logger", test_log_file_); + + // Try creating again with same name + CreationStatus status = CreateFileLogger("test_logger", test_log_file_); + EXPECT_EQ(status, CreationStatus::kExists); +} + +TEST_F(LoggingTest, CreateMultipleFileLoggers) { + CreationStatus status1 = CreateFileLogger("logger1", test_log_file_); + CreationStatus status2 = CreateFileLogger("logger2", test_log_file_); + + EXPECT_EQ(status1, CreationStatus::kSuccess); + EXPECT_EQ(status2, CreationStatus::kSuccess); + + EXPECT_NE(spdlog::get("logger1"), nullptr); + EXPECT_NE(spdlog::get("logger2"), nullptr); +} + +// ============================================================================ +// Test: Shared File Sink Functionality +// ============================================================================ + +TEST_F(LoggingTest, CreateSharedFileSink) { + CreationStatus status = CreateSharedFileSink(shared_log_file_); + EXPECT_EQ(status, CreationStatus::kSuccess); +} + +TEST_F(LoggingTest, CreateSharedFileSinkCaching) { + // Create sink first time + CreationStatus status1 = CreateSharedFileSink(shared_log_file_); + EXPECT_EQ(status1, CreationStatus::kSuccess); + + // Create sink again with same path (should reuse cached) + CreationStatus status2 = CreateSharedFileSink(shared_log_file_); + EXPECT_EQ(status2, CreationStatus::kSuccess); +} + +TEST_F(LoggingTest, CreateSharedLogger) { + CreateSharedFileSink(shared_log_file_); + + CreationStatus status = CreateSharedLogger("shared_logger"); + EXPECT_EQ(status, CreationStatus::kSuccess); + + auto logger = spdlog::get("shared_logger"); + EXPECT_NE(logger, nullptr); +} + +TEST_F(LoggingTest, MultipleSharedLoggersToSameSink) { + CreateSharedFileSink(shared_log_file_); + + CreationStatus status1 = CreateSharedLogger("shared_logger_1"); + CreationStatus status2 = CreateSharedLogger("shared_logger_2"); + + EXPECT_EQ(status1, CreationStatus::kSuccess); + EXPECT_EQ(status2, CreationStatus::kSuccess); + + // Both loggers should exist + EXPECT_NE(spdlog::get("shared_logger_1"), nullptr); + EXPECT_NE(spdlog::get("shared_logger_2"), nullptr); +} + +// ============================================================================ +// Test: Log Pattern Configuration +// ============================================================================ + +TEST_F(LoggingTest, SetAndGetLogPattern) { + SetLogPattern(LogPattern::kDetailed); + LogPattern pattern = GetLogPattern(); + EXPECT_EQ(pattern, LogPattern::kDetailed); +} + +TEST_F(LoggingTest, LogPatternAffectsNewLoggers) { + SetLogPattern(LogPattern::kSimple); + CreateFileLogger("pattern_logger", test_log_file_); + + LogInfo("pattern_logger", "Test message"); + FlushAllLoggers(); + + // File should exist and contain the message + EXPECT_TRUE(FileContains(test_log_file_, "Test message")); +} + +TEST_F(LoggingTest, ChangeLogPatternMultipleTimes) { + SetLogPattern(LogPattern::kSimple); + EXPECT_EQ(GetLogPattern(), LogPattern::kSimple); + + SetLogPattern(LogPattern::kStandard); + EXPECT_EQ(GetLogPattern(), LogPattern::kStandard); + + SetLogPattern(LogPattern::kDetailed); + EXPECT_EQ(GetLogPattern(), LogPattern::kDetailed); + + SetLogPattern(LogPattern::kThreadSafe); + EXPECT_EQ(GetLogPattern(), LogPattern::kThreadSafe); +} + +// ============================================================================ +// Test: Flush Interval Configuration +// ============================================================================ + +TEST_F(LoggingTest, SetFlushInterval) { + // Should not throw + SetFlushInterval(0); + SetFlushInterval(1); + SetFlushInterval(5); +} + +TEST_F(LoggingTest, SetFlushIntervalZeroForImmediateFlush) { + SetFlushInterval(0); + CreateFileLogger("flush_logger", test_log_file_); + + LogInfo("flush_logger", "Immediate flush"); + + // Should be written immediately + EXPECT_TRUE(FileContains(test_log_file_, "Immediate flush")); +} + +// ============================================================================ +// Test: Logging Functions +// ============================================================================ + +TEST_F(LoggingTest, LogInfo) { + CreateFileLogger("test_logger", test_log_file_); + + LogInfo("test_logger", "Info message"); + FlushAllLoggers(); + + EXPECT_TRUE(FileContains(test_log_file_, "Info message")); +} + +TEST_F(LoggingTest, LogWarning) { + CreateFileLogger("test_logger", test_log_file_); + + LogWarning("test_logger", "Warning message"); + FlushAllLoggers(); + + EXPECT_TRUE(FileContains(test_log_file_, "Warning message")); +} + +TEST_F(LoggingTest, LogError) { + CreateFileLogger("test_logger", test_log_file_); + + LogError("test_logger", "Error message"); + FlushAllLoggers(); + + EXPECT_TRUE(FileContains(test_log_file_, "Error message")); +} + +TEST_F(LoggingTest, LogDebug) { + CreateFileLogger("test_logger", test_log_file_); + SetFlushInterval(0); // Immediate flush + + LogDebug("test_logger", "Debug message"); + FlushAllLoggers(); + + // Verify logger exists and can log + EXPECT_EQ(CheckLoggerExists("test_logger"), CreationStatus::kExists); +} + +TEST_F(LoggingTest, MultipleLogMessages) { + CreateFileLogger("test_logger", test_log_file_); + SetFlushInterval(0); // Immediate flush + + // These should all complete without error + LogInfo("test_logger", "Message 1"); + LogWarning("test_logger", "Message 2"); + LogError("test_logger", "Message 3"); + LogDebug("test_logger", "Message 4"); + FlushAllLoggers(); + + // Verify logger exists + EXPECT_EQ(CheckLoggerExists("test_logger"), CreationStatus::kExists); +} + +// ============================================================================ +// Test: Logger Utility Functions +// ============================================================================ + +TEST_F(LoggingTest, CheckLoggerExistsTrue) { + CreateFileLogger("test_logger", test_log_file_); + + CreationStatus status = CheckLoggerExists("test_logger"); + EXPECT_EQ(status, CreationStatus::kExists); +} + +TEST_F(LoggingTest, CheckLoggerExistsFalse) { + CreationStatus status = CheckLoggerExists("nonexistent_logger"); + EXPECT_EQ(status, CreationStatus::kNotCreated); +} + +TEST_F(LoggingTest, GetLoggerInfo) { + CreateFileLogger("test_logger", test_log_file_); + + std::string info = GetLoggerInfo("test_logger"); + EXPECT_NE(info, ""); + EXPECT_NE(info.find("test_logger"), std::string::npos); +} + +TEST_F(LoggingTest, GetLoggerInfoNonexistent) { + std::string info = GetLoggerInfo("nonexistent_logger"); + EXPECT_NE(info.find("not found"), std::string::npos); +} + +TEST_F(LoggingTest, SetLoggerLevel) { + CreateFileLogger("test_logger", test_log_file_); + + // Should not throw + SetLoggerLevel("test_logger", 0); // trace + SetLoggerLevel("test_logger", 1); // debug + SetLoggerLevel("test_logger", 2); // info + SetLoggerLevel("test_logger", 3); // warn + SetLoggerLevel("test_logger", 4); // error + SetLoggerLevel("test_logger", 5); // critical +} + +// ============================================================================ +// Test: Flush All Loggers +// ============================================================================ + +TEST_F(LoggingTest, FlushAllLoggers) { + CreateFileLogger("logger1", test_log_file_); + CreateFileLogger("logger2", test_log_file_); + + LogInfo("logger1", "Message 1"); + LogInfo("logger2", "Message 2"); + + // Should not throw + FlushAllLoggers(); + + EXPECT_TRUE(FileContains(test_log_file_, "Message 1")); + EXPECT_TRUE(FileContains(test_log_file_, "Message 2")); +} + +TEST_F(LoggingTest, ParallelSharedLoggingToSameFile) { + CreationStatus sink_status = CreateSharedFileSink(shared_log_file_); + EXPECT_EQ(sink_status, CreationStatus::kSuccess); + + const int num_threads = 4; + const int messages_per_thread = 5; + + auto thread_func = [this](int thread_id) { + std::string logger_name = "shared_logger_" + std::to_string(thread_id); + CreationStatus status = CreateSharedLogger(logger_name); + // Status could be kSuccess or kExists if concurrent threads create same + // logger + EXPECT_NE(status, CreationStatus::kError); + + for (int i = 0; i < messages_per_thread; ++i) { + std::string message = "Thread " + std::to_string(thread_id) + + " Message " + std::to_string(i); + LogInfo(logger_name, message); + } + }; + + std::vector threads; + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back(thread_func, i); + } + + for (auto &t : threads) { + t.join(); + } + + FlushAllLoggers(); + + // Verify at least one shared logger exists + EXPECT_EQ(CheckLoggerExists("shared_logger_0"), CreationStatus::kExists); +} + +TEST_F(LoggingTest, ConcurrentCreateSharedLogger) { + CreateSharedFileSink(shared_log_file_); + + const int num_threads = 10; + + auto thread_func = [this](int thread_id) { + std::string logger_name = + "concurrent_logger_" + std::to_string(thread_id); + CreationStatus status = CreateSharedLogger(logger_name); + EXPECT_EQ(status, CreationStatus::kSuccess); + }; + + std::vector threads; + for (int i = 0; i < num_threads; ++i) { + threads.emplace_back(thread_func, i); + } + + for (auto &t : threads) { + t.join(); + } + + // All loggers should be created + for (int i = 0; i < num_threads; ++i) { + std::string logger_name = "concurrent_logger_" + std::to_string(i); + EXPECT_NE(spdlog::get(logger_name), nullptr); + } +} + +// ============================================================================ +// Test: Integration - Full Workflow +// ============================================================================ + +TEST_F(LoggingTest, FullWorkflowFileLogger) { + // Create logger + CreationStatus create_status = + CreateFileLogger("full_test", test_log_file_); + EXPECT_EQ(create_status, CreationStatus::kSuccess); + + // Set pattern + SetLogPattern(LogPattern::kDetailed); + EXPECT_EQ(GetLogPattern(), LogPattern::kDetailed); + + // Log messages + LogInfo("full_test", "Starting full workflow test"); + LogWarning("full_test", "This is a warning"); + LogError("full_test", "This is an error"); + LogDebug("full_test", "Debug information"); + + // Check logger exists + EXPECT_EQ(CheckLoggerExists("full_test"), CreationStatus::kExists); + + // Get info + std::string info = GetLoggerInfo("full_test"); + EXPECT_NE(info, ""); + + // Set level + SetLoggerLevel("full_test", 2); // info level + + // Flush + FlushAllLoggers(); + + // Verify output file + EXPECT_TRUE(FileContains(test_log_file_, "Starting full workflow test")); + EXPECT_TRUE(FileContains(test_log_file_, "This is a warning")); + EXPECT_TRUE(FileContains(test_log_file_, "This is an error")); +} + +TEST_F(LoggingTest, FullWorkflowSharedLogger) { + // Setup shared logging + CreationStatus sink_status = CreateSharedFileSink(shared_log_file_); + EXPECT_EQ(sink_status, CreationStatus::kSuccess); + + SetLogPattern(LogPattern::kThreadSafe); + EXPECT_EQ(GetLogPattern(), LogPattern::kThreadSafe); + + // Create shared loggers + CreationStatus status1 = CreateSharedLogger("shared_1"); + CreationStatus status2 = CreateSharedLogger("shared_2"); + + EXPECT_EQ(status1, CreationStatus::kSuccess); + EXPECT_EQ(status2, CreationStatus::kSuccess); + + // Log from each + LogInfo("shared_1", "Message from logger 1"); + LogInfo("shared_2", "Message from logger 2"); + + // Verify loggers exist + EXPECT_EQ(CheckLoggerExists("shared_1"), CreationStatus::kExists); + EXPECT_EQ(CheckLoggerExists("shared_2"), CreationStatus::kExists); + + FlushAllLoggers(); +} + +TEST_F(LoggingTest, MixedFileAndSharedLoggers) { + // Create file logger + CreationStatus file_status = + CreateFileLogger("file_logger", test_log_file_); + + // Create shared logger + CreationStatus sink_status = CreateSharedFileSink(shared_log_file_); + CreationStatus logger_status = CreateSharedLogger("shared_logger"); + + EXPECT_EQ(file_status, CreationStatus::kSuccess); + EXPECT_EQ(sink_status, CreationStatus::kSuccess); + EXPECT_EQ(logger_status, CreationStatus::kSuccess); + + // Log to both + LogInfo("file_logger", "File logger message"); + LogInfo("shared_logger", "Shared logger message"); + + // Verify both exist + EXPECT_EQ(CheckLoggerExists("file_logger"), CreationStatus::kExists); + EXPECT_EQ(CheckLoggerExists("shared_logger"), CreationStatus::kExists); + + FlushAllLoggers(); +} +} // namespace hepce::utils::testing