Files
clio/tests/unit/util/config/ConfigFileJsonTests.cpp
2026-03-24 15:25:32 +00:00

534 lines
19 KiB
C++

#include "util/MockAssert.hpp"
#include "util/NameGenerator.hpp"
#include "util/OverloadSet.hpp"
#include "util/TmpFile.hpp"
#include "util/config/ConfigFileJson.hpp"
#include <boost/json/array.hpp>
#include <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <variant>
#include <vector>
using namespace util::config;
namespace {
constexpr auto kEPS = 1e-9;
} // namespace
struct ConfigFileJsonParseTestBundle {
using ValidationMap = std::unordered_map<
std::string,
std::variant<int64_t, double, bool, std::string, boost::json::object, boost::json::array>>;
std::string testName;
std::string configStr;
ValidationMap validationMap;
};
struct ConfigFileJsonParseTest : public virtual ::testing::Test,
testing::WithParamInterface<ConfigFileJsonParseTestBundle> {};
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 : public virtual ::testing::Test {};
TEST_F(ConfigFileJsonTest, getValue)
{
auto const jsonStr = R"JSON({
"int": 42,
"object": { "string": "some string" },
"bool": true,
"double": 123.456
})JSON";
auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()};
auto const intValue = jsonFileObj.getValue("int");
ASSERT_TRUE(std::holds_alternative<int64_t>(intValue));
EXPECT_EQ(std::get<int64_t>(intValue), 42);
auto const stringValue = jsonFileObj.getValue("object.string");
ASSERT_TRUE(std::holds_alternative<std::string>(stringValue));
EXPECT_EQ(std::get<std::string>(stringValue), "some string");
auto const boolValue = jsonFileObj.getValue("bool");
ASSERT_TRUE(std::holds_alternative<bool>(boolValue));
EXPECT_EQ(std::get<bool>(boolValue), true);
auto const doubleValue = jsonFileObj.getValue("double");
ASSERT_TRUE(std::holds_alternative<double>(doubleValue));
EXPECT_NEAR(std::get<double>(doubleValue), 123.456, kEPS);
EXPECT_FALSE(jsonFileObj.containsKey("object.int"));
}
struct ConfigFileJsonAssertTest : common::util::WithMockAssert, ConfigFileJsonTest {};
TEST_F(ConfigFileJsonAssertTest, getValueInvalidKey)
{
auto const jsonFileObj = ConfigFileJson{boost::json::parse("{}").as_object()};
EXPECT_CLIO_ASSERT_FAIL([[maybe_unused]] auto a = jsonFileObj.getValue("some_key"));
}
TEST_F(ConfigFileJsonAssertTest, getValueOfArray)
{
auto const jsonStr = R"JSON({
"array": [1, 2, 3]
})JSON";
auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()};
EXPECT_CLIO_ASSERT_FAIL([[maybe_unused]] auto a = jsonFileObj.getValue("array"));
}
TEST_F(ConfigFileJsonAssertTest, nullIsNotSupported)
{
auto const jsonStr = R"JSON({
"null": null
})JSON";
auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()};
EXPECT_CLIO_ASSERT_FAIL([[maybe_unused]] auto a = jsonFileObj.getValue("null"));
}
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);
auto const value0 = array.at(0).value();
ASSERT_TRUE(std::holds_alternative<int64_t>(value0));
EXPECT_EQ(std::get<int64_t>(value0), 1);
auto const value1 = array.at(1).value();
ASSERT_TRUE(std::holds_alternative<std::string>(value1));
EXPECT_EQ(std::get<std::string>(value1), "2");
auto const value2 = array.at(2).value();
ASSERT_TRUE(std::holds_alternative<double>(value2));
EXPECT_NEAR(std::get<double>(value2), 3.14, kEPS);
auto const value3 = array.at(3).value();
ASSERT_TRUE(std::holds_alternative<bool>(value3));
EXPECT_EQ(std::get<bool>(value3), true);
auto const arrayFromObject = jsonFileObj.getArray("object.array.[]");
ASSERT_EQ(arrayFromObject.size(), 2);
EXPECT_EQ(std::get<int64_t>(arrayFromObject.at(0).value()), 3);
EXPECT_EQ(std::get<int64_t>(arrayFromObject.at(1).value()), 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<int64_t>(ints.at(0).value()));
EXPECT_EQ(std::get<int64_t>(ints.at(0).value()), 42);
EXPECT_FALSE(ints.at(1).has_value());
auto const strings = jsonFileObj.getArray("array.[].string");
ASSERT_EQ(strings.size(), 2);
EXPECT_FALSE(strings.at(0).has_value());
ASSERT_TRUE(std::holds_alternative<std::string>(strings.at(1).value()));
EXPECT_EQ(std::get<std::string>(strings.at(1).value()), "some string");
}
TEST_F(ConfigFileJsonTest, getArrayOptionalInArray)
{
auto const jsonStr = R"JSON({
"array": [
{ "int": 42 },
{ "int": 24, "bool": true }
]
})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<int64_t>(ints.at(0).value()));
EXPECT_EQ(std::get<int64_t>(ints.at(0).value()), 42);
ASSERT_TRUE(std::holds_alternative<int64_t>(ints.at(1).value()));
EXPECT_EQ(std::get<int64_t>(ints.at(1).value()), 24);
auto const bools = jsonFileObj.getArray("array.[].bool");
ASSERT_EQ(bools.size(), 2);
EXPECT_FALSE(bools.at(0).has_value());
ASSERT_TRUE(std::holds_alternative<bool>(bools.at(1).value()));
EXPECT_EQ(std::get<bool>(bools.at(1).value()), true);
}
TEST_F(ConfigFileJsonAssertTest, getArrayInvalidKey)
{
auto const jsonFileObj = ConfigFileJson{boost::json::parse("{}").as_object()};
EXPECT_CLIO_ASSERT_FAIL([[maybe_unused]] auto a = jsonFileObj.getArray("some_key"));
}
TEST_F(ConfigFileJsonAssertTest, getArrayNotArray)
{
auto const jsonStr = R"JSON({
"int": 42
})JSON";
auto const jsonFileObj = ConfigFileJson{boost::json::parse(jsonStr).as_object()};
EXPECT_CLIO_ASSERT_FAIL([[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"));
}
TEST_F(ConfigFileJsonTest, getAllKeys)
{
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()};
auto allKeys = jsonFileObj.getAllKeys();
std::ranges::sort(allKeys);
EXPECT_EQ(allKeys.size(), 6);
std::vector<std::string> const expectedKeys{
{"array2.[]",
"array_of_objects.[].int",
"array_of_objects.[].string",
"int",
"object.array.[]",
"object.string"}
};
EXPECT_EQ(allKeys, expectedKeys);
}
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);
}