diff --git a/.gitignore b/.gitignore index c19769f..8189bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ subprojects/qhull/ subprojects/libconstants/ subprojects/liblogging/ subprojects/packagecache/ +subprojects/glaze/ qhull.wrap diff --git a/build-config/glaze/meson.build b/build-config/glaze/meson.build new file mode 100644 index 0000000..f364fcb --- /dev/null +++ b/build-config/glaze/meson.build @@ -0,0 +1,16 @@ +glaze_cmake_options = cmake.subproject_options() + +glaze_cmake_options.add_cmake_defines({ + 'BUILD_SHARED_LIBS': 'OFF', + 'BUILD_STATIC_LIBS': 'ON', + 'CMAKE_INSTALL_LIBDIR': get_option('libdir'), + 'CMAKE_INSTALL_INCLUDEDIR': get_option('includedir'), + 'CMAKE_POSITION_INDEPENDENT_CODE': 'ON', + 'galze_BUILD_EXAMPLES': 'OFF', + }) + +glaze_sp = cmake.subproject( + 'glaze', + options: glaze_cmake_options, +) +glaze_dep = glaze_sp.dependency('glaze_glaze') diff --git a/build-config/meson.build b/build-config/meson.build index e96dbf5..9f408ea 100644 --- a/build-config/meson.build +++ b/build-config/meson.build @@ -1,2 +1,2 @@ cmake = import('cmake') -subdir('yaml-cpp') +subdir('glaze') diff --git a/build-config/yaml-cpp/meson.build b/build-config/yaml-cpp/meson.build deleted file mode 100644 index 82d1b20..0000000 --- a/build-config/yaml-cpp/meson.build +++ /dev/null @@ -1,42 +0,0 @@ -cmake = import('cmake') -yaml_cpp_cmake_options = cmake.subproject_options() - -yaml_cpp_cmake_options.add_cmake_defines({ - 'BUILD_SHARED_LIBS': 'OFF', - 'BUILD_STATIC_LIBS': 'ON', - 'YAML_CPP_BUILD_TESTS': 'OFF', - 'CMAKE_CXX_FLAGS': '-Wno-shadow', - 'CMAKE_C_FLAGS': '-Wno-shadow', - 'CMAKE_INSTALL_LIBDIR': get_option('libdir'), - 'CMAKE_INSTALL_INCLUDEDIR': get_option('includedir'), - 'CMAKE_POLICY_VERSION_MINIMUM': '3.5', - 'CMAKE_POSITION_INDEPENDENT_CODE': 'ON' - }) - -yaml_cpp_sp = cmake.subproject( - 'yaml-cpp', - options: yaml_cpp_cmake_options, -) - - -yaml_cpp_tgt = yaml_cpp_sp.target('yaml-cpp') -yaml_cpp_inc = yaml_cpp_sp.include_directories('yaml-cpp') -empty_yaml_cpp_file = configure_file( - output: 'yaml_cpp_dummy_ar.cpp', - command: ['echo'], - capture: true - ) -libyaml_static = static_library( - 'yaml_cpp-static', - empty_yaml_cpp_file, - objects: [yaml_cpp_tgt.extract_all_objects(recursive: true)], - include_directories: yaml_cpp_inc, - pic: true, - install: false -) - - -yaml_cpp_dep = declare_dependency( - link_with: libyaml_static, - include_directories: yaml_cpp_inc, -) diff --git a/examples/config_example.toml b/examples/config_example.toml new file mode 100644 index 0000000..258283a --- /dev/null +++ b/examples/config_example.toml @@ -0,0 +1,4 @@ +[main] +x = 10 +y = 20 +person = {age = 30, name = "Alice"} \ No newline at end of file diff --git a/examples/config_example.yml b/examples/config_example.yml deleted file mode 100644 index d8bca95..0000000 --- a/examples/config_example.yml +++ /dev/null @@ -1,4 +0,0 @@ -men: ["John Smith", "Bill Jones"] -women: - - Karol Boudreaux - - Emily Boudreaux diff --git a/examples/simple.cpp b/examples/simple.cpp index e925d10..53abffd 100644 --- a/examples/simple.cpp +++ b/examples/simple.cpp @@ -1,35 +1,46 @@ #include "fourdst/config/config.h" +#include "glaze/glaze.hpp" #include #include -#include -#include -std::string get_validated_filename(int argc, char* argv[]) { - if (argc != 2) { - std::cout << "Usage: " << argv[0] << " " << std::endl; - exit(1); - } +using namespace fourdst::config; - std::filesystem::path const file_path{ argv[1] }; +struct sub { + double x; + double y; +}; - if (not std::filesystem::exists(file_path) || not std::filesystem::is_regular_file(file_path)) { - std::cout << "Error: File does not exist or is not a regular file." << std::endl; - exit(1); - } +struct BoundaryConditions { + double pressure = 1e6; + sub sub; +}; - return file_path.string(); -} +struct ExampleConfig { + double parameterA = 1.0; + int parameterB = 1.0; + std::string parameterC = "default_value"; + std::vector parameterD = {0.1, 0.2, 0.3}; + BoundaryConditions boundaryConditions; +}; -int main(int argc, char* argv[]) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); +struct Person { + int age; + std::string name; +}; - std::string filename = get_validated_filename(argc, argv); - config.loadConfig(filename); +struct AppConfig { + double x; + double y; + Person person; +}; - auto men = config.get>("men", {}); +int main() { + const Config cfg; + cfg.save(); + cfg.save_schema("."); - for (const auto& name : men) { - std::cout << "men are " << name << std::endl; - } - -} + Config loaded; + loaded.save_schema("."); + loaded.load("config_example.toml"); + std::println("{}", loaded); +} \ No newline at end of file diff --git a/examples/wasm.cpp b/examples/wasm.cpp index ecff35e..ba5d840 100644 --- a/examples/wasm.cpp +++ b/examples/wasm.cpp @@ -2,18 +2,22 @@ #include #include #include -#include +struct AppConfig { + double x; + double y; + std::vector men; +}; int main(int argc, char* argv[]) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); + fourdst::config::Config config; + config.load("/input.toml"); - config.loadConfig("/input.yaml"); + auto x = config->x; + const auto& men = config->men; - auto men = config.get>("men", {}); - - for (const auto& name : men) { - std::cout << "men are " << name << std::endl; - } + for (const auto& name : men) { + std::cout << "men are " << name << std::endl; + } } diff --git a/meson.build b/meson.build index 4f6ac74..37606bc 100644 --- a/meson.build +++ b/meson.build @@ -35,16 +35,16 @@ if get_option('build_examples') subdir('examples') endif -if get_option('pkg_config') - message('Generating pkg-config file for libconfig...') - pkg = import('pkgconfig') - pkg.generate( - name: 'libconfig', - description: 'Configuration module for SERiF and related projects', - version: meson.project_version(), - libraries: [libconfig], - subdirs: ['fourdst'], - filebase: 'fourdst_config', - install_dir: join_paths(get_option('libdir'), 'pkgconfig') - ) -endif +#if get_option('pkg_config') +# message('Generating pkg-config file for libconfig...') +# pkg = import('pkgconfig') +# pkg.generate( +# name: 'libconfig', +# description: 'Configuration module for SERiF and related projects', +# version: meson.project_version(), +# libraries: [libconfig], +# subdirs: ['fourdst'], +# filebase: 'fourdst_config', +# install_dir: join_paths(get_option('libdir'), 'pkgconfig') +# ) +#endif diff --git a/src/config/include/fourdst/config/base.h b/src/config/include/fourdst/config/base.h new file mode 100644 index 0000000..d6e034f --- /dev/null +++ b/src/config/include/fourdst/config/base.h @@ -0,0 +1,166 @@ +#pragma once +#include +#include "glaze/toml.hpp" +#include +#include +#include + +#include "fourdst/config/registry.h" +#include "fourdst/config/exceptions/exceptions.h" + +namespace fourdst::config::utils { + inline std::string extract_error_key(const std::string& buffer, const glz::error_ctx& pe) { + if (pe.location >= buffer.size()) return "unknown"; + + size_t line_start = pe.location; + while (line_start > 0 && buffer[line_start - 1] != '\n') { + line_start--; + } + + size_t line_end = pe.location; + while (line_end < buffer.size() && buffer[line_end] != '\n' && buffer[line_end] != '\r') { + line_end++; + } + + std::string line = buffer.substr(line_start, line_end - line_start); + + const size_t separator_pos = line.find_first_of("=:"); + + if (separator_pos != std::string::npos) { + std::string key_part = line.substr(0, separator_pos); + + while (!key_part.empty() && std::isspace(key_part.back())) { + key_part.pop_back(); + } + + size_t first_char = 0; + while (first_char < key_part.size() && std::isspace(key_part[first_char])) { + first_char++; + } + + if (first_char < key_part.size()) { + return key_part.substr(first_char); + } + } + + return line; + } +} + +namespace fourdst::config { + enum class ConfigState { + DEFAULT, + LOADED_FROM_FILE + }; + + template + class Config { + public: + Config() { + (void)m_registrar; + } + + const T* operator->() const { return &m_content.main; } + const T& main() const { return m_content.main; } + + void save(std::optional path = std::nullopt) const { + if (!path) { + path = std::string(glz::name_v) + ".toml"; + } + auto err = glz::write_file_toml(m_content, path.value(), std::string{}); + if (err) { + throw exceptions::ConfigSaveError( + std::format( + "Config::save: Failed to save config to {} with glaze error {} ", + std::string(path.value()), + glz::format_error(err.ec) + ) + ); + } + } + + void load(const std::string_view path) { + std::string buffer; + const auto ec = glz::file_to_buffer(buffer, path); + if (ec != glz::error_code::none) { + throw exceptions::ConfigLoadError( + std::format( + "Config::load: Failed to load config from {} with glaze error {} ", + std::string(path), + glz::format_error(ec) + ) + ); + } + auto err = glz::read< + glz::opts{ + .format = glz::TOML, + .error_on_unknown_keys = true + }>(m_content, buffer); + if (err) { + throw exceptions::ConfigParseError( + std::format( + "Config::load: Failed to parse config from {} with glaze error {} (Key: {}) ", + std::string(path), + glz::format_error(err.ec), + utils::extract_error_key(buffer, err) + ) + ); + } + m_state = ConfigState::LOADED_FROM_FILE; + } + + void save_schema(const std::string_view dir) const { + Registry::generate_named(dir, std::string(glz::name_v)); + } + + ConfigState get_state() const { return m_state; } + + std::string describe_state() const { + switch (m_state) { + case ConfigState::DEFAULT: + return "DEFAULT"; + case ConfigState::LOADED_FROM_FILE: + return "LOADED_FROM_FILE"; + default: + return "UNKNOWN"; + } + } + + private: + struct Content { + T main; + }; + Content m_content; + ConfigState m_state = ConfigState::DEFAULT; + + + struct Registrar { + Registrar() { + const auto name = std::string(glz::name_v); + Registry::register_schema(name); + } + }; + static inline Registrar m_registrar; + }; +} + +template +struct std::formatter, CharT> { + + static constexpr auto parse(auto& ctx) { return ctx.begin(); } + + auto format(const fourdst::config::Config& config, auto& ctx) const { + const T& inner_value = config.main(); + struct Content { + T main; + }; + Content content{inner_value}; + std::string buffer; + const glz::error_ctx ec = glz::write(content, buffer); + if (ec) { + return std::format_to(ctx.out(), "Error serializing config"); + } + + return std::format_to(ctx.out(), "{}", buffer); + } +}; diff --git a/src/config/include/fourdst/config/config.h b/src/config/include/fourdst/config/config.h index 32cdee0..f8f8d0a 100644 --- a/src/config/include/fourdst/config/config.h +++ b/src/config/include/fourdst/config/config.h @@ -20,227 +20,6 @@ // *********************************************************************** */ #pragma once -#include -#include -#include -#include -#include -#include -#include +#include "fourdst/config/base.h" +#include "fourdst/config/exceptions/exceptions.h" -// Required for YAML parsing -#include "yaml-cpp/yaml.h" - -// -- Forward Def of Resource manager to let it act as a friend of Config -- -// Note this is for SERiF development -namespace serif::resource { class ResourceManager; } - -class configTestPrivateAccessor; // Forward declaration for test utility - -namespace fourdst::config { - - /** - * @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. - bool debug = false; ///< Flag to enable debug output. - bool loaded = false; ///< Flag to indicate if the configuration has been loaded. - - std::map configMap; ///< Cache for the location of configuration settings. - std::vector unknownKeys; ///< Cache for the existence of configuration settings. - - /** - * @brief Get a value from the configuration cache. - * @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. - */ - template - T getFromCache(const std::string &key, T defaultValue) { - if (configMap.find(key) != configMap.end()) { - try { - return configMap[key].as(); - } catch (const YAML::Exception& e) { - return defaultValue; - } - } - return defaultValue; - } - - - /** - * @brief Check if a key exists in the configuration cache. - * @param key Key to check. - * @return True if the key exists in the cache, false otherwise. - */ - bool isKeyInCache(const std::string &key); - - /** - * @brief Add a key-value pair to the configuration cache. - * @param key Key of the configuration value. - * @param node YAML node containing the configuration value. - */ - void addToCache(const std::string &key, const YAML::Node &node); - - /** - * @brief Register a key as not found in the configuration. - * @param key Key that was not found. - */ - void registerUnknownKey(const std::string &key); - - bool m_loaded = false; - - // Only friends can access get without a default value - template - T get(const std::string &key) { - if (!m_loaded) { - throw std::runtime_error("Error! Config file not loaded"); - } - if (has(key)) { - return getFromCache(key, T()); - } else { - throw std::runtime_error("Error! Key not found in config 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; - - void setDebug(bool debug) { this->debug = debug; } - - /** - * @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) { - if (!m_loaded) { - // ONLY THROW ERROR IF HARSH OR WARN CONFIGURATION -#if defined(CONFIG_HARSH) - throw std::runtime_error("Error! Config file not loaded. To disable this error, recompile with CONFIG_HARSH=0"); -#elif defined(CONFIG_WARN) - std::cerr << "Warning! Config file not loaded. This instance of 4DSSE was compiled with CONFIG_WARN so the code will continue using only default values" << std::endl; -#endif - } - // --- Check if the key has already been checked for existence - if (std::find(unknownKeys.begin(), unknownKeys.end(), key) != unknownKeys.end()) { - return defaultValue; // If the key has already been added to the unknown cache do not traverse the YAML tree or hit the cache - } - - // --- Check if the key is already in the cache (avoid traversing YAML nodes) - if (isKeyInCache(key)) { - return getFromCache(key, defaultValue); - } - // --- If the key is not in the cache, check the YAML file - else { - YAML::Node node = YAML::Clone(yamlRoot); - std::istringstream keyStream(key); - std::string subKey; - while (std::getline(keyStream, subKey, ':')) { - if (!node[subKey]) { - // Key does not exist - registerUnknownKey(key); - return defaultValue; - } - node = node[subKey]; // go deeper - } - - try { - // Key exists and is of the requested type - addToCache(key, node); - return node.as(); - } catch (const YAML::Exception& e) { - // Key is not of the requested type - registerUnknownKey(key); - return defaultValue; // return default value if the key does not exist - } - } - } - - /** - * @brief Check if the key exists in the given config file - * @param key Key to check; - * @return boolean true or false - */ - bool has(const std::string &key); - - /** - * @brief Get all keys defined in the configuration file. - * @return Vector of all keys in the configuration file. - */ - std::vector keys() const; - - /** - * @brief Print the configuration file path and the YAML root node. - * @param os Output stream. - * @param config Config object to print. - * @return Output stream. - */ - friend std::ostream& operator<<(std::ostream& os, const Config& config) { - if (!config.m_loaded) { - os << "Config file not loaded" << std::endl; - return os; - } - if (!config.debug) { - os << "Config file: " << config.configFilePath << std::endl; - } else{ - // Print entire YAML file from root - os << "Config file: " << config.configFilePath << std::endl; - os << config.yamlRoot << std::endl; - } - return os; - } - - // Setup gTest class as a friend - friend class ::configTestPrivateAccessor; // Friend declaration for global test accessor - // -- Resource Manager is a friend of config so it can create a seperate instance - friend class serif::resource::ResourceManager; // Adjusted friend declaration - }; - -} diff --git a/src/config/include/fourdst/config/exceptions/exceptions.h b/src/config/include/fourdst/config/exceptions/exceptions.h new file mode 100644 index 0000000..f3e4a48 --- /dev/null +++ b/src/config/include/fourdst/config/exceptions/exceptions.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace fourdst::config::exceptions { + class ConfigError : public std::exception { + public: + ConfigError(const std::string & what): m_msg(what) {} + + const char* what() const noexcept override { + return m_msg.c_str(); + } + private: + std::string m_msg; + }; + + class ConfigSaveError final : public ConfigError { + using ConfigError::ConfigError; + }; + + class ConfigLoadError final : public ConfigError { + using ConfigError::ConfigError; + }; + + class ConfigParseError final : public ConfigError { + using ConfigError::ConfigError; + }; + + class SchemaGenerationError final : public ConfigError { + using ConfigError::ConfigError; + }; + + class SchemaNameError final : public ConfigError { + using ConfigError::ConfigError; + }; + +} \ No newline at end of file diff --git a/src/config/include/fourdst/config/registry.h b/src/config/include/fourdst/config/registry.h new file mode 100644 index 0000000..19e165c --- /dev/null +++ b/src/config/include/fourdst/config/registry.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include "fourdst/config/exceptions/exceptions.h" + +namespace fourdst::config { + + struct Registry { + using SchemaWriter = std::function; + + static std::unordered_map& get_writers() { + static std::unordered_map writers; + return writers; + } + + template + static void register_schema(const std::string& name) { + auto& writers = get_writers(); + + writers.insert({name, [name](const std::filesystem::path &dir) { + + auto schema_r = glz::write_json_schema(); + if (!schema_r.has_value()) { + throw std::runtime_error("Failed to generate schema for " + name); + } + std::string schema = schema_r.value(); + + const auto path = dir / (name + ".schema.json"); + const auto err = glz::buffer_to_file(schema, path.string()); + + if (err != glz::error_code::none) { + throw exceptions::SchemaGenerationError("Failed to write schema for " + name + " to " + path.string()); + } + }}); + } + + static void generate_all(const std::filesystem::path& dir) { + std::filesystem::create_directories(dir); + for (const auto &writer: get_writers() | std::views::values) { + writer(dir); + } + } + + static void generate_named(const std::filesystem::path& dir, const std::string& name) { + const auto& writers = get_writers(); + const auto it = writers.find(name); + if (it == writers.end()) { + throw exceptions::SchemaNameError("No schema registered with name: " + name); + } + it->second(dir); + } + }; + +} \ No newline at end of file diff --git a/src/config/meson.build b/src/config/meson.build index 940210f..5fddbf0 100644 --- a/src/config/meson.build +++ b/src/config/meson.build @@ -5,22 +5,23 @@ config_sources = files( # Define the libconfig library so it can be linked against by other parts of the build system -libconfig = library('config', - config_sources, - include_directories: include_directories('include'), - cpp_args: ['-fvisibility=default'], - dependencies: [yaml_cpp_dep], - install : true) +#libconfig = library('config', +# config_sources, +# include_directories: include_directories('include'), +# cpp_args: ['-fvisibility=default'], +# dependencies: [yaml_cpp_dep, glaze_dep], +# install : true) config_dep = declare_dependency( include_directories: include_directories('include'), - link_with: libconfig, - sources: config_sources, - dependencies: [yaml_cpp_dep], +# link_with: libconfig, +# sources: config_sources, + dependencies: [glaze_dep], ) # Make headers accessible config_headers = files( - 'include/fourdst/config/config.h' + 'include/fourdst/config/config.h', + 'include/fourdst/config/registry.h', ) install_headers(config_headers, subdir : 'fourdst/fourdst/config') diff --git a/subprojects/glaze.wrap b/subprojects/glaze.wrap new file mode 100644 index 0000000..1fa2ecf --- /dev/null +++ b/subprojects/glaze.wrap @@ -0,0 +1,5 @@ +[wrap-git] +url = https://github.com/stephenberry/glaze.git +revision = v6.1.0 + +[cmake] \ No newline at end of file diff --git a/subprojects/packagefiles/yaml-cpp/addCSTDINTToEmitterUtils.patch b/subprojects/packagefiles/yaml-cpp/addCSTDINTToEmitterUtils.patch deleted file mode 100644 index c4a8224..0000000 --- a/subprojects/packagefiles/yaml-cpp/addCSTDINTToEmitterUtils.patch +++ /dev/null @@ -1,9 +0,0 @@ ---- yaml-cpp/src/emitterutils.cpp.bak 2025-07-29 08:42:40 -+++ yaml-cpp/src/emitterutils.cpp 2025-07-29 08:42:50 -@@ -1,5 +1,6 @@ - #include - #include -+#include - #include - - #include "emitterutils.h" diff --git a/subprojects/packagefiles/yaml-cpp/disableShadowWarnings.patch b/subprojects/packagefiles/yaml-cpp/disableShadowWarnings.patch deleted file mode 100644 index 174af7f..0000000 --- a/subprojects/packagefiles/yaml-cpp/disableShadowWarnings.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- yaml-cpp/CMakeLists.txt.orig 2025-07-24 08:18:01 -+++ yaml-cpp/CMakeLists.txt 2025-07-24 08:18:25 -@@ -93,7 +93,6 @@ - - target_compile_options(yaml-cpp - PRIVATE -- $<${not-msvc}:-Wall -Wextra -Wshadow -Weffc++ -Wno-long-long> - $<${not-msvc}:-pedantic -pedantic-errors> - - $<$:-MTd> diff --git a/subprojects/yaml-cpp.wrap b/subprojects/yaml-cpp.wrap deleted file mode 100644 index 77469d6..0000000 --- a/subprojects/yaml-cpp.wrap +++ /dev/null @@ -1,6 +0,0 @@ -[wrap-git] -url = https://github.com/jbeder/yaml-cpp.git -revision = yaml-cpp-0.7.0 -diff_files = yaml-cpp/disableShadowWarnings.patch, yaml-cpp/addCSTDINTToEmitterUtils.patch - -[cmake] diff --git a/tests/config/configTest.cpp b/tests/config/configTest.cpp index 62add52..2ba10be 100644 --- a/tests/config/configTest.cpp +++ b/tests/config/configTest.cpp @@ -7,38 +7,46 @@ #include #include "fourdst/config/config.h" +#include "test_schema.h" -std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.yaml"; + +std::string get_good_example_file() { + const char* source_root = getenv("MESON_SOURCE_ROOT"); + if (source_root == nullptr) { + throw std::runtime_error("MESON_SOURCE_ROOT environment variable is not set."); + } + return std::string(source_root) + "/tests/config/example_config_files/example.good.toml"; +} + + +enum class BAD_FILES { + UNKNOWN_KEY, + INVALID_TYPE, + INCORRECT_ARRAY_SIZE +}; + +std::string get_bad_example_file(BAD_FILES type) { + const char* source_root = getenv("MESON_SOURCE_ROOT"); + if (source_root == nullptr) { + throw std::runtime_error("MESON_SOURCE_ROOT environment variable is not set."); + } + switch (type) { + case BAD_FILES::UNKNOWN_KEY: + return std::string(source_root) + "/tests/config/example_config_files/example.unknownkey.toml"; + case BAD_FILES::INVALID_TYPE: + return std::string(source_root) + "/tests/config/example_config_files/example.invalidtype.toml"; + case BAD_FILES::INCORRECT_ARRAY_SIZE: + return std::string(source_root) + "/tests/config/example_config_files/example.incorrectarraysize.toml"; + } + throw std::runtime_error("Invalid BAD_FILES type."); +} + +// std::string EXAMPLE_FILENAME = std::string(getenv("MESON_SOURCE_ROOT")) + "/tests/config/example.toml"; /** * @file configTest.cpp * @brief Unit tests for the Config class. */ -class configTestPrivateAccessor { -public: - static bool callIsKeyInCache(fourdst::config::Config& config, const std::string& key) { - return config.isKeyInCache(key); - } - - static int callCacheSize(fourdst::config::Config& config) { - return config.configMap.size(); - } - - static void callAddToCache(fourdst::config::Config& config, const std::string& key, const YAML::Node& node) { - config.addToCache(key, node); - } - - static void callRegisterKeyNotFound(fourdst::config::Config& config, const std::string& key) { - config.registerUnknownKey(key); - } - - static bool CheckIfKeyUnknown(fourdst::config::Config& config, const std::string& key) { - if (std::find(config.unknownKeys.begin(), config.unknownKeys.end(), key) == config.unknownKeys.end()) { - return false; - } - return true; - } -}; /** * @brief Test suite for the Config class. @@ -49,64 +57,77 @@ class configTest : public ::testing::Test {}; * @brief Test the constructor of the Config class. */ TEST_F(configTest, constructor) { - EXPECT_NO_THROW(fourdst::config::Config::getInstance()); + EXPECT_NO_THROW(fourdst::config::Config()); } -TEST_F(configTest, loadConfig) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); - EXPECT_TRUE(config.loadConfig(EXAMPLE_FILENAME)); +TEST_F(configTest, load_good_file) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); } -TEST_F(configTest, singletonTest) { - fourdst::config::Config& config1 = fourdst::config::Config::getInstance(); - fourdst::config::Config& config2 = fourdst::config::Config::getInstance(); - EXPECT_EQ(&config1, &config2); +TEST_F(configTest, load_unknown_key_file) { + using namespace fourdst::config; + Config cfg; + EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::UNKNOWN_KEY)), exceptions::ConfigParseError); } -TEST_F(configTest, getTest) { - fourdst::config::Config& config = fourdst::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, load_invalid_type_file) { + using namespace fourdst::config; + Config cfg; + EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::INVALID_TYPE)), exceptions::ConfigParseError); } -TEST_F(configTest, secondSingletonTest) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); - EXPECT_EQ(config.get("opac:lowTemp:numeric:maxIter", 10), 100); +TEST_F(configTest, load_incorrect_array_size_file) { + using namespace fourdst::config; + Config cfg; + EXPECT_THROW(cfg.load(get_bad_example_file(BAD_FILES::INCORRECT_ARRAY_SIZE)), exceptions::ConfigParseError); } -TEST_F(configTest, isKeyInCacheTest) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); - config.loadConfig(EXAMPLE_FILENAME); - EXPECT_TRUE(configTestPrivateAccessor::callIsKeyInCache(config, "opac:lowTemp:numeric:maxIter")); - EXPECT_FALSE(configTestPrivateAccessor::callIsKeyInCache(config, "opac:lowTemp:numeric:maxIter2")); +TEST_F(configTest, check_value) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); + EXPECT_EQ(cfg->author, "Example Author"); } -TEST_F(configTest, cacheSize) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); - config.loadConfig(EXAMPLE_FILENAME); - EXPECT_EQ(configTestPrivateAccessor::callCacheSize(config), 3); - EXPECT_NE(configTestPrivateAccessor::callCacheSize(config), 4); - config.get("outputDir", "DEBUG"); - EXPECT_EQ(configTestPrivateAccessor::callCacheSize(config), 4); +TEST_F(configTest, nested_values) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); + EXPECT_EQ(cfg->physics.convection, false); } -TEST_F(configTest, unknownKeyTest) { - fourdst::config::Config& config = fourdst::config::Config::getInstance(); - config.loadConfig(EXAMPLE_FILENAME); - config.get("opac:lowTemp:numeric:random", 10); - EXPECT_FALSE(configTestPrivateAccessor::CheckIfKeyUnknown(config, "opac:lowTemp:numeric:maxIter")); - EXPECT_TRUE(configTestPrivateAccessor::CheckIfKeyUnknown(config, "opac:lowTemp:numeric:random")); +TEST_F(configTest, override_default) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); + EXPECT_EQ(cfg->simulation.time_step, 0.01); +} + +TEST_F(configTest, array_values) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); + constexpr std::array expected = {1, 0, 1}; + EXPECT_EQ(cfg->physics.flags, expected); +} + +TEST_F(configTest, string_values) { + using namespace fourdst::config; + Config cfg; + EXPECT_NO_THROW(cfg.load(get_good_example_file())); + EXPECT_EQ(cfg->output.format, "csv"); +} + +TEST_F(configTest, save_default) { + using namespace fourdst::config; + const Config cfg; + EXPECT_NO_THROW(cfg.save("TestConfigSchema.toml")); +} + +TEST_F(configTest, save_schema) { + using namespace fourdst::config; + const Config cfg; + EXPECT_NO_THROW(cfg.save_schema("./")); } diff --git a/tests/config/example.yaml b/tests/config/example.yaml deleted file mode 100644 index de7fc5f..0000000 --- a/tests/config/example.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# 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/example_config_files/example.good.toml b/tests/config/example_config_files/example.good.toml new file mode 100644 index 0000000..ff8775f --- /dev/null +++ b/tests/config/example_config_files/example.good.toml @@ -0,0 +1,19 @@ +[main] +description = "This is an example configuration file." +author = "Example Author" + +[main.physics] +diffusion = true +convection = false +radiation = true +flags = [1, 0, 1] + +[main.simulation] +time_step = 0.01 +total_time = 100.0 +output_frequency = 10 + +[main.output] +format = "csv" +directory = "results/" +save_plots = true \ No newline at end of file diff --git a/tests/config/example_config_files/example.incorrectarraysize.toml b/tests/config/example_config_files/example.incorrectarraysize.toml new file mode 100644 index 0000000..9855197 --- /dev/null +++ b/tests/config/example_config_files/example.incorrectarraysize.toml @@ -0,0 +1,19 @@ +[main] +description = "This is an example configuration file." +author = "Your Name" + +[main.physics] +diffusion = true +convection = false +radiation = true +flags = [1, 2, 3, 5] + +[main.simulation] +time_step = 0.01 +total_time = 100.0 +output_frequency = 10 + +[main.output] +format = "csv" +directory = "results/" +save_plots = true \ No newline at end of file diff --git a/tests/config/example_config_files/example.invalidtype.toml b/tests/config/example_config_files/example.invalidtype.toml new file mode 100644 index 0000000..7f2b6a3 --- /dev/null +++ b/tests/config/example_config_files/example.invalidtype.toml @@ -0,0 +1,19 @@ +[main] +description = "This is an example configuration file." +author = "Your Name" + +[main.physics] +diffusion = "HELLO" +convection = false +radiation = true +flags = [1, 2, 3] + +[main.simulation] +time_step = 0.01 +total_time = 100.0 +output_frequency = 10 + +[main.output] +format = "csv" +directory = "results/" +save_plots = true \ No newline at end of file diff --git a/tests/config/example_config_files/example.unknownkey.toml b/tests/config/example_config_files/example.unknownkey.toml new file mode 100644 index 0000000..c833b85 --- /dev/null +++ b/tests/config/example_config_files/example.unknownkey.toml @@ -0,0 +1,19 @@ +[main] +description = "This is an example configuration file." +autor = "Your Name" + +[main.physics] +diffusion = true +convection = false +radiation = true +flags = [1, 2, 3] + +[main.simulation] +time_step = 0.01 +total_time = 100.0 +output_frequency = 10 + +[main.output] +format = "csv" +directory = "results/" +save_plots = true \ No newline at end of file diff --git a/tests/config/test_schema.h b/tests/config/test_schema.h new file mode 100644 index 0000000..b21bdcf --- /dev/null +++ b/tests/config/test_schema.h @@ -0,0 +1,30 @@ +#pragma once +#include + +struct PhysicsConfigOptions { + bool diffusion; + bool convection; + bool radiation; + std::array flags; +}; + +struct SimulationConfigOptions { + double time_step = 1; + double total_time = 10; + int output_frequency = 1; +}; + +struct OutputConfigOptions { + std::string directory = "./output"; + std::string format = "hdf5"; + bool save_plots = false; +}; + +struct TestConfigSchema { + std::string description; + std::string author; + + PhysicsConfigOptions physics; + SimulationConfigOptions simulation; + OutputConfigOptions output; +}; \ No newline at end of file