From e503dffc9ac54e8d1afd5adf850fb948a487b539 Mon Sep 17 00:00:00 2001 From: Sergey Kuznetsov Date: Wed, 12 Feb 2025 13:28:06 +0000 Subject: [PATCH] fix: Array parsing in new config (#1884) Fixes #1870. --- src/util/newconfig/Array.cpp | 16 +- src/util/newconfig/Array.hpp | 11 + src/util/newconfig/ConfigFileJson.cpp | 122 +++-- src/util/newconfig/ConfigFileJson.hpp | 20 +- src/util/newconfig/ConfigValue.hpp | 26 +- src/util/newconfig/Types.hpp | 37 +- tests/unit/CMakeLists.txt | 3 +- tests/unit/util/newconfig/ArrayTests.cpp | 12 + .../util/newconfig/ConfigFileJsonTests.cpp | 480 ++++++++++++++++++ .../unit/util/newconfig/ConfigValueTests.cpp | 165 +++++- .../util/newconfig/JsonConfigFileTests.cpp | 113 ----- tests/unit/util/newconfig/JsonFileTests.cpp | 4 +- 12 files changed, 818 insertions(+), 191 deletions(-) create mode 100644 tests/unit/util/newconfig/ConfigFileJsonTests.cpp delete mode 100644 tests/unit/util/newconfig/JsonConfigFileTests.cpp diff --git a/src/util/newconfig/Array.cpp b/src/util/newconfig/Array.cpp index 13d9b836..46cbe1ee 100644 --- a/src/util/newconfig/Array.cpp +++ b/src/util/newconfig/Array.cpp @@ -36,14 +36,22 @@ Array::Array(ConfigValue arg) : itemPattern_{std::move(arg)} { } +std::string_view +Array::prefix(std::string_view key) +{ + static constexpr std::string_view kARRAY_SUFFIX = ".[]"; + ASSERT(key.contains(kARRAY_SUFFIX), "Provided key is not an array key: {}", key); + + return key.substr(0, key.rfind(kARRAY_SUFFIX) + kARRAY_SUFFIX.size()); +} + std::optional Array::addValue(Value value, std::optional key) { - auto const& configValPattern = itemPattern_; - auto const constraint = configValPattern.getConstraint(); + auto const constraint = itemPattern_.getConstraint(); - auto newElem = constraint.has_value() ? ConfigValue{configValPattern.type()}.withConstraint(constraint->get()) - : ConfigValue{configValPattern.type()}; + auto newElem = constraint.has_value() ? ConfigValue{itemPattern_.type()}.withConstraint(constraint->get()) + : ConfigValue{itemPattern_.type()}; if (auto const maybeError = newElem.setValue(value, key); maybeError.has_value()) return maybeError; elements_.emplace_back(std::move(newElem)); diff --git a/src/util/newconfig/Array.hpp b/src/util/newconfig/Array.hpp index 67275765..ca8b4cf0 100644 --- a/src/util/newconfig/Array.hpp +++ b/src/util/newconfig/Array.hpp @@ -47,6 +47,17 @@ public: */ Array(ConfigValue arg); + /** + * @brief Extract array prefix from a key, For example for a key foo.[].bar the method will return foo.[] + * @note Provided key must contain '.[]' + * @warning Be careful with string_view! Returned value is valid only while the key is valid + * + * @param key The key to extract the array prefix from + * @return Prefix of array extracted from the key + */ + static std::string_view + prefix(std::string_view key); + /** * @brief Add ConfigValues to Array class * diff --git a/src/util/newconfig/ConfigFileJson.cpp b/src/util/newconfig/ConfigFileJson.cpp index bb9730fb..976523be 100644 --- a/src/util/newconfig/ConfigFileJson.cpp +++ b/src/util/newconfig/ConfigFileJson.cpp @@ -20,6 +20,7 @@ #include "util/newconfig/ConfigFileJson.hpp" #include "util/Assert.hpp" +#include "util/newconfig/Array.hpp" #include "util/newconfig/Error.hpp" #include "util/newconfig/Types.hpp" @@ -30,15 +31,19 @@ #include #include +#include #include #include #include #include #include #include +#include +#include #include #include #include +#include #include #include @@ -69,14 +74,17 @@ extractJsonValue(boost::json::value const& jsonValue) if (jsonValue.is_double()) { return jsonValue.as_double(); } - ASSERT(false, "Json is not of type int, uint, string, bool or double"); + if (jsonValue.is_null()) { + return NullType{}; + } + ASSERT(false, "Json is not of type null, int, uint, string, bool or double"); std::unreachable(); } } // namespace ConfigFileJson::ConfigFileJson(boost::json::object jsonObj) { - flattenJson(jsonObj, ""); + flattenJson(jsonObj); } std::expected @@ -86,8 +94,7 @@ ConfigFileJson::makeConfigFileJson(std::filesystem::path const& configFilePath) if (auto const in = std::ifstream(configFilePath.string(), std::ios::in | std::ios::binary); in) { std::stringstream contents; contents << in.rdbuf(); - auto opts = boost::json::parse_options{}; - opts.allow_comments = true; + auto const opts = boost::json::parse_options{.allow_comments = true}; auto const tempObj = boost::json::parse(contents.str(), {}, opts).as_object(); return ConfigFileJson{tempObj}; } @@ -105,7 +112,9 @@ ConfigFileJson::makeConfigFileJson(std::filesystem::path const& configFilePath) Value ConfigFileJson::getValue(std::string_view key) const { + ASSERT(containsKey(key), "Key {} not found in ConfigFileJson", key); auto const jsonValue = jsonObject_.at(key); + ASSERT(jsonValue.is_primitive(), "Key {} has value that is not a primitive", key); auto const value = extractJsonValue(jsonValue); return value; } @@ -113,14 +122,15 @@ ConfigFileJson::getValue(std::string_view key) const std::vector ConfigFileJson::getArray(std::string_view key) const { + ASSERT(containsKey(key), "Key {} not found in ConfigFileJson", key); ASSERT(jsonObject_.at(key).is_array(), "Key {} has value that is not an array", key); std::vector configValues; auto const arr = jsonObject_.at(key).as_array(); for (auto const& item : arr) { - auto const value = extractJsonValue(item); - configValues.emplace_back(value); + auto value = extractJsonValue(item); + configValues.emplace_back(std::move(value)); } return configValues; } @@ -131,38 +141,90 @@ ConfigFileJson::containsKey(std::string_view key) const return jsonObject_.contains(key); } -void -ConfigFileJson::flattenJson(boost::json::object const& obj, std::string const& prefix) +boost::json::object const& +ConfigFileJson::inner() const { - for (auto const& [key, value] : obj) { - std::string const fullKey = prefix.empty() ? std::string(key) : fmt::format("{}.{}", prefix, std::string(key)); + return jsonObject_; +} - // In ClioConfigDefinition, value must be a primitive or array - if (value.is_object()) { - flattenJson(value.as_object(), fullKey); - } else if (value.is_array()) { - auto const& arr = value.as_array(); - for (std::size_t i = 0; i < arr.size(); ++i) { - std::string const arrayPrefix = fullKey + ".[]"; - if (arr[i].is_object()) { - flattenJson(arr[i].as_object(), arrayPrefix); +void +ConfigFileJson::flattenJson(boost::json::object const& jsonRootObject) +{ + struct Task { + boost::json::object const& object; + std::string prefix; + std::optional arrayIndex = std::nullopt; + }; + + std::queue tasks; + tasks.push(Task{.object = jsonRootObject, .prefix = ""}); + + std::unordered_map arraysSizes; + + while (not tasks.empty()) { + auto const task = std::move(tasks.front()); + tasks.pop(); + + for (auto const& [key, value] : task.object) { + auto fullKey = + task.prefix.empty() ? std::string(key) : fmt::format("{}.{}", task.prefix, std::string_view{key}); + + if (value.is_object()) { + tasks.push( + Task{.object = value.as_object(), .prefix = std::move(fullKey), .arrayIndex = task.arrayIndex} + ); + } else if (value.is_array()) { + fullKey += ".[]"; + auto const& array = value.as_array(); + + if (std::ranges::all_of(array, [](auto const& v) { return v.is_primitive(); })) { + jsonObject_[fullKey] = array; + } else if (std::ranges::all_of(array, [](auto const& v) { return v.is_object(); })) { + for (size_t i = 0; i < array.size(); ++i) { + tasks.push(Task{.object = array.at(i).as_object(), .prefix = fullKey, .arrayIndex = i}); + } } else { - jsonObject_[arrayPrefix] = arr; + ASSERT( + false, + "Arrays containing both values and objects are not supported. Please check the array {}", + fullKey + ); } - } - } else { - // if "[]" is present in key, then value must be an array instead of primitive - if (fullKey.contains(".[]") && !jsonObject_.contains(fullKey)) { - boost::json::array newArray; - newArray.emplace_back(value); - jsonObject_[fullKey] = newArray; - } else if (fullKey.contains(".[]") && jsonObject_.contains(fullKey)) { - jsonObject_[fullKey].as_array().emplace_back(value); } else { - jsonObject_[fullKey] = value; + if (task.arrayIndex.has_value()) { + if (not jsonObject_.contains(fullKey)) { + jsonObject_[fullKey] = boost::json::array{}; + } + + auto& targetArray = jsonObject_.at(fullKey).as_array(); + while (targetArray.size() < (*task.arrayIndex + 1)) { + targetArray.push_back(boost::json::value()); + } + targetArray.at(*task.arrayIndex) = value; + auto const prefix = std::string{Array::prefix(fullKey)}; + arraysSizes[prefix] = std::max(arraysSizes[prefix], targetArray.size()); + } else { + jsonObject_[fullKey] = value; + } } } } + + // adjust length of each array containing objects + std::ranges::for_each(jsonObject_, [&arraysSizes](auto& item) { + auto const key = item.key(); + if (not key.contains("[]")) + return; + + auto& value = item.value(); + auto const prefix = std::string{Array::prefix(key)}; + if (auto const it = arraysSizes.find(prefix); it != arraysSizes.end()) { + auto const size = it->second; + while (value.as_array().size() < size) { + value.as_array().push_back(boost::json::value{}); + } + } + }); } } // namespace util::config diff --git a/src/util/newconfig/ConfigFileJson.hpp b/src/util/newconfig/ConfigFileJson.hpp index c208a334..a21b18f2 100644 --- a/src/util/newconfig/ConfigFileJson.hpp +++ b/src/util/newconfig/ConfigFileJson.hpp @@ -35,6 +35,8 @@ namespace util::config { /** @brief Json representation of config */ class ConfigFileJson final : public ConfigFileInterface { + boost::json::object jsonObject_; + public: /** * @brief Construct a new ConfigJson object and stores the values from @@ -81,20 +83,26 @@ public: [[nodiscard]] static std::expected makeConfigFileJson(std::filesystem::path const& configFilePath); + /** + * @brief Get the inner representation of json file. + * @note This method is mostly used for testing purposes. + * + * @return The inner representation of json file. + */ + [[nodiscard]] boost::json::object const& + inner() const; + private: /** - * @brief Recursive function to flatten a JSON object into the same structure as the Clio Config. + * @brief Method to flatten a JSON object into the same structure as the Clio Config. * - * The keys will end up having the same naming convensions in Clio Config. + * The keys will end up having the same naming conventions in Clio Config. * Other than the keys specified in user Config file, no new keys are created. * * @param obj The JSON object to flatten. - * @param prefix The prefix to use for the keys in the flattened object. */ void - flattenJson(boost::json::object const& obj, std::string const& prefix); - - boost::json::object jsonObject_; + flattenJson(boost::json::object const& jsonRootObject); }; } // namespace util::config diff --git a/src/util/newconfig/ConfigValue.hpp b/src/util/newconfig/ConfigValue.hpp index d83025ac..d1a50193 100644 --- a/src/util/newconfig/ConfigValue.hpp +++ b/src/util/newconfig/ConfigValue.hpp @@ -34,6 +34,7 @@ #include #include #include +#include #include namespace util::config { @@ -80,18 +81,31 @@ public: [[nodiscard]] std::optional setValue(Value value, std::optional key = std::nullopt) { + if (std::holds_alternative(value)) { + if (hasValue()) { + // Using default value + return std::nullopt; + } + if (not isOptional()) { + return Error{ + key.value_or("Unknown_key"), + "Provided value is null but ConfigValue is not optional and doesn't have a default value." + }; + } + value_ = std::move(value); + return std::nullopt; + } + auto err = checkTypeConsistency(type_, value); if (err.has_value()) { - if (key.has_value()) - err->error = fmt::format("{} {}", key.value(), err->error); + err->error = fmt::format("{} {}", key.value_or("Unknown_key"), err->error); return err; } if (cons_.has_value()) { auto constraintCheck = cons_->get().checkConstraint(value); if (constraintCheck.has_value()) { - if (key.has_value()) - constraintCheck->error = fmt::format("{} {}", key.value(), constraintCheck->error); + constraintCheck->error = fmt::format("{} {}", key.value_or("Unknown_key"), constraintCheck->error); return constraintCheck; } } @@ -127,7 +141,8 @@ public: [&type](bool tmp) { type = fmt::format("bool {}", tmp); }, [&type](std::string const& tmp) { type = fmt::format("string {}", tmp); }, [&type](double tmp) { type = fmt::format("double {}", tmp); }, - [&type](int64_t tmp) { type = fmt::format("int {}", tmp); } + [&type](int64_t tmp) { type = fmt::format("int {}", tmp); }, + [&type](NullType) { type = "null"; }, }, value_.value() ); @@ -199,6 +214,7 @@ public: [[nodiscard]] Value const& getValue() const { + ASSERT(value_.has_value(), "getValue() is called when there is no value set"); return value_.value(); } diff --git a/src/util/newconfig/Types.hpp b/src/util/newconfig/Types.hpp index bc2f1cfd..4e24514c 100644 --- a/src/util/newconfig/Types.hpp +++ b/src/util/newconfig/Types.hpp @@ -21,6 +21,8 @@ #include "util/UnsupportedType.hpp" +#include + #include #include #include @@ -30,7 +32,23 @@ namespace util::config { /** @brief Custom clio config types */ -enum class ConfigType { Integer, String, Double, Boolean }; +enum class ConfigType { Integer, String, Double, Boolean, Null }; + +/** + * @brief A type that represents a null value + */ +struct NullType { + /** + * @brief Compare two NullType objects + * + * @return true always. Any two NullType objects are equal + */ + [[nodiscard]] bool + operator==(NullType const&) const + { + return true; + } +}; /** * @brief Prints the specified config type to output stream @@ -43,7 +61,7 @@ std::ostream& operator<<(std::ostream& stream, ConfigType type); /** @brief Represents the supported Config Values */ -using Value = std::variant; +using Value = std::variant; /** * @brief Prints the specified value to output stream @@ -73,9 +91,24 @@ getType() return ConfigType::Double; } else if constexpr (std::is_same_v) { return ConfigType::Boolean; + } else if constexpr (std::is_same_v) { + return ConfigType::Null; } else { static_assert(util::Unsupported, "Wrong config type"); } } } // namespace util::config + +/** @cond */ +// Doxygen could not parse this +template <> +struct fmt::formatter : fmt::formatter { + [[nodiscard]] + auto + format(util::config::NullType const&, fmt::format_context& ctx) + { + return fmt::formatter::format("null", ctx); + } +}; +/** @endcond */ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 2f0dd7c7..aa37a69d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -183,8 +183,7 @@ target_sources( util/newconfig/ClioConfigDefinitionTests.cpp util/newconfig/ConfigValueTests.cpp util/newconfig/ObjectViewTests.cpp - util/newconfig/JsonConfigFileTests.cpp - util/newconfig/JsonFileTests.cpp + util/newconfig/ConfigFileJsonTests.cpp util/newconfig/ValueViewTests.cpp ) diff --git a/tests/unit/util/newconfig/ArrayTests.cpp b/tests/unit/util/newconfig/ArrayTests.cpp index 3135fdac..59de95b6 100644 --- a/tests/unit/util/newconfig/ArrayTests.cpp +++ b/tests/unit/util/newconfig/ArrayTests.cpp @@ -31,6 +31,18 @@ using namespace util::config; +TEST(ArrayTest, prefix) +{ + EXPECT_EQ(Array::prefix("foo.[]"), "foo.[]"); + EXPECT_EQ(Array::prefix("foo.[].bar"), "foo.[]"); + EXPECT_EQ(Array::prefix("foo.bar.[].baz"), "foo.bar.[]"); +} + +TEST(ArrayDeathTest, prefix) +{ + EXPECT_DEATH(Array::prefix("foo.bar"), ".*"); +} + TEST(ArrayTest, addSingleValue) { auto arr = Array{ConfigValue{ConfigType::Double}}; diff --git a/tests/unit/util/newconfig/ConfigFileJsonTests.cpp b/tests/unit/util/newconfig/ConfigFileJsonTests.cpp new file mode 100644 index 00000000..5ff6cd7e --- /dev/null +++ b/tests/unit/util/newconfig/ConfigFileJsonTests.cpp @@ -0,0 +1,480 @@ +//------------------------------------------------------------------------------ +/* + This file is part of clio: https://github.com/XRPLF/clio + Copyright (c) 2024, the clio developers. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include "util/LoggerFixtures.hpp" +#include "util/NameGenerator.hpp" +#include "util/OverloadSet.hpp" +#include "util/TmpFile.hpp" +#include "util/newconfig/ConfigFileJson.hpp" +#include "util/newconfig/Types.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace util::config; + +namespace { +constexpr auto kEPS = 1e-9; +} // namespace + +struct ConfigFileJsonParseTestBundle { + using ValidationMap = std::unordered_map< + std::string, + std::variant>; + + std::string testName; + std::string configStr; + ValidationMap validationMap; +}; + +struct ConfigFileJsonParseTest : NoLoggerFixture, testing::WithParamInterface {}; + +TEST_P(ConfigFileJsonParseTest, parseValues) +{ + ConfigFileJson const configFile{boost::json::parse(GetParam().configStr).as_object()}; + + auto const& flatJson = configFile.inner(); + + ASSERT_EQ(GetParam().validationMap.size(), flatJson.size()); + std::ranges::for_each(GetParam().validationMap, [&flatJson](auto const& kvPair) { + auto const& key = kvPair.first; + auto const& value = kvPair.second; + + EXPECT_TRUE(flatJson.contains(key)); + + std::visit( + util::OverloadSet{ + [&flatJson, &key](int64_t const v) { + EXPECT_TRUE(flatJson.at(key).is_number()) << key << ": " << v; + EXPECT_EQ(flatJson.at(key).as_int64(), v) << key << ": " << v; + }, + [&flatJson, &key](double const v) { + EXPECT_TRUE(flatJson.at(key).is_double()) << key << ": " << v; + EXPECT_NEAR(flatJson.at(key).as_double(), v, kEPS) << key << ": " << v; + }, + [&flatJson, &key](bool const v) { + EXPECT_TRUE(flatJson.at(key).is_bool()) << key << ": " << v; + EXPECT_EQ(flatJson.at(key).as_bool(), v) << key << ": " << v; + }, + [&flatJson, &key](std::string const& v) { + EXPECT_TRUE(flatJson.at(key).is_string()) << key << ": " << v; + EXPECT_EQ(flatJson.at(key).as_string(), v) << key << ": " << v; + }, + [&flatJson, &key](boost::json::object const& v) { + EXPECT_TRUE(flatJson.at(key).is_object()) << key << ": " << v; + EXPECT_EQ(flatJson.at(key).as_object(), v) << key << ": " << v; + }, + [&flatJson, &key](boost::json::array const& v) { + EXPECT_TRUE(flatJson.at(key).is_array()) << key << ": " << v; + EXPECT_EQ(flatJson.at(key).as_array(), v) << key << ": " << v; + }, + }, + value + ); + }); +} + +INSTANTIATE_TEST_CASE_P( + ConfigFileJsonParseTestGroup, + ConfigFileJsonParseTest, + testing::Values( + ConfigFileJsonParseTestBundle{ + .testName = "values", + .configStr = R"json({ + "int": 42, + "double": 123.456, + "bool": true, + "string": "some string" + })json", + .validationMap = {{"int", 42}, {"double", 123.456}, {"bool", true}, {"string", "some string"}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "nested", + .configStr = R"json({ + "level_0": { + "int": 42, + "level_1":{ + "double": 123.456, + "level_2": { + "bool": true, + "level_3": { + "string": "some string" + } + } + } + } + })json", + .validationMap = + {{"level_0.int", 42}, + {"level_0.level_1.double", 123.456}, + {"level_0.level_1.level_2.bool", true}, + {"level_0.level_1.level_2.level_3.string", "some string"}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "array", + .configStr = R"json({ + "array": [1, 2, 3] + })json", + .validationMap = {{"array.[]", boost::json::array{1, 2, 3}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "nested_array", + .configStr = R"json({ + "level_0": { + "array": [1, 2, 3], + "level_1": { + "array": [4, 5, 6], + "level_2": { + "array": [7, 8, 9] + } + } + } + })json", + .validationMap = + { + {"level_0.array.[]", boost::json::array{1, 2, 3}}, + {"level_0.level_1.array.[]", boost::json::array{4, 5, 6}}, + {"level_0.level_1.level_2.array.[]", boost::json::array{7, 8, 9}}, + } + }, + ConfigFileJsonParseTestBundle{ + .testName = "mixed", + .configStr = R"json({ + "int": 42, + "double": 123.456, + "bool": true, + "string": "some string", + "array": [1, 2, 3], + "nested": { + "int": 42, + "double": 123.456, + "bool": true, + "string": "some string", + "array": [1, 2, 3] + } + })json", + .validationMap = + { + {"int", 42}, + {"double", 123.456}, + {"bool", true}, + {"string", "some string"}, + {"array.[]", boost::json::array{1, 2, 3}}, + {"nested.int", 42}, + {"nested.double", 123.456}, + {"nested.bool", true}, + {"nested.string", "some string"}, + {"nested.array.[]", boost::json::array{1, 2, 3}}, + } + }, + ConfigFileJsonParseTestBundle{.testName = "empty", .configStr = R"json({})json", .validationMap = {}}, + ConfigFileJsonParseTestBundle{ + .testName = "empty_nested", + .configStr = R"json({ + "level_0": { + "level_1": { + "level_2": { + "level_3": {} + } + } + } + })json", + .validationMap = {} + }, + ConfigFileJsonParseTestBundle{ + .testName = "empty_array", + .configStr = R"json({ + "array": [] + })json", + .validationMap = {{"array.[]", boost::json::array{}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "empty_nested_array", + .configStr = R"json({ + "level_0": { + "array": [], + "level_1": { + "array": [], + "level_2": { + "array": [] + } + } + } + })json", + .validationMap = + { + {"level_0.array.[]", boost::json::array{}}, + {"level_0.level_1.array.[]", boost::json::array{}}, + {"level_0.level_1.level_2.array.[]", boost::json::array{}}, + } + }, + ConfigFileJsonParseTestBundle{ + .testName = "object_inside_array", + .configStr = R"json({ + "array": [ + { "int": 42 } + ] + })json", + .validationMap = {{"array.[].int", boost::json::array{42}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "object_with_optional_fields_inside_array", + .configStr = R"json({ + "array": [ + {"int": 42}, + {"int": 24, "bool": true} + ] + })json", + .validationMap = + {{"array.[].int", boost::json::array{42, 24}}, + {"array.[].bool", boost::json::array{boost::json::value{}, true}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "full_object_is_at_the_front_of_array", + .configStr = R"json({ + "array": [ + {"int": 42, "bool": true}, + {"int": 2}, + {"int": 4} + ] + })json", + .validationMap = + {{"array.[].int", boost::json::array{42, 2, 4}}, + {"array.[].bool", boost::json::array{true, boost::json::value{}, boost::json::value{}}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "full_object_is_in_the_middle_of_array", + .configStr = R"json({ + "array": [ + {"int": 42}, + {"int": 2, "bool": true}, + {"int": 4} + ] + })json", + .validationMap = + {{"array.[].int", boost::json::array{42, 2, 4}}, + {"array.[].bool", boost::json::array{boost::json::value{}, true, boost::json::value{}}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "no_full_object", + .configStr = R"json({ + "array": [ + {"int": 42}, + {"int": 2}, + {"bool": true} + ] + })json", + .validationMap = + {{"array.[].int", boost::json::array{42, 2, boost::json::value{}}}, + {"array.[].bool", boost::json::array{boost::json::value{}, boost::json::value{}, true}}} + }, + ConfigFileJsonParseTestBundle{ + .testName = "array_with_nexted_objects", + .configStr = R"json({ + "array": [ + { "object": { "int": 42 } }, + { "object": { "string": "some string" } } + ] + })json", + .validationMap = + {{"array.[].object.int", boost::json::array{42, boost::json::value{}}}, + {"array.[].object.string", boost::json::array{boost::json::value{}, "some string"}}} + } + ), + tests::util::kNAME_GENERATOR +); + +struct ConfigFileJsonTest : NoLoggerFixture {}; + +TEST_F(ConfigFileJsonTest, getValue) +{ + auto const jsonStr = R"json({ + "int": 42, + "object": { "string": "some string" }, + "bool": true, + "double": 123.456, + "null": null + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + + auto const intValue = jsonFileObj.getValue("int"); + ASSERT_TRUE(std::holds_alternative(intValue)); + EXPECT_EQ(std::get(intValue), 42); + + auto const stringValue = jsonFileObj.getValue("object.string"); + ASSERT_TRUE(std::holds_alternative(stringValue)); + EXPECT_EQ(std::get(stringValue), "some string"); + + auto const boolValue = jsonFileObj.getValue("bool"); + ASSERT_TRUE(std::holds_alternative(boolValue)); + EXPECT_EQ(std::get(boolValue), true); + + auto const doubleValue = jsonFileObj.getValue("double"); + ASSERT_TRUE(std::holds_alternative(doubleValue)); + EXPECT_NEAR(std::get(doubleValue), 123.456, kEPS); + + auto const nullValue = jsonFileObj.getValue("null"); + EXPECT_TRUE(std::holds_alternative(nullValue)); + + EXPECT_FALSE(jsonFileObj.containsKey("object.int")); +} + +struct ConfigFileJsonDeathTest : ConfigFileJsonTest {}; + +TEST_F(ConfigFileJsonDeathTest, getValueInvalidKey) +{ + auto const jsonFileObj = ConfigFileJson{boost::json::parse("{}").as_object()}; + EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getValue("some_key"), ".*"); +} + +TEST_F(ConfigFileJsonDeathTest, getValueOfArray) +{ + auto const jsonStr = R"json({ + "array": [1, 2, 3] + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getValue("array"), ".*"); +} + +TEST_F(ConfigFileJsonTest, getArray) +{ + auto const jsonStr = R"json({ + "array": [1, "2", 3.14, true], + "object": { "array": [3, 4] } + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + + auto const array = jsonFileObj.getArray("array.[]"); + ASSERT_EQ(array.size(), 4); + ASSERT_TRUE(std::holds_alternative(array.at(0))); + EXPECT_EQ(std::get(array.at(0)), 1); + ASSERT_TRUE(std::holds_alternative(array.at(1))); + EXPECT_EQ(std::get(array.at(1)), "2"); + ASSERT_TRUE(std::holds_alternative(array.at(2))); + EXPECT_NEAR(std::get(array.at(2)), 3.14, kEPS); + ASSERT_TRUE(std::holds_alternative(array.at(3))); + EXPECT_EQ(std::get(array.at(3)), true); + + auto const arrayFromObject = jsonFileObj.getArray("object.array.[]"); + ASSERT_EQ(arrayFromObject.size(), 2); + EXPECT_EQ(std::get(arrayFromObject.at(0)), 3); + EXPECT_EQ(std::get(arrayFromObject.at(1)), 4); +} + +TEST_F(ConfigFileJsonTest, getArrayObjectInArray) +{ + auto const jsonStr = R"json({ + "array": [ + { "int": 42 }, + { "string": "some string" } + ] + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + + auto const ints = jsonFileObj.getArray("array.[].int"); + ASSERT_EQ(ints.size(), 2); + ASSERT_TRUE(std::holds_alternative(ints.at(0))); + EXPECT_EQ(std::get(ints.at(0)), 42); + EXPECT_TRUE(std::holds_alternative(ints.at(1))); + + auto const strings = jsonFileObj.getArray("array.[].string"); + ASSERT_EQ(strings.size(), 2); + EXPECT_TRUE(std::holds_alternative(strings.at(0))); + ASSERT_TRUE(std::holds_alternative(strings.at(1))); + EXPECT_EQ(std::get(strings.at(1)), "some string"); +} + +TEST_F(ConfigFileJsonDeathTest, getArrayInvalidKey) +{ + auto const jsonFileObj = ConfigFileJson{boost::json::parse("{}").as_object()}; + EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("some_key"), ".*"); +} + +TEST_F(ConfigFileJsonDeathTest, getArrayNotArray) +{ + auto const jsonStr = R"json({ + "int": 42 + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("int"), ".*"); +} + +TEST_F(ConfigFileJsonTest, containsKey) +{ + auto const jsonStr = R"json({ + "int": 42, + "object": { "string": "some string", "array": [1, 2, 3] }, + "array2": [1, 2, 3], + "array_of_objects": [ {"int": 42}, {"string": "some string"} ] + })json"; + auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()}; + + EXPECT_TRUE(jsonFileObj.containsKey("int")); + EXPECT_FALSE(jsonFileObj.containsKey("other_key")); + + EXPECT_TRUE(jsonFileObj.containsKey("object.string")); + EXPECT_FALSE(jsonFileObj.containsKey("object.int")); + EXPECT_TRUE(jsonFileObj.containsKey("object.array.[]")); + EXPECT_FALSE(jsonFileObj.containsKey("object.array")); + + EXPECT_TRUE(jsonFileObj.containsKey("array2.[]")); + EXPECT_FALSE(jsonFileObj.containsKey("array2")); + EXPECT_FALSE(jsonFileObj.containsKey("array2.[].int")); + + EXPECT_TRUE(jsonFileObj.containsKey("array_of_objects.[].int")); + EXPECT_TRUE(jsonFileObj.containsKey("array_of_objects.[].string")); + EXPECT_FALSE(jsonFileObj.containsKey("array_of_objects.[]")); + EXPECT_FALSE(jsonFileObj.containsKey("array_of_objects.[].object")); +} + +struct ConfigFileJsonMakeTest : ConfigFileJsonTest {}; + +TEST_F(ConfigFileJsonMakeTest, invalidFile) +{ + auto const jsonFileObj = ConfigFileJson::makeConfigFileJson("does_not_exist"); + EXPECT_FALSE(jsonFileObj.has_value()); +} + +TEST_F(ConfigFileJsonMakeTest, invalidJson) +{ + auto const file = TmpFile("invalid json"); + auto const jsonFileObj = ConfigFileJson::makeConfigFileJson(file.path); + EXPECT_FALSE(jsonFileObj.has_value()); +} + +TEST_F(ConfigFileJsonMakeTest, validFile) +{ + auto const file = TmpFile(R"json({ "int": 42 })json"); + auto const jsonFileObj = ConfigFileJson::makeConfigFileJson(file.path); + ASSERT_TRUE(jsonFileObj.has_value()); + + auto const& flatJson = jsonFileObj->inner(); + ASSERT_EQ(flatJson.size(), 1); + ASSERT_TRUE(flatJson.contains("int")); + ASSERT_TRUE(flatJson.at("int").is_number()); + EXPECT_EQ(flatJson.at("int").as_int64(), 42); +} diff --git a/tests/unit/util/newconfig/ConfigValueTests.cpp b/tests/unit/util/newconfig/ConfigValueTests.cpp index cee07ac7..8faf9dc4 100644 --- a/tests/unit/util/newconfig/ConfigValueTests.cpp +++ b/tests/unit/util/newconfig/ConfigValueTests.cpp @@ -17,62 +17,173 @@ */ //============================================================================== +#include "util/LoggerFixtures.hpp" #include "util/newconfig/ConfigConstraints.hpp" #include "util/newconfig/ConfigValue.hpp" +#include "util/newconfig/Error.hpp" #include "util/newconfig/Types.hpp" #include +#include #include #include +#include +#include #include using namespace util::config; -TEST(ConfigValue, GetSetString) +struct ConfigValueTest : NoLoggerFixture {}; +struct ConfigValueDeathTest : ConfigValueTest {}; + +TEST_F(ConfigValueTest, construct) { - auto const cvStr = ConfigValue{ConfigType::String}.defaultValue("12345"); - EXPECT_EQ(cvStr.type(), ConfigType::String); - EXPECT_TRUE(cvStr.hasValue()); - EXPECT_FALSE(cvStr.isOptional()); + ConfigValue const cv{ConfigType::Integer}; + EXPECT_FALSE(cv.hasValue()); + EXPECT_FALSE(cv.isOptional()); + EXPECT_EQ(cv.type(), ConfigType::Integer); } -TEST(ConfigValue, GetSetInteger) +TEST_F(ConfigValueTest, optional) { - auto const cvInt = ConfigValue{ConfigType::Integer}.defaultValue(543); - EXPECT_EQ(cvInt.type(), ConfigType::Integer); - EXPECT_TRUE(cvInt.hasValue()); - EXPECT_FALSE(cvInt.isOptional()); + auto const cv = ConfigValue{ConfigType::Integer}.optional(); + EXPECT_FALSE(cv.hasValue()); + EXPECT_TRUE(cv.isOptional()); + EXPECT_EQ(cv.type(), ConfigType::Integer); +} - auto const cvOpt = ConfigValue{ConfigType::Integer}.optional(); - EXPECT_TRUE(cvOpt.isOptional()); +TEST_F(ConfigValueTest, defaultValue) +{ + auto const cv = ConfigValue{ConfigType::Integer}.defaultValue(123); + EXPECT_TRUE(cv.hasValue()); + EXPECT_FALSE(cv.isOptional()); + EXPECT_EQ(cv.type(), ConfigType::Integer); +} + +TEST_F(ConfigValueDeathTest, invalidDefaultValue) +{ + EXPECT_DEATH({ [[maybe_unused]] auto const a = ConfigValue{ConfigType::String}.defaultValue(33); }, ".*"); +} + +TEST_F(ConfigValueTest, setValueNull) +{ + auto cv = ConfigValue{ConfigType::Integer}; + auto const err = cv.setValue(NullType{}); + EXPECT_TRUE(err.has_value()); +} + +TEST_F(ConfigValueTest, setValueNullOptional) +{ + auto cv = ConfigValue{ConfigType::Integer}.optional(); + auto const err = cv.setValue(NullType{}); + EXPECT_FALSE(err.has_value()); +} + +TEST_F(ConfigValueTest, setValueNullDefault) +{ + auto cv = ConfigValue{ConfigType::Integer}.defaultValue(123); + auto const err = cv.setValue(NullType{}); + EXPECT_FALSE(err.has_value()); + EXPECT_EQ(cv.getValue(), Value{123}); +} + +TEST_F(ConfigValueTest, setValueWrongType) +{ + auto cv = ConfigValue{ConfigType::Integer}; + auto const err = cv.setValue("123"); + EXPECT_TRUE(err.has_value()); +} + +TEST_F(ConfigValueTest, setValueNormalPath) +{ + auto cv = ConfigValue{ConfigType::Integer}; + auto const err = cv.setValue(123); + EXPECT_FALSE(err.has_value()); + EXPECT_EQ(cv.getValue(), Value{123}); +} + +struct ConfigValueConstraintTest : ConfigValueTest { + struct MockConstraint : Constraint { + MOCK_METHOD(std::optional, checkTypeImpl, (Value const&), (const, override)); + MOCK_METHOD(std::optional, checkValueImpl, (Value const&), (const, override)); + MOCK_METHOD(void, print, (std::ostream&), (const, override)); + }; + + testing::StrictMock constraint; +}; + +TEST_F(ConfigValueConstraintTest, setValueWithConstraint) +{ + auto cv = ConfigValue{ConfigType::Integer}.withConstraint(constraint); + auto const value = Value{123}; + EXPECT_CALL(constraint, checkTypeImpl).WillOnce(testing::Return(std::nullopt)); + EXPECT_CALL(constraint, checkValueImpl).WillOnce(testing::Return(std::nullopt)); + auto const err = cv.setValue(value); + EXPECT_FALSE(err.has_value()); + EXPECT_EQ(cv.getValue(), value); +} + +TEST_F(ConfigValueConstraintTest, setValueWithConstraintTypeCheckError) +{ + auto cv = ConfigValue{ConfigType::Integer}.withConstraint(constraint); + auto const value = 123; + EXPECT_CALL(constraint, checkTypeImpl).WillOnce(testing::Return(Error{"type error"})); + auto const err = cv.setValue(value); + EXPECT_TRUE(err.has_value()); + EXPECT_EQ(err->error, "Unknown_key type error"); +} + +TEST_F(ConfigValueConstraintTest, defaultValueWithConstraint) +{ + EXPECT_CALL(constraint, checkTypeImpl).WillOnce(testing::Return(std::nullopt)); + EXPECT_CALL(constraint, checkValueImpl).WillOnce(testing::Return(std::nullopt)); + auto const cv = ConfigValue{ConfigType::Integer}.defaultValue(123).withConstraint(constraint); + EXPECT_EQ(cv.getValue(), Value{123}); +} + +struct ConfigValueConstraintDeathTest : ConfigValueConstraintTest {}; + +TEST_F(ConfigValueConstraintDeathTest, defaultValueWithConstraintCheckError) +{ + EXPECT_DEATH( + { + EXPECT_CALL(constraint, checkTypeImpl).WillOnce(testing::Return(std::nullopt)); + EXPECT_CALL(constraint, checkValueImpl).WillOnce(testing::Return(Error{"value error"})); + [[maybe_unused]] auto const cv = + ConfigValue{ConfigType::Integer}.defaultValue(123).withConstraint(constraint); + }, + ".*" + ); } // A test for each constraint so it's easy to change in the future -TEST(ConfigValue, PortConstraint) +struct ConstraintTest : NoLoggerFixture {}; + +TEST_F(ConstraintTest, PortConstraint) { auto const portConstraint{PortConstraint{}}; EXPECT_FALSE(portConstraint.checkConstraint(4444).has_value()); EXPECT_TRUE(portConstraint.checkConstraint(99999).has_value()); } -TEST(ConfigValue, SetValuesOnPortConstraint) +TEST_F(ConstraintTest, SetValuesOnPortConstraint) { auto cvPort = ConfigValue{ConfigType::Integer}.defaultValue(4444).withConstraint(gValidatePort); auto const err = cvPort.setValue(99999); EXPECT_TRUE(err.has_value()); - EXPECT_EQ(err->error, "Port does not satisfy the constraint bounds"); + EXPECT_EQ(err->error, "Unknown_key Port does not satisfy the constraint bounds"); EXPECT_TRUE(cvPort.setValue(33.33).has_value()); - EXPECT_TRUE(cvPort.setValue(33.33).value().error == "value does not match type integer"); + EXPECT_EQ(cvPort.setValue(33.33).value().error, "Unknown_key value does not match type integer"); EXPECT_FALSE(cvPort.setValue(1).has_value()); auto cvPort2 = ConfigValue{ConfigType::String}.defaultValue("4444").withConstraint(gValidatePort); auto const strPortError = cvPort2.setValue("100000"); EXPECT_TRUE(strPortError.has_value()); - EXPECT_EQ(strPortError->error, "Port does not satisfy the constraint bounds"); + EXPECT_EQ(strPortError->error, "Unknown_key Port does not satisfy the constraint bounds"); } -TEST(ConfigValue, OneOfConstraintOneValue) +TEST_F(ConstraintTest, OneOfConstraintOneValue) { std::array const arr = {"tracer"}; auto const databaseConstraint{OneOf{"database.type", arr}}; @@ -88,7 +199,7 @@ TEST(ConfigValue, OneOfConstraintOneValue) ); } -TEST(ConfigValue, OneOfConstraint) +TEST_F(ConstraintTest, OneOfConstraint) { std::array const arr = {"123", "trace", "haha"}; auto const oneOfCons{OneOf{"log_level", arr}}; @@ -105,14 +216,14 @@ TEST(ConfigValue, OneOfConstraint) ); } -TEST(ConfigValue, IpConstraint) +TEST_F(ConstraintTest, IpConstraint) { auto ip = ConfigValue{ConfigType::String}.defaultValue("127.0.0.1").withConstraint(gValidateIp); EXPECT_FALSE(ip.setValue("http://127.0.0.1").has_value()); EXPECT_FALSE(ip.setValue("http://127.0.0.1.com").has_value()); auto const err = ip.setValue("123.44"); EXPECT_TRUE(err.has_value()); - EXPECT_EQ(err->error, "Ip is not a valid ip address"); + EXPECT_EQ(err->error, "Unknown_key Ip is not a valid ip address"); EXPECT_FALSE(ip.setValue("126.0.0.2")); EXPECT_TRUE(ip.setValue("644.3.3.0")); @@ -123,7 +234,7 @@ TEST(ConfigValue, IpConstraint) EXPECT_FALSE(ip.setValue("http://example.com:8080/path")); } -TEST(ConfigValue, positiveNumConstraint) +TEST_F(ConstraintTest, positiveNumConstraint) { auto const numCons{NumberValueConstraint{0, 5}}; EXPECT_FALSE(numCons.checkConstraint(0)); @@ -136,7 +247,7 @@ TEST(ConfigValue, positiveNumConstraint) EXPECT_EQ(numCons.checkConstraint(8)->error, fmt::format("Number must be between {} and {}", 0, 5)); } -TEST(ConfigValue, SetValuesOnNumberConstraint) +TEST_F(ConstraintTest, SetValuesOnNumberConstraint) { auto positiveNum = ConfigValue{ConfigType::Integer}.defaultValue(20u).withConstraint(gValidateUint16); auto const err = positiveNum.setValue(-22, "key"); @@ -145,7 +256,7 @@ TEST(ConfigValue, SetValuesOnNumberConstraint) EXPECT_FALSE(positiveNum.setValue(99, "key")); } -TEST(ConfigValue, PositiveDoubleConstraint) +TEST_F(ConstraintTest, PositiveDoubleConstraint) { auto const doubleCons{PositiveDouble{}}; EXPECT_FALSE(doubleCons.checkConstraint(0.2)); @@ -162,7 +273,7 @@ struct ConstraintTestBundle { Constraint const& constraint; }; -struct ConstraintDeathTest : public testing::Test, public testing::WithParamInterface {}; +struct ConstraintDeathTest : testing::TestWithParam {}; INSTANTIATE_TEST_SUITE_P( EachConstraints, @@ -195,7 +306,7 @@ TEST_P(ConstraintDeathTest, TestEachConstraint) ); } -TEST(ConfigValueDeathTest, SetInvalidValueTypeStringAndBool) +TEST(ConstraintDeathTest, SetInvalidValueTypeStringAndBool) { EXPECT_DEATH( { @@ -207,7 +318,7 @@ TEST(ConfigValueDeathTest, SetInvalidValueTypeStringAndBool) EXPECT_DEATH({ [[maybe_unused]] auto a = ConfigValue{ConfigType::Boolean}.defaultValue(-66); }, ".*"); } -TEST(ConfigValueDeathTest, OutOfBounceIntegerConstraint) +TEST(ConstraintDeathTest, OutOfBounceIntegerConstraint) { EXPECT_DEATH( { diff --git a/tests/unit/util/newconfig/JsonConfigFileTests.cpp b/tests/unit/util/newconfig/JsonConfigFileTests.cpp deleted file mode 100644 index c8c11390..00000000 --- a/tests/unit/util/newconfig/JsonConfigFileTests.cpp +++ /dev/null @@ -1,113 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of clio: https://github.com/XRPLF/clio - Copyright (c) 2024, the clio developers. - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include "util/TmpFile.hpp" -#include "util/newconfig/ConfigFileJson.hpp" -#include "util/newconfig/FakeConfigData.hpp" - -#include -#include - -#include -#include -#include -#include -#include - -TEST(CreateConfigFile, filePath) -{ - auto const jsonFileObj = ConfigFileJson::makeConfigFileJson(TmpFile(kJSON_DATA).path); - EXPECT_TRUE(jsonFileObj.has_value()); - - EXPECT_TRUE(jsonFileObj->containsKey("array.[].sub")); - auto const arrSub = jsonFileObj->getArray("array.[].sub"); - EXPECT_EQ(arrSub.size(), 3); -} - -TEST(CreateConfigFile, incorrectFilePath) -{ - auto const jsonFileObj = util::config::ConfigFileJson::makeConfigFileJson("123/clio"); - EXPECT_FALSE(jsonFileObj.has_value()); -} - -struct ParseJson : testing::Test { - ParseJson() : jsonFileObj{boost::json::parse(kJSON_DATA).as_object()} - { - } - - ConfigFileJson const jsonFileObj; -}; - -TEST_F(ParseJson, validateValues) -{ - EXPECT_TRUE(jsonFileObj.containsKey("header.text1")); - EXPECT_EQ(std::get(jsonFileObj.getValue("header.text1")), "value"); - - EXPECT_TRUE(jsonFileObj.containsKey("header.sub.sub2Value")); - EXPECT_EQ(std::get(jsonFileObj.getValue("header.sub.sub2Value")), "TSM"); - - EXPECT_TRUE(jsonFileObj.containsKey("dosguard.port")); - EXPECT_EQ(std::get(jsonFileObj.getValue("dosguard.port")), 44444); - - EXPECT_FALSE(jsonFileObj.containsKey("idk")); - EXPECT_FALSE(jsonFileObj.containsKey("optional.withNoDefault")); -} - -TEST_F(ParseJson, validateArrayValue) -{ - // validate array.[].sub matches expected values - EXPECT_TRUE(jsonFileObj.containsKey("array.[].sub")); - auto const arrSub = jsonFileObj.getArray("array.[].sub"); - EXPECT_EQ(arrSub.size(), 3); - - std::vector expectedArrSubVal{111.11, 4321.55, 5555.44}; - std::vector actualArrSubVal{}; - - for (auto it = arrSub.begin(); it != arrSub.end(); ++it) { - ASSERT_TRUE(std::holds_alternative(*it)); - actualArrSubVal.emplace_back(std::get(*it)); - } - EXPECT_TRUE(std::ranges::equal(expectedArrSubVal, actualArrSubVal)); - - // validate array.[].sub2 matches expected values - EXPECT_TRUE(jsonFileObj.containsKey("array.[].sub2")); - auto const arrSub2 = jsonFileObj.getArray("array.[].sub2"); - EXPECT_EQ(arrSub2.size(), 3); - std::vector expectedArrSub2Val{"subCategory", "temporary", "london"}; - std::vector actualArrSub2Val{}; - - for (auto it = arrSub2.begin(); it != arrSub2.end(); ++it) { - ASSERT_TRUE(std::holds_alternative(*it)); - actualArrSub2Val.emplace_back(std::get(*it)); - } - EXPECT_TRUE(std::ranges::equal(expectedArrSub2Val, actualArrSub2Val)); - - EXPECT_TRUE(jsonFileObj.containsKey("dosguard.whitelist.[]")); - auto const whitelistArr = jsonFileObj.getArray("dosguard.whitelist.[]"); - EXPECT_EQ(whitelistArr.size(), 2); - EXPECT_EQ("125.5.5.1", std::get(whitelistArr.at(0))); - EXPECT_EQ("204.2.2.1", std::get(whitelistArr.at(1))); -} - -struct JsonValueDeathTest : ParseJson {}; - -TEST_F(JsonValueDeathTest, invalidGetArray) -{ - EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("header.text1"), ".*"); -} diff --git a/tests/unit/util/newconfig/JsonFileTests.cpp b/tests/unit/util/newconfig/JsonFileTests.cpp index 8204004e..94bde312 100644 --- a/tests/unit/util/newconfig/JsonFileTests.cpp +++ b/tests/unit/util/newconfig/JsonFileTests.cpp @@ -89,9 +89,9 @@ TEST_F(JsonFromTempFile, validateArrayValue) EXPECT_EQ("204.2.2.1", std::get(whitelistArr.at(1))); } -struct JsonValueDeathTest : JsonFromTempFile {}; +struct ConfigValueJsonGetArrayDeathTest : JsonFromTempFile {}; -TEST_F(JsonValueDeathTest, invalidGetValues) +TEST_F(ConfigValueJsonGetArrayDeathTest, invalidGetValues) { // not possible for json value to call a value that doesn't exist EXPECT_DEATH([[maybe_unused]] auto a = jsonFileObj.getArray("header.text1"), ".*");