#include "ikvxx/ikvxx.hpp" #include #include #include #include #include #include #include #include #include namespace { int failures = 0; int exception_expectations = 0; void check(bool condition, const char* expression, int line) { if (!condition) { std::cerr << "line " << line << ": CHECK(" << expression << ") failed\n"; ++failures; } } #define CHECK(expression) check(static_cast(expression), #expression, __LINE__) template void expectThrow(Function&& function) { const int current = ++exception_expectations; try { function(); std::cerr << "expected exception case " << current << " did not throw\n"; CHECK(false); } catch (const std::exception&) { } } std::filesystem::path testPath(const std::string& name) { return std::filesystem::current_path() / ("ikvxx-test-" + name); } struct FileCleanup { explicit FileCleanup(std::filesystem::path value) : path(std::move(value)) {} ~FileCleanup() { std::error_code ignored; std::filesystem::remove(path, ignored); } std::filesystem::path path; }; ikv::Value makeScalarTree() { ikv::Value root(ikv::objectValue, "types"); root["string"] = std::string("value"); root["int"] = std::int64_t{-9}; root["real"] = 2.5; root["bool"] = true; root.makeObject("object"); root.makeArray("array"); return root; } void testConstructionAndTypeInspection() { for (const std::string& name : {"root", "", "save data"}) { ikv::Value object(ikv::objectValue, name); CHECK(object.type() == ikv::objectValue); CHECK(object.isObject()); CHECK(!object.isArray()); CHECK(object.name() == name); CHECK(object.empty()); CHECK(object.size() == 0); } for (const std::string& name : {"items", "", "typed values"}) { ikv::Value array(ikv::arrayValue, name); CHECK(array.type() == ikv::arrayValue); CHECK(array.isArray()); CHECK(!array.isObject()); CHECK(array.name() == name); CHECK(array.empty()); CHECK(array.size() == 0); } expectThrow([] { ikv::Value value(ikv::stringValue); }); expectThrow([] { ikv::Value value(ikv::intValue); }); expectThrow([] { ikv::Value value(ikv::nullValue); }); auto root = makeScalarTree(); auto missing = root["missing"]; CHECK(missing.type() == ikv::nullValue); CHECK(missing.isNull()); CHECK(!root.isNull()); CHECK(!root["array"].isNull()); CHECK(missing.empty()); CHECK(missing.size() == 0); struct ExpectedType { const char* key; ikv::ValueType type; }; const ExpectedType expected[] = { {"string", ikv::stringValue}, {"int", ikv::intValue}, {"real", ikv::realValue}, {"bool", ikv::booleanValue}, {"object", ikv::objectValue}, {"array", ikv::arrayValue} }; for (const auto& entry : expected) CHECK(root[entry.key].type() == entry.type); CHECK(root["string"].isString()); CHECK(!root["int"].isString()); CHECK(!root["missing"].isString()); CHECK(root["int"].isInt()); CHECK(!root["real"].isInt()); CHECK(!root["missing"].isInt()); CHECK(root["real"].isDouble()); CHECK(!root["int"].isDouble()); CHECK(!root["missing"].isDouble()); CHECK(root["bool"].isBool()); CHECK(!root["string"].isBool()); CHECK(!root["missing"].isBool()); CHECK(root["object"].isObject()); CHECK(!root["array"].isObject()); CHECK(!root["missing"].isObject()); CHECK(root["array"].isArray()); CHECK(!root["object"].isArray()); CHECK(!root["missing"].isArray()); } void testAssignmentsAndConversions() { ikv::Value root; const std::vector strings = {"", "hello", "line\nquote\""}; for (std::size_t i = 0; i < strings.size(); ++i) { root["std-string-" + std::to_string(i)] = strings[i]; CHECK(root["std-string-" + std::to_string(i)].asString() == strings[i]); } for (const char* value : {"alpha", "", "unicode-ish"}) { root[std::string("c-string-") + std::to_string(root.size())] = value; } CHECK(root["c-string-3"].asString() == "alpha"); CHECK(root["c-string-4"].asString().empty()); CHECK(root["c-string-5"].asString() == "unicode-ish"); expectThrow([&] { root["bad"] = static_cast(nullptr); }); const std::int64_t wide_values[] = { std::numeric_limits::min(), 0, std::numeric_limits::max() }; for (int i = 0; i < 3; ++i) { root["wide-" + std::to_string(i)] = wide_values[i]; CHECK(root["wide-" + std::to_string(i)].asInt64() == wide_values[i]); } for (int value : {-17, 0, 42}) { const auto key = "int-" + std::to_string(value); root[key] = value; CHECK(root[key].asInt() == value); } for (double value : {-3.5, 0.0, 1.0 / 3.0}) { const auto key = "real-" + std::to_string(root.size()); root[key] = value; CHECK(std::abs(root[key].asDouble() - value) < 1e-12); } root["bool-false"] = false; root["bool-true"] = true; root["bool-replaced"] = false; root["bool-replaced"] = true; CHECK(!root["bool-false"].asBool()); CHECK(root["bool-true"].asBool()); CHECK(root["bool-replaced"].asBool()); root["replace"] = "first"; root["replace"] = 2; root["replace"] = 4.5; CHECK(root["replace"].isDouble()); CHECK(std::abs(root["replace"].asDouble() - 4.5) < 1e-12); root["too-large"] = std::numeric_limits::max(); root["too-small"] = std::numeric_limits::min(); expectThrow([&] { (void)root["too-large"].asInt(); }); expectThrow([&] { (void)root["too-small"].asInt(); }); expectThrow([&] { (void)root["bool-true"].asString(); }); expectThrow([&] { (void)root["bool-true"].asInt64(); }); expectThrow([&] { (void)root["bool-true"].asDouble(); }); expectThrow([&] { (void)root["replace"].asBool(); }); } void testObjectsMembersAndLookupOverloads() { ikv::Value root; for (const std::string& key : {"first", "", "with spaces"}) { auto object = root.makeObject(key); object["value"] = key; CHECK(object.isObject()); CHECK(object.name() == key); } CHECK(root.isMember("first")); CHECK(root.isMember("")); CHECK(root.isMember("with spaces")); CHECK(!root.isMember("missing")); CHECK(!root["first"].isMember("missing")); std::string first = "first"; CHECK(root[first][std::string("value")].asString() == "first"); CHECK(root["with spaces"]["value"].asString() == "with spaces"); expectThrow([&] { (void)root[static_cast(nullptr)]; }); const ikv::Value& const_root = root; CHECK(const_root[first][std::string("value")].asString() == "first"); const std::string spaced = "with spaces"; const std::string absent = "missing"; CHECK(const_root[spaced].isObject()); CHECK(const_root[absent].isNull()); CHECK(const_root["with spaces"]["value"].asString() == "with spaces"); CHECK(const_root["missing"].isNull()); expectThrow([&] { (void)const_root[static_cast(nullptr)]; }); ikv::Value fallback(ikv::objectValue, "fallback"); CHECK(const_root.get("first", fallback).name() == "first"); CHECK(const_root.get("missing", fallback).name() == "fallback"); CHECK(const_root.get("", fallback).name().empty()); expectThrow([&] { root["scalar"] = 1; root["scalar"].makeObject("bad"); }); expectThrow([&] { root["scalar"].makeArray("bad"); }); const ikv::Value& read_only = root; expectThrow([&] { read_only["first"].makeObject("bad"); }); } void testArraysAndIndexOverloads() { ikv::Value root; auto mixed = root.makeArray("mixed"); auto strings = root.makeArray("strings", ikv::stringValue); auto integers = root.makeArray("integers", ikv::intValue); auto reals = root.makeArray("reals", ikv::realValue); auto booleans = root.makeArray("booleans", ikv::booleanValue); auto objects = root.makeArray("objects", ikv::objectValue); for (const std::string& value : {"", "two", "three"}) strings.append(value); for (const char* value : {"four", "five", "six"}) mixed.append(value); for (std::int64_t value : {-9000000000LL, 0LL, 9000000000LL}) integers.append(value); for (int value : {-2, 0, 7}) mixed.append(value); for (double value : {-1.5, 0.0, 9.25}) reals.append(value); for (bool value : {false, true, false}) booleans.append(value); for (int i = 0; i < 3; ++i) objects.appendObject()["index"] = i; CHECK(strings.size() == 3); CHECK(strings[0].asString().empty()); CHECK(strings[1].asString() == "two"); CHECK(strings[static_cast(2)].asString() == "three"); CHECK(strings[static_cast(0)].asString().empty()); CHECK(strings[static_cast(1)].asString() == "two"); CHECK(integers[0].asInt64() == -9000000000LL); CHECK(integers[1].asInt64() == 0); CHECK(integers[2].asInt64() == 9000000000LL); CHECK(std::abs(reals[0].asDouble() + 1.5) < 1e-12); CHECK(std::abs(reals[1].asDouble()) < 1e-12); CHECK(std::abs(reals[2].asDouble() - 9.25) < 1e-12); CHECK(!booleans[0].asBool()); CHECK(booleans[1].asBool()); CHECK(!booleans[2].asBool()); CHECK(objects[0]["index"].asInt() == 0); CHECK(objects[1]["index"].asInt() == 1); CHECK(objects[2]["index"].asInt() == 2); const ikv::Value& const_strings = strings; CHECK(const_strings[0].asString().empty()); CHECK(const_strings[1].asString() == "two"); CHECK(const_strings[2].asString() == "three"); CHECK(const_strings[static_cast(0)].asString().empty()); CHECK(const_strings[static_cast(1)].asString() == "two"); CHECK(const_strings[static_cast(2)].asString() == "three"); expectThrow([&] { (void)strings[-1]; }); expectThrow([&] { (void)strings[3]; }); expectThrow([&] { (void)const_strings[-1]; }); expectThrow([&] { (void)const_strings[3]; }); expectThrow([&] { strings.append(7); }); expectThrow([&] { integers.append("wrong"); }); expectThrow([&] { objects.append(false); }); expectThrow([&] { mixed.append(static_cast(nullptr)); }); expectThrow([&] { root.makeArray("nested", ikv::arrayValue); }); expectThrow([&] { strings.appendObject(); }); const ikv::Value& read_only = root; expectThrow([&] { read_only["mixed"].append(1); }); expectThrow([&] { read_only["mixed"].append("value"); }); expectThrow([&] { read_only["mixed"].appendObject(); }); } void testTextParsingAndFiles() { const std::string documents[] = { "ikv2 \"root\"\n{\n \"name\" \"test\"\n}\n", "ikv1 \"legacy\"\n{\n \"n\" -7\n}\n", "ikv2 \"nested\"\n{\n \"child\" { \"ok\" true }\n}\n" }; CHECK(ikv::Value::parse(documents[0])["name"].asString() == "test"); CHECK(ikv::Value::parse(documents[1])["n"].asInt() == -7); CHECK(ikv::Value::parse(documents[2])["child"]["ok"].asBool()); expectThrow([] { (void)ikv::Value::parse("ikv2 \"root\"\n"); }); expectThrow([] { (void)ikv::Value::parse("not ikv"); }); expectThrow([] { (void)ikv::Value::parse("ikv2 \"root\"\n{\n \"value\" 1\n"); }); const FileCleanup v1(testPath("text-v1.ikv")); const FileCleanup v2(testPath("text-v2.ikv")); const FileCleanup bad(testPath("text-bad.ikv")); ikv::Value source; source["value"] = 11; source.write(v1.path.string(), ikv::Version::v1); source.write(v2.path.string()); CHECK(ikv::Value::load(v1.path.string())["value"].asInt() == 11); CHECK(ikv::Value::load(v2.path.string())["value"].asInt() == 11); expectThrow([&] { (void)ikv::Value::load(testPath("missing.ikv").string()); }); expectThrow([&] { source.write(testPath("missing-dir/file.ikv").string()); }); { std::ofstream output(bad.path); output << "broken"; } expectThrow([&] { (void)ikv::Value::load(bad.path.string()); }); } void testBinaryMemoryAndFiles() { ikv::Value source(ikv::objectValue, "binary"); source["number"] = 73; source["text"] = "payload"; source.makeArray("flags", ikv::booleanValue).append(true); const auto v1 = source.toBinary(ikv::Version::v1); const auto v2 = source.toBinary(); const auto v2_explicit = source.toBinary(ikv::Version::v2); CHECK(!v1.empty()); CHECK(!v2.empty()); CHECK(v2_explicit == v2); CHECK(ikv::Value::fromBinary(v1.data(), v1.size())["number"].asInt() == 73); CHECK(ikv::Value::fromBinary(v2.data(), v2.size())["text"].asString() == "payload"); CHECK(ikv::Value::fromBinary(v2.data(), v2.size())["flags"][0].asBool()); const std::uint8_t invalid[] = {0, 1, 2, 3, 4}; expectThrow([&] { (void)ikv::Value::fromBinary(invalid, sizeof(invalid)); }); expectThrow([] { (void)ikv::Value::fromBinary(nullptr, 1); }); expectThrow([] { (void)ikv::Value::fromBinary(nullptr, 0); }); const FileCleanup v1_file(testPath("binary-v1.ikvb")); const FileCleanup v2_file(testPath("binary-v2.ikvb")); const FileCleanup bad_file(testPath("binary-bad.ikvb")); source.writeBinary(v1_file.path.string(), ikv::Version::v1); source.writeBinary(v2_file.path.string()); CHECK(ikv::Value::loadBinary(v1_file.path.string())["number"].asInt() == 73); CHECK(ikv::Value::loadBinary(v2_file.path.string())["text"].asString() == "payload"); expectThrow([&] { source.writeBinary(testPath("missing-dir/file.ikvb").string()); }); expectThrow([&] { (void)ikv::Value::loadBinary(testPath("missing.ikvb").string()); }); { std::ofstream output(bad_file.path, std::ios::binary); output << "broken"; } expectThrow([&] { (void)ikv::Value::loadBinary(bad_file.path.string()); }); } void testRefreshAndOwnership() { const FileCleanup first(testPath("refresh-first.ikv")); const FileCleanup second(testPath("refresh-second.ikvb")); ikv::Value text_source; text_source["value"] = 1; text_source.write(first.path.string()); ikv::Value binary_source; binary_source["value"] = 2; binary_source.writeBinary(second.path.string()); ikv::Value root; root["value"] = 0; auto shared = root; root.refresh(first.path.string()); CHECK(root["value"].asInt() == 1); CHECK(shared["value"].asInt() == 1); root.refresh(second.path.string()); CHECK(root["value"].asInt() == 2); expectThrow([&] { root.refresh(testPath("missing-refresh.ikv").string()); }); CHECK(root["value"].asInt() == 2); auto child = root["value"]; expectThrow([&] { child.refresh(first.path.string()); }); const ikv::Value& read_only = root; expectThrow([&] { read_only["value"] = 3; }); } } // namespace int main(int argc, char** argv) { struct TestCase { const char* name; void (*function)(); }; const TestCase tests[] = { {"construction_and_type_inspection", testConstructionAndTypeInspection}, {"assignments_and_conversions", testAssignmentsAndConversions}, {"objects_members_and_lookup_overloads", testObjectsMembersAndLookupOverloads}, {"arrays_and_index_overloads", testArraysAndIndexOverloads}, {"text_parsing_and_files", testTextParsingAndFiles}, {"binary_memory_and_files", testBinaryMemoryAndFiles}, {"refresh_and_ownership", testRefreshAndOwnership} }; if (argc == 3 && std::string(argv[1]) == "--case") { for (const auto& test : tests) { if (argv[2] == std::string(test.name)) { test.function(); if (failures != 0) std::cerr << failures << " test(s) failed\n"; return failures == 0 ? 0 : 1; } } std::cerr << "unknown test case: " << argv[2] << '\n'; return 2; } if (argc != 1) { std::cerr << "usage: ikvxx_tests [--case ]\n"; return 2; } for (const auto& test : tests) test.function(); if (failures != 0) std::cerr << failures << " test(s) failed\n"; return failures == 0 ? 0 : 1; }