diff --git a/.gitignore b/.gitignore index 9e57e7f..3aba890 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,8 @@ tags subprojects/mfem/ subprojects/tetgen/ subprojects/yaml-cpp/ +subprojects/quill/ .vscode/ + +*.log diff --git a/build-config/meson.build b/build-config/meson.build index 894cf67..92335cf 100644 --- a/build-config/meson.build +++ b/build-config/meson.build @@ -1,4 +1,5 @@ cmake = import('cmake') subdir('mfem') -subdir('yaml-cpp') \ No newline at end of file +subdir('yaml-cpp') +subdir('quill') \ No newline at end of file diff --git a/build-config/quill/meson.build b/build-config/quill/meson.build new file mode 100644 index 0000000..3c5a89d --- /dev/null +++ b/build-config/quill/meson.build @@ -0,0 +1,5 @@ +quill_sp = cmake.subproject( + 'quill' +) +quill_dep = quill_sp.dependency('quill') +add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/quill/__CMake_build', language: 'cpp') \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index bcad636..25c0edd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,4 +6,5 @@ subdir('dobj') subdir('const') subdir('opatIO') subdir('meshIO') -subdir('config') \ No newline at end of file +subdir('config') +subdir('probe') \ No newline at end of file diff --git a/src/probe/meson.build b/src/probe/meson.build new file mode 100644 index 0000000..59f951e --- /dev/null +++ b/src/probe/meson.build @@ -0,0 +1,22 @@ +# Define the library +probe_sources = files( + 'private/probe.cpp', +) + +probe_headers = files( + 'public/probe.h' +) + +# Define the liblogger library so it can be linked against by other parts of the build system +libprobe = static_library('probe', + probe_sources, + include_directories: include_directories('public'), + cpp_args: ['-fvisibility=default'], + install : true, + dependencies: [config_dep, mfem_dep, quill_dep] + ) + +probe_dep = declare_dependency( + include_directories: include_directories('public'), + link_with: libprobe, +) \ No newline at end of file diff --git a/src/probe/private/probe.cpp b/src/probe/private/probe.cpp new file mode 100644 index 0000000..6d26f66 --- /dev/null +++ b/src/probe/private/probe.cpp @@ -0,0 +1,99 @@ +#include "quill/Backend.h" +#include "quill/Frontend.h" +#include "quill/Logger.h" +#include "quill/sinks/ConsoleSink.h" +#include "quill/sinks/FileSink.h" +#include +#include +#include +#include + +#include "mfem.hpp" + +#include "config.h" +#include "probe.h" + + +namespace Probe { + +void pause() { + std::cout << "Execution paused. Please press enter to continue..." + << std::endl; // Use endl to flush + std::cin.get(); +} + +void wait(int seconds) { + std::this_thread::sleep_for(std::chrono::seconds(seconds)); +} + +void glVisView(mfem::GridFunction& u, mfem::Mesh& mesh, + const std::string& windowTitle) { + Config& config = Config::getInstance(); + if (config.get("Probe:GLVis:Visualization", true)) { + std::string vishost = config.get("Probe:GLVis:Host", "localhost"); + int visport = config.get("Probe:GLVis:Port", 19916); // Changed default port + mfem::socketstream sol_sock(vishost.c_str(), visport); + sol_sock.precision(8); + sol_sock << "solution\n" << mesh << u + << "window_title '" << windowTitle << "'\n" << std::flush; // Added title + } +} + +LogManager::LogManager() { + Config& config = Config::getInstance(); + quill::Backend::start(); + auto CLILogger = quill::Frontend::create_or_get_logger( + "root", + quill::Frontend::create_or_get_sink("sink_id_1")); + + newFileLogger(config.get("Probe:LogManager:DefaultLogName", "4DSSE.log"), "log"); + loggerMap.emplace("stdout", CLILogger); +} + +LogManager::~LogManager() = default; + +quill::Logger* LogManager::getLogger(const std::string& loggerName) { + auto it = loggerMap.find(loggerName); // Find *once* + if (it == loggerMap.end()) { + throw std::runtime_error("Cannot find logger " + loggerName); + } + return it->second; // Return the raw pointer from the shared_ptr +} + +std::vector LogManager::getLoggerNames() { + std::vector loggerNames; + loggerNames.reserve(loggerMap.size()); + for (const auto& pair : loggerMap) { // Use range-based for loop and const auto& + loggerNames.push_back(pair.first); + } + return loggerNames; +} + +std::vector LogManager::getLoggers() { + std::vector loggers; + loggers.reserve(loggerMap.size()); + for (const auto& pair : loggerMap) { + loggers.push_back(pair.second); // Get the raw pointer + } + return loggers; +} + +quill::Logger* LogManager::newFileLogger(const std::string& filename, + const std::string& loggerName) { + auto file_sink = quill::Frontend::create_or_get_sink( + filename, + []() { + quill::FileSinkConfig cfg; + cfg.set_open_mode('w'); + return cfg; + }(), + quill::FileEventNotifier{}); + // Get the raw pointer. + quill::Logger* rawLogger = quill::Frontend::create_or_get_logger(loggerName, std::move(file_sink)); + + // Create a shared_ptr from the raw pointer. + loggerMap.emplace(loggerName, rawLogger); + return rawLogger; +} + +} // namespace Probe \ No newline at end of file diff --git a/src/probe/public/probe.h b/src/probe/public/probe.h new file mode 100644 index 0000000..1ef7e09 --- /dev/null +++ b/src/probe/public/probe.h @@ -0,0 +1,99 @@ +//=== Probe.h === +#ifndef PROBE_H +#define PROBE_H + +#include +#include +#include +#include + +#include "mfem.hpp" +#include "quill/Logger.h" + +/** + * @brief The Probe namespace contains utility functions for debugging and logging. + */ +namespace Probe { + /** + * @brief Pause the execution and wait for user input. + */ + void pause(); + + /** + * @brief Wait for a specified number of seconds. + * @param seconds The number of seconds to wait. + */ + void wait(int seconds); + + /** + * @brief Visualize a solution using GLVis. + * @param u The GridFunction to visualize. + * @param mesh The mesh associated with the GridFunction. + * @param windowTitle The title of the visualization window. + */ + void glVisView(mfem::GridFunction& u, mfem::Mesh& mesh, + const std::string& windowTitle = "solution"); + + /** + * @brief Class to manage logging operations. + */ + class LogManager { + private: + /** + * @brief Private constructor for singleton pattern. + */ + LogManager(); + + /** + * @brief Destructor. + */ + ~LogManager(); + + // Map to store pointers to quill loggers (raw pointers as quill deals with its own memory managment in a seperated, detatched, thread) + std::map loggerMap; + + // Prevent copying and assignment (Rule of Zero) + LogManager(const LogManager&) = delete; + LogManager& operator=(const LogManager&) = delete; + + public: + /** + * @brief Get the singleton instance of LogManager. + * @return The singleton instance of LogManager. + */ + static LogManager& getInstance() { + static LogManager instance; + return instance; + } + + /** + * @brief Get a logger by name. + * @param loggerName The name of the logger. + * @return A pointer to the logger. + */ + quill::Logger* getLogger(const std::string& loggerName); + + /** + * @brief Get the names of all loggers. + * @return A vector of logger names. + */ + std::vector getLoggerNames(); + + /** + * @brief Get all loggers. + * @return A vector of pointers to the loggers. + */ + std::vector getLoggers(); + + /** + * @brief Create a new file logger. + * @param filename The name of the log file. + * @param loggerName The name of the logger. + * @return A pointer to the new logger. + */ + quill::Logger* newFileLogger(const std::string& filename, + const std::string& loggerName); + }; + +} // namespace Probe +#endif \ No newline at end of file diff --git a/subprojects/quill.wrap b/subprojects/quill.wrap new file mode 100644 index 0000000..d857641 --- /dev/null +++ b/subprojects/quill.wrap @@ -0,0 +1,5 @@ +[wrap-git] +url = https://github.com/odygrd/quill +revision = v8.1.1 + +[cmake] \ No newline at end of file diff --git a/tests/meson.build b/tests/meson.build index 502b0d0..a5f55ac 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -8,6 +8,7 @@ subdir('const') subdir('opatIO') subdir('meshIO') subdir('config') +subdir('probe') # Subdirectories for sandbox tests subdir('dobj_sandbox') diff --git a/tests/probe/meson.build b/tests/probe/meson.build new file mode 100644 index 0000000..159b206 --- /dev/null +++ b/tests/probe/meson.build @@ -0,0 +1,20 @@ +# Test files for dobj +test_sources = [ + 'probeTest.cpp', +] + +foreach test_file : test_sources + exe_name = test_file.split('.')[0] + message('Building test: ' + exe_name) + + # Create an executable target for each test + test_exe = executable( + exe_name, + test_file, + dependencies: [gtest_dep, probe_dep, mfem_dep, quill_dep], + install_rpath: '@loader_path/../../src' # Ensure runtime library path resolves correctly + ) + + # Add the executable as a test + test(exe_name, test_exe) +endforeach diff --git a/tests/probe/probeTest.cpp b/tests/probe/probeTest.cpp new file mode 100644 index 0000000..2014488 --- /dev/null +++ b/tests/probe/probeTest.cpp @@ -0,0 +1,88 @@ +#include +#include "probe.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "quill/LogMacros.h" + +std::string getLastLine(const std::string& filename) { + std::ifstream file(filename); + std::string line, lastLine; + + if (!file.is_open()) { + throw std::runtime_error("Could not open file"); + } + + while (std::getline(file, line)) { + lastLine = line; + } + + return lastLine; // Returns the last non-empty line +} + +std::string stripTimestamps(const std::string& logLine) { + std::regex logPattern(R"(\d+:\d+:\d+\.\d+\s+\[\d+\]\s+probeTest\.cpp:\d+\s+LOG_INFO\s+[A-Za-z]*\s+(.*))"); + std::smatch match; + if (std::regex_match(logLine, match, logPattern) && match.size() > 1) { + return match[1].str(); // Extract log message after timestamp + } + return logLine; // Return as-is if pattern doesn't match +} + + +class probeTest : public ::testing::Test {}; + +TEST_F(probeTest, DefaultConstructorTest) { + EXPECT_NO_THROW(Probe::LogManager::getInstance()); +} + +TEST_F(probeTest, waitTest) { + auto start = std::chrono::high_resolution_clock::now(); + Probe::wait(1); + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = end - start; + EXPECT_LE(elapsed.count(), 1.1); +} + +TEST_F(probeTest, getLoggerTest) { + Probe::LogManager& logManager = Probe::LogManager::getInstance(); + std::string loggerName = "log"; + quill::Logger* logger = logManager.getLogger(loggerName); + EXPECT_NE(logger, nullptr); + LOG_INFO(logger, "This is a test message"); + // Wait for the log to be written by calling getLastLine until it is non empty + std::string lastLine; + while (lastLine.empty()) { + lastLine = getLastLine("4DSSE.log"); + } + EXPECT_EQ(stripTimestamps(lastLine), "This is a test message"); +} + +TEST_F(probeTest, newFileLoggerTest) { + Probe::LogManager& logManager = Probe::LogManager::getInstance(); + const std::string loggerName = "newLog"; + const std::string filename = "newLog.log"; + quill::Logger* logger = logManager.newFileLogger(filename, loggerName); + EXPECT_NE(logger, nullptr); + LOG_INFO(logger, "This is a new test message"); + // Wait for the log to be written by calling getLastLine until it is non empty + std::string lastLine; + while (lastLine.empty()) { + lastLine = getLastLine(filename); + } + EXPECT_EQ(stripTimestamps(lastLine), "This is a new test message"); +} + +TEST_F(probeTest, getLoggerNames) { + Probe::LogManager& logManager = Probe::LogManager::getInstance(); + std::vector loggerNames = logManager.getLoggerNames(); + EXPECT_EQ(loggerNames.size(), 3); + EXPECT_EQ(loggerNames.at(0), "log"); + EXPECT_EQ(loggerNames.at(1), "newLog"); + EXPECT_EQ(loggerNames.at(2), "stdout"); +}