382 lines
15 KiB
C++
382 lines
15 KiB
C++
#include "ikvxx/ikvxx.hpp"
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<bool>(expression), #expression, __LINE__)
|
|
|
|
template <typename Function>
|
|
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<std::string> 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<const char*>(nullptr); });
|
|
|
|
const std::int64_t wide_values[] = {
|
|
std::numeric_limits<std::int64_t>::min(), 0, std::numeric_limits<std::int64_t>::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<std::int64_t>::max();
|
|
root["too-small"] = std::numeric_limits<std::int64_t>::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<const char*>(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<const char*>(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<ikv::Value::ArrayIndex>(2)].asString() == "three");
|
|
CHECK(strings[static_cast<ikv::Value::ArrayIndex>(0)].asString().empty());
|
|
CHECK(strings[static_cast<ikv::Value::ArrayIndex>(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<ikv::Value::ArrayIndex>(0)].asString().empty());
|
|
CHECK(const_strings[static_cast<ikv::Value::ArrayIndex>(1)].asString() == "two");
|
|
CHECK(const_strings[static_cast<ikv::Value::ArrayIndex>(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<const char*>(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() {
|
|
testConstructionAndTypeInspection();
|
|
testAssignmentsAndConversions();
|
|
testObjectsMembersAndLookupOverloads();
|
|
testArraysAndIndexOverloads();
|
|
testTextParsingAndFiles();
|
|
testBinaryMemoryAndFiles();
|
|
testRefreshAndOwnership();
|
|
if (failures != 0) std::cerr << failures << " test(s) failed\n";
|
|
return failures == 0 ? 0 : 1;
|
|
}
|