From 53fc943129776b27dfee42ce0aa7894e6b9f5801 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Wed, 19 Feb 2025 14:50:44 -0500 Subject: [PATCH 1/3] build(yaml-cpp): added yaml-cpp as a dependency config files will be written in yaml, added a well tested yaml parser to 4DSSE --- .gitignore | 1 + build-config/meson.build | 5 ++++- build-config/mfem/meson.build | 1 - build-config/yaml-cpp/meson.build | 5 +++++ subprojects/yaml-cpp.wrap | 5 +++++ 5 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 build-config/yaml-cpp/meson.build create mode 100644 subprojects/yaml-cpp.wrap diff --git a/.gitignore b/.gitignore index 3d33190..9e57e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,5 +60,6 @@ tags subprojects/mfem/ subprojects/tetgen/ +subprojects/yaml-cpp/ .vscode/ diff --git a/build-config/meson.build b/build-config/meson.build index cefc100..894cf67 100644 --- a/build-config/meson.build +++ b/build-config/meson.build @@ -1 +1,4 @@ -subdir('mfem') \ No newline at end of file +cmake = import('cmake') + +subdir('mfem') +subdir('yaml-cpp') \ No newline at end of file diff --git a/build-config/mfem/meson.build b/build-config/mfem/meson.build index 84f69c8..7152d04 100644 --- a/build-config/mfem/meson.build +++ b/build-config/mfem/meson.build @@ -1,4 +1,3 @@ -cmake = import('cmake') patchFile = files('disable_mfem_selfcheck.patch') patch_check = run_command('grep', '-q', 'MFEM_CHECK_TARGET_NAME', 'subprojects/mfem/CMakeLists.txt', check: false) diff --git a/build-config/yaml-cpp/meson.build b/build-config/yaml-cpp/meson.build new file mode 100644 index 0000000..434a233 --- /dev/null +++ b/build-config/yaml-cpp/meson.build @@ -0,0 +1,5 @@ +yaml_cpp_sp = cmake.subproject( + 'yaml-cpp' +) +yaml_cpp_dep = yaml_cpp_sp.dependency('yaml-cpp') +add_project_arguments('-I' + meson.current_build_dir() + '/subprojects/yaml-cpp/__CMake_build', language: 'cpp') \ No newline at end of file diff --git a/subprojects/yaml-cpp.wrap b/subprojects/yaml-cpp.wrap new file mode 100644 index 0000000..df0133d --- /dev/null +++ b/subprojects/yaml-cpp.wrap @@ -0,0 +1,5 @@ +[wrap-git] +url = https://github.com/jbeder/yaml-cpp.git +revision = yaml-cpp-0.7.0 + +[cmake] \ No newline at end of file From a134878e6773b8dc31cdc101f2cd83a16581257a Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Wed, 19 Feb 2025 16:11:55 -0500 Subject: [PATCH 2/3] feat(config): config class added At many points in the code we may want configurable options, the Config class usses a yaml file to make this easy. It also allows for namespace references "opac:lowtemp:file" etc... --- src/config/meson.build | 26 ++++++++++ src/config/private/config.cpp | 31 ++++++++++++ src/config/public/config.h | 91 +++++++++++++++++++++++++++++++++++ src/meson.build | 3 +- 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 src/config/meson.build create mode 100644 src/config/private/config.cpp create mode 100644 src/config/public/config.h diff --git a/src/config/meson.build b/src/config/meson.build new file mode 100644 index 0000000..289b086 --- /dev/null +++ b/src/config/meson.build @@ -0,0 +1,26 @@ +# Define the library +config_sources = files( + 'private/config.cpp', +) + +config_headers = files( + 'public/config.h' +) + +# Define the libconfig library so it can be linked against by other parts of the build system +libconfig = static_library('config', + config_sources, + include_directories: include_directories('public'), + cpp_args: ['-fvisibility=default'], + dependencies: [yaml_cpp_dep], + install : true) + +config_dep = declare_dependency( + include_directories: include_directories('public'), + link_with: libconfig, + sources: config_sources, + dependencies: [yaml_cpp_dep], +) + +# Make headers accessible +install_headers(config_headers, subdir : '4DSSE/config') \ No newline at end of file diff --git a/src/config/private/config.cpp b/src/config/private/config.cpp new file mode 100644 index 0000000..45fcf27 --- /dev/null +++ b/src/config/private/config.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include + +#include "yaml-cpp/yaml.h" + +#include "config.h" + +Config::Config() {} + +Config::~Config() {} + +Config& Config::getInstance() { + static Config instance; + return instance; +} + +bool Config::loadConfig(const std::string& configFile) { + configFilePath = configFile; + try { + yamlRoot = YAML::LoadFile(configFile); + } catch (YAML::BadFile& e) { + std::cerr << "Error: " << e.what() << std::endl; + return false; + } + return true; +} + diff --git a/src/config/public/config.h b/src/config/public/config.h new file mode 100644 index 0000000..c79e841 --- /dev/null +++ b/src/config/public/config.h @@ -0,0 +1,91 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include +#include +#include +#include + +#include "yaml-cpp/yaml.h" + +/** + * @class Config + * @brief Singleton class to manage configuration settings loaded from a YAML file. + */ +class Config { + private: + /** + * @brief Private constructor to prevent instantiation. + */ + Config(); + + /** + * @brief Destructor. + */ + ~Config(); + + YAML::Node yamlRoot; ///< Root node of the YAML configuration. + std::string configFilePath; ///< Path to the configuration file. + + public: + /** + * @brief Get the singleton instance of the Config class. + * @return Reference to the Config instance. + */ + static Config& getInstance(); + + Config (const Config&) = delete; + Config& operator= (const Config&) = delete; + Config (Config&&) = delete; + Config& operator= (Config&&) = delete; + + /** + * @brief Load configuration from a YAML file. + * @param configFilePath Path to the YAML configuration file. + * @return True if the configuration was loaded successfully, false otherwise. + */ + bool loadConfig(const std::string& configFilePath); + + /** + * @brief Get the input table from the configuration. + * @return Input table as a string. + */ + std::string getInputTable() const; + + /** + * @brief Get a configuration value by key. + * @tparam T Type of the value to retrieve. + * @param key Key of the configuration value. + * @param defaultValue Default value to return if the key does not exist. + * @return Configuration value of type T. + * + * @example + * @code + * Config& config = Config::getInstance(); + * config.loadConfig("example.yaml"); + * int maxIter = config.get("opac:lowTemp:numeric:maxIter", 10); + */ + template + T get(const std::string &key, T defaultValue) { + YAML::Node node = YAML::Clone(yamlRoot); + std::istringstream keyStream(key); + std::string subKey; + while (std::getline(keyStream, subKey, ':')) { + if (!node[subKey]) { + return defaultValue; + } + node = node[subKey]; // go deeper + } + + try { + return node.as(); + } catch (const YAML::Exception& e) { + return defaultValue; // return default value if the key does not exist + } + } + +}; + +#endif \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 9ae232d..bcad636 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,4 +5,5 @@ subdir('resources') subdir('dobj') subdir('const') subdir('opatIO') -subdir('meshIO') \ No newline at end of file +subdir('meshIO') +subdir('config') \ No newline at end of file From 8bf62b68d068be3565dbc173b8bb98ae5f81cd5e Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Wed, 19 Feb 2025 16:12:23 -0500 Subject: [PATCH 3/3] test(tests/config): config tests added --- tests/config/configTest.cpp | 60 +++++++++++++++++++++++++++++++++++++ tests/config/example.yaml | 32 ++++++++++++++++++++ tests/config/meson.build | 25 ++++++++++++++++ tests/meson.build | 1 + 4 files changed, 118 insertions(+) create mode 100644 tests/config/configTest.cpp create mode 100644 tests/config/example.yaml create mode 100644 tests/config/meson.build diff --git a/tests/config/configTest.cpp b/tests/config/configTest.cpp new file mode 100644 index 0000000..817ead7 --- /dev/null +++ b/tests/config/configTest.cpp @@ -0,0 +1,60 @@ +#include +#include "config.h" +#include +#include +#include +#include +#include + +std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.yaml"; +/** + * @file configTest.cpp + * @brief Unit tests for the Config class. + */ + +/** + * @brief Test suite for the Config class. + */ +class configTest : public ::testing::Test {}; + +/** + * @brief Test the constructor of the Config class. + */ +TEST_F(configTest, constructor) { + EXPECT_NO_THROW(Config::getInstance()); +} + +TEST_F(configTest, loadConfig) { + Config& config = Config::getInstance(); + EXPECT_TRUE(config.loadConfig(EXAMPLE_FILENAME)); +} + +TEST_F(configTest, singletonTest) { + Config& config1 = Config::getInstance(); + Config& config2 = Config::getInstance(); + EXPECT_EQ(&config1, &config2); +} + +TEST_F(configTest, getTest) { + Config& config = Config::getInstance(); + config.loadConfig(EXAMPLE_FILENAME); + int maxIter = config.get("opac:lowTemp:numeric:maxIter", 10); + EXPECT_EQ(maxIter, 100); + EXPECT_NE(maxIter, 10); + + std::string logLevel = config.get("logLevel", "DEBUG"); + EXPECT_EQ(logLevel, "INFO"); + EXPECT_NE(logLevel, "DEBUG"); + + float polytropicIndex = config.get("poly:physics:index", 2); + EXPECT_EQ(polytropicIndex, 1.5); + EXPECT_NE(polytropicIndex, 2); + + float polytropicIndex2 = config.get("poly:physics:index2", 2.0); + EXPECT_EQ(polytropicIndex2, 2.0); +} + +TEST_F(configTest, secondSingleton) { + Config& config = Config::getInstance(); + EXPECT_EQ(config.get("opac:lowTemp:numeric:maxIter", 10), 100); +} diff --git a/tests/config/example.yaml b/tests/config/example.yaml new file mode 100644 index 0000000..de7fc5f --- /dev/null +++ b/tests/config/example.yaml @@ -0,0 +1,32 @@ +# High level options +logLevel: "INFO" +outputDir: output + +# Module options +poly: + numeric: + newtonTol: 1e-6 + newtonMaxIter: 100 + gmresTol: 1e-6 + gmresMaxIter: 100 + physics: + index: 1.5 + +# Module options +opac: + highTemp: + physics: + table: "/path/to/highTempTable.dat" + numeric: + tol: 1e-6 + maxIter: 100 + lowTemp: + physics: + table: "/path/to/lowTempTable.dat" + numeric: + tol: 1e-6 + maxIter: 100 + +mesh: + structure: + refine: 2 \ No newline at end of file diff --git a/tests/config/meson.build b/tests/config/meson.build new file mode 100644 index 0000000..7ae9ae9 --- /dev/null +++ b/tests/config/meson.build @@ -0,0 +1,25 @@ +# Test files for const +test_sources = [ + 'configTest.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, config_dep], + include_directories: include_directories('../../src/config/public'), + link_with: libconst, # Link the dobj library + install_rpath: '@loader_path/../../src' # Ensure runtime library path resolves correctly + ) + + # Add the executable as a test + test( + exe_name, + test_exe, + env: ['MESON_SOURCE_ROOT=' + meson.project_source_root(), 'MESON_BUILD_ROOT=' + meson.project_build_root()]) +endforeach diff --git a/tests/meson.build b/tests/meson.build index 162daab..502b0d0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -7,6 +7,7 @@ subdir('dobj') subdir('const') subdir('opatIO') subdir('meshIO') +subdir('config') # Subdirectories for sandbox tests subdir('dobj_sandbox')