diff --git a/.clang-format b/.clang-format index 8bb6ededfdb..b1963c909f3 100644 --- a/.clang-format +++ b/.clang-format @@ -70,7 +70,7 @@ IncludeCategories: - Regex: '^<(omp|cu|hip|oneapi|thrust|CL/|cooperative|mpi|nvToolsExt|Kokkos).*' Priority: 2 SortPriority: 3 - - Regex: '^<(nlohmann|gflags|gtest|sde_lib|papi).*' + - Regex: '^<(yaml-cpp|nlohmann|gflags|gtest|sde_lib|papi).*' Priority: 4 - Regex: '' Priority: 6 diff --git a/extensions/test/config/CMakeLists.txt b/extensions/test/config/CMakeLists.txt index a3f2017d530..14be75625a8 100644 --- a/extensions/test/config/CMakeLists.txt +++ b/extensions/test/config/CMakeLists.txt @@ -1,6 +1,20 @@ +find_package(yaml-cpp 0.8.0 QUIET) +if(NOT yaml-cpp_FOUND) +message(STATUS "Fetching external yaml-cpp") + FetchContent_Declare( + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG 0.8.0 + ) + FetchContent_MakeAvailable(yaml-cpp) +endif() ginkgo_create_test(json_config ADDITIONAL_LIBRARIES nlohmann_json::nlohmann_json) +ginkgo_create_test(yaml_config ADDITIONAL_LIBRARIES yaml-cpp::yaml-cpp) # prepare the testing file and generate location configure_file("${Ginkgo_SOURCE_DIR}/extensions/test/config/file_location.hpp.in" "${Ginkgo_BINARY_DIR}/extensions/test/config/file_location.hpp" @ONLY) configure_file(test.json "${Ginkgo_BINARY_DIR}/extensions/test/config/test.json") +configure_file(test.yaml "${Ginkgo_BINARY_DIR}/extensions/test/config/test.yaml") +configure_file(alias.yaml "${Ginkgo_BINARY_DIR}/extensions/test/config/alias.yaml") +configure_file(nested_alias.yaml "${Ginkgo_BINARY_DIR}/extensions/test/config/nested_alias.yaml") diff --git a/extensions/test/config/alias.yaml b/extensions/test/config/alias.yaml new file mode 100644 index 00000000000..9fddb039346 --- /dev/null +++ b/extensions/test/config/alias.yaml @@ -0,0 +1,7 @@ +base: &base_config + key1: 123 +base2: &base_config2 + key2: test +test: + <<: [*base_config, *base_config2] + key3: true diff --git a/extensions/test/config/file_location.hpp.in b/extensions/test/config/file_location.hpp.in index 3d2a462c19d..cf4cb7bc162 100644 --- a/extensions/test/config/file_location.hpp.in +++ b/extensions/test/config/file_location.hpp.in @@ -13,6 +13,13 @@ namespace config { const char* location_test_json = "@Ginkgo_BINARY_DIR@/extensions/test/config/test.json"; +const char* location_test_yaml = + "@Ginkgo_BINARY_DIR@/extensions/test/config/test.yaml"; +const char* location_alias_yaml = + "@Ginkgo_BINARY_DIR@/extensions/test/config/alias.yaml"; +const char* location_nested_alias_yaml = + "@Ginkgo_BINARY_DIR@/extensions/test/config/nested_alias.yaml"; + } // namespace config diff --git a/extensions/test/config/nested_alias.yaml b/extensions/test/config/nested_alias.yaml new file mode 100644 index 00000000000..62544aba4f5 --- /dev/null +++ b/extensions/test/config/nested_alias.yaml @@ -0,0 +1,9 @@ +base: &base_config + key1: 123 +base2: &base_config2 + <<: *base_config + key2: test +test: + <<: *base_config2 + key2: override + key3: true diff --git a/extensions/test/config/test.yaml b/extensions/test/config/test.yaml new file mode 100644 index 00000000000..986512727f8 --- /dev/null +++ b/extensions/test/config/test.yaml @@ -0,0 +1,6 @@ +item: 4 +array: + - 3.0 + - 4.5 +map: + bool: false diff --git a/extensions/test/config/yaml_config.cpp b/extensions/test/config/yaml_config.cpp new file mode 100644 index 00000000000..2e86127de39 --- /dev/null +++ b/extensions/test/config/yaml_config.cpp @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include + +#include +#include + +#include +#include + +#include "core/test/utils.hpp" +#include "extensions/test/config/file_location.hpp" + + +TEST(YamlConfig, ThrowIfInvalid) +{ + const char yaml[] = "test: null"; + auto d = YAML::Load(yaml); + + ASSERT_THROW(gko::ext::config::parse_yaml(d), std::runtime_error); +} + + +TEST(YamlConfig, ReadMap) +{ + const char yaml[] = R"( +test: A +bool: true +)"; + auto d = YAML::Load(yaml); + + auto ptree = gko::ext::config::parse_yaml(d); + + ASSERT_EQ(ptree.get_map().size(), 2); + ASSERT_EQ(ptree.get("test").get_string(), "A"); + ASSERT_EQ(ptree.get("bool").get_boolean(), true); +} + + +TEST(YamlConfig, ReadArray) +{ + const char yaml[] = R"( +- A +- B +- C +)"; + auto d = YAML::Load(yaml); + + auto ptree = gko::ext::config::parse_yaml(d); + + ASSERT_EQ(ptree.get_array().size(), 3); + ASSERT_EQ(ptree.get(0).get_string(), "A"); + ASSERT_EQ(ptree.get(1).get_string(), "B"); + ASSERT_EQ(ptree.get(2).get_string(), "C"); +} + + +TEST(YamlConfig, ReadInput) +{ + const char yaml[] = R"( +item: 4 +array: + - 3.0 + - 4.5 +map: + bool: false)"; + auto d = YAML::Load(yaml); + + auto ptree = gko::ext::config::parse_yaml(d); + + auto& child_array = ptree.get("array").get_array(); + auto& child_map = ptree.get("map").get_map(); + ASSERT_EQ(ptree.get_map().size(), 3); + ASSERT_EQ(ptree.get("item").get_integer(), 4); + ASSERT_EQ(child_array.size(), 2); + ASSERT_EQ(child_array.at(0).get_real(), 3.0); + ASSERT_EQ(child_array.at(1).get_real(), 4.5); + ASSERT_EQ(child_map.size(), 1); + ASSERT_EQ(child_map.at("bool").get_boolean(), false); +} + + +TEST(YamlConfig, ReadInputFromFile) +{ + auto ptree = + gko::ext::config::parse_yaml_file(gko::ext::config::location_test_yaml); + + auto& child_array = ptree.get("array").get_array(); + auto& child_map = ptree.get("map").get_map(); + ASSERT_EQ(ptree.get_map().size(), 3); + ASSERT_EQ(ptree.get("item").get_integer(), 4); + ASSERT_EQ(child_array.size(), 2); + ASSERT_EQ(child_array.at(0).get_real(), 3.0); + ASSERT_EQ(child_array.at(1).get_real(), 4.5); + ASSERT_EQ(child_map.size(), 1); + ASSERT_EQ(child_map.at("bool").get_boolean(), false); +} + + +TEST(YamlConfig, ReadInputFromFileWithAlias) +{ + auto yaml = YAML::LoadFile(gko::ext::config::location_alias_yaml); + + auto ptree = gko::ext::config::parse_yaml(yaml["test"]); + + ASSERT_EQ(ptree.get_map().size(), 3); + ASSERT_EQ(ptree.get_map().at("key1").get_integer(), 123); + ASSERT_EQ(ptree.get_map().at("key2").get_string(), "test"); + ASSERT_EQ(ptree.get_map().at("key3").get_boolean(), true); +} + + +TEST(YamlConfig, ReadInputFromFileWithNestedAliasAndOverwrite) +{ + auto yaml = YAML::LoadFile(gko::ext::config::location_nested_alias_yaml); + + auto ptree = gko::ext::config::parse_yaml(yaml["test"]); + + ASSERT_EQ(ptree.get_map().size(), 3); + ASSERT_EQ(ptree.get_map().at("key1").get_integer(), 123); + ASSERT_EQ(ptree.get_map().at("key2").get_string(), "override"); + ASSERT_EQ(ptree.get_map().at("key3").get_boolean(), true); +} diff --git a/include/ginkgo/extensions/config/yaml_config.hpp b/include/ginkgo/extensions/config/yaml_config.hpp new file mode 100644 index 00000000000..b7dfd07a593 --- /dev/null +++ b/include/ginkgo/extensions/config/yaml_config.hpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2017 - 2024 The Ginkgo authors +// +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef GKO_PUBLIC_EXTENSIONS_CONFIG_JSON_CONFIG_HPP_ +#define GKO_PUBLIC_EXTENSIONS_CONFIG_JSON_CONFIG_HPP_ + +#include +#include +#include + +#include + +#include + + +namespace gko { +namespace ext { +namespace config { + + +/** + * parse_yaml takes the yaml-cpp node object to generate the property tree + * object + */ +inline gko::config::pnode parse_yaml(const YAML::Node& input) +{ + auto parse_array = [](const auto& arr) { + gko::config::pnode::array_type nodes; + for (const auto& it : arr) { + nodes.emplace_back(parse_yaml(it)); + } + return gko::config::pnode{nodes}; + }; + auto parse_map = [](const auto& map) { + gko::config::pnode::map_type nodes; + // use [] to get override behavior + for (YAML::const_iterator it = map.begin(); it != map.end(); ++it) { + std::string key = it->first.as(); + // yaml-cpp keeps the alias without resolving it when parsing. + // We resolve them here. + if (key == "<<") { + auto node = parse_yaml(it->second); + if (node.get_tag() == gko::config::pnode::tag_t::array) { + for (const auto& arr : node.get_array()) { + for (const auto& item : arr.get_map()) { + nodes[item.first] = item.second; + } + } + } else if (node.get_tag() == gko::config::pnode::tag_t::map) { + for (const auto& item : node.get_map()) { + nodes[item.first] = item.second; + } + } else { + std::runtime_error("can not handle this alias: " + + YAML::Dump(it->second)); + } + } else { + std::string content = it->first.as(); + nodes[key] = parse_yaml(it->second); + } + } + return gko::config::pnode{nodes}; + }; + // yaml-cpp does not have type check + auto parse_data = [](const auto& data) { + if (std::int64_t value; + YAML::convert::decode(data, value)) { + return gko::config::pnode{value}; + } + if (bool value; YAML::convert::decode(data, value)) { + return gko::config::pnode{value}; + } + if (double value; YAML::convert::decode(data, value)) { + return gko::config::pnode{value}; + } + if (std::string value; + YAML::convert::decode(data, value)) { + return gko::config::pnode{value}; + } + std::string content = YAML::Dump(data); + throw std::runtime_error( + "property_tree can not handle the node with content: " + content); + }; + + if (input.IsSequence()) { + return parse_array(input); + } + if (input.IsMap()) { + return parse_map(input); + } + return parse_data(input); +} + + +/** + * parse_yaml_file takes the yaml file to generate the property tree object + * + * @note Because YAML always needs a entry for reusing, there will be more than + * one entry when putting the anchors in the top level. This function can not + * know which entry is the actual solver, so please use the parse_yaml function. + */ +inline gko::config::pnode parse_yaml_file(std::string filename) +{ + return parse_yaml(YAML::LoadFile(filename)); +} + + +} // namespace config +} // namespace ext +} // namespace gko + + +#endif // GKO_PUBLIC_EXTENSIONS_CONFIG_JSON_CONFIG_HPP_