Files
iKvxx/tests/value_tests.cpp
GigabiteStudios e651ae1575
Some checks failed
C++ bindings / gcc / Debug (push) Failing after 10s
C++ bindings / clang / Debug (push) Failing after 17s
C++ bindings / clang / Release (push) Failing after 10s
C++ bindings / gcc / Release (push) Failing after 16s
feat(bindings): add modern C++ interface for iKv
2026-06-17 18:57:54 -05:00

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;
}