270 lines
7.5 KiB
C++
270 lines
7.5 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Json.h"
|
|
|
|
#include <string>
|
|
|
|
using namespace eve::json;
|
|
|
|
TEST_CASE("json.parseScalars") {
|
|
Document doc = Document::parse(R"({
|
|
"s": "hello",
|
|
"i": 42,
|
|
"neg": -7,
|
|
"f": 1.5,
|
|
"exp": 2e3,
|
|
"t": true,
|
|
"f2": false,
|
|
"n": null
|
|
})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
CHECK(r.isObject());
|
|
CHECK_EQ(r.size(), 8u);
|
|
|
|
CHECK_EQ(r.getString("s"), "hello");
|
|
CHECK_EQ(r.getInt("i"), 42);
|
|
CHECK_EQ(r.getInt("neg"), -7);
|
|
CHECK_EQ(r.getFloat("f"), 1.5f);
|
|
CHECK_EQ(r.getDouble("exp"), 2000.0);
|
|
CHECK(r.getBool("t"));
|
|
CHECK(!r.getBool("f2"));
|
|
CHECK(r.get("n").isNull());
|
|
|
|
CHECK(r.isObject());
|
|
CHECK(r.get("s").isString());
|
|
CHECK(r.get("i").isNumber());
|
|
CHECK(r.get("t").isBool());
|
|
}
|
|
|
|
TEST_CASE("json.missingKeysFallBack") {
|
|
Document doc = Document::parse(R"({"a": 1})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
CHECK(!r.has("nope"));
|
|
CHECK_EQ(r.getInt("nope", 9), 9);
|
|
CHECK_EQ(r.getFloat("nope", 2.5f), 2.5f);
|
|
CHECK_EQ(r.getString("nope", "dflt"), "dflt");
|
|
CHECK(r.getBool("nope", true));
|
|
// A missing key yields a null Value, and chaining off it stays safe.
|
|
CHECK(!r.get("nope"));
|
|
CHECK_EQ(r.get("nope").get("deeper").getInt("deepest", 3), 3);
|
|
}
|
|
|
|
TEST_CASE("json.wrongTypeFallsBack") {
|
|
Document doc = Document::parse(R"({"s": "abc", "o": {"k": 1}, "a": [1,2]})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
// A non-numeric string is not a number.
|
|
CHECK_EQ(r.getInt("s", 5), 5);
|
|
// Objects and arrays are not scalars.
|
|
CHECK_EQ(r.getString("o", "dflt"), "dflt");
|
|
CHECK_EQ(r.getInt("a", 7), 7);
|
|
// Indexing a non-array, or out of range, yields null.
|
|
CHECK(!r.get("o").at(0));
|
|
CHECK(!r.get("a").at(2));
|
|
}
|
|
|
|
TEST_CASE("json.lenientScalarConversion") {
|
|
Document doc = Document::parse(R"({"num": "12", "real": "1.25", "n": 3, "b": true})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
// Numeric strings read as numbers, matching the Poco-based helpers this
|
|
// facade replaces.
|
|
CHECK_EQ(r.getInt("num", 0), 12);
|
|
CHECK_EQ(r.getFloat("real", 0.f), 1.25f);
|
|
// Numbers read back as strings.
|
|
CHECK_EQ(r.getString("n"), "3");
|
|
CHECK_EQ(r.getString("b"), "true");
|
|
}
|
|
|
|
TEST_CASE("json.integerExactness") {
|
|
// Values a double cannot represent exactly must still round-trip through
|
|
// getInt, and out-of-range ones fall back rather than wrap.
|
|
Document doc = Document::parse(R"({"big": 2147483647, "over": 4294967296, "trunc": 2.9})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
CHECK_EQ(r.getInt("big", 0), 2147483647);
|
|
CHECK_EQ(r.getInt("over", -1), -1);
|
|
CHECK_EQ(r.getInt("trunc", 0), 2);
|
|
}
|
|
|
|
TEST_CASE("json.arrays") {
|
|
Document doc = Document::parse(R"({
|
|
"tags": ["a", "b", "c"],
|
|
"sizes": [1, 2, 3],
|
|
"coords": [0.5, 1.5],
|
|
"mixed": ["x", 2, true]
|
|
})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
auto tags = r.getStringArray("tags");
|
|
REQUIRE(tags.size() == 3u);
|
|
CHECK_EQ(tags[0], "a");
|
|
CHECK_EQ(tags[2], "c");
|
|
|
|
auto sizes = r.getIntArray("sizes");
|
|
REQUIRE(sizes.size() == 3u);
|
|
CHECK_EQ(sizes[1], 2);
|
|
|
|
auto coords = r.getFloatArray("coords");
|
|
REQUIRE(coords.size() == 2u);
|
|
CHECK_EQ(coords[0], 0.5f);
|
|
|
|
// Mixed arrays stringify elementwise.
|
|
auto mixed = r.getStringArray("mixed");
|
|
REQUIRE(mixed.size() == 3u);
|
|
CHECK_EQ(mixed[1], "2");
|
|
|
|
CHECK(r.getStringArray("absent").empty());
|
|
CHECK(r.getIntArray("tags").size() == 3u); // non-numeric strings -> 0
|
|
}
|
|
|
|
TEST_CASE("json.maps") {
|
|
Document doc = Document::parse(R"({
|
|
"extra": {"faction": "elf", "rank": "3"},
|
|
"counts": {"wood": 5, "ore": 2}
|
|
})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
auto extra = r.getStringMap("extra");
|
|
CHECK_EQ(extra.size(), 2u);
|
|
CHECK_EQ(extra["faction"], "elf");
|
|
|
|
auto counts = r.getIntMap("counts");
|
|
CHECK_EQ(counts.size(), 2u);
|
|
CHECK_EQ(counts["wood"], 5);
|
|
|
|
CHECK(r.getStringMap("absent").empty());
|
|
}
|
|
|
|
TEST_CASE("json.nestedNavigation") {
|
|
Document doc = Document::parse(R"({
|
|
"effects": [
|
|
{"id": "burn", "modifiers": [{"attribute": "hp", "value": -3}]},
|
|
{"id": "chill"}
|
|
]
|
|
})");
|
|
REQUIRE(doc.valid());
|
|
Value effects = doc.root().get("effects");
|
|
REQUIRE(effects.isArray());
|
|
CHECK_EQ(effects.size(), 2u);
|
|
|
|
Value first = effects.at(0);
|
|
CHECK_EQ(first.getString("id"), "burn");
|
|
Value mods = first.get("modifiers");
|
|
REQUIRE(mods.isArray());
|
|
CHECK_EQ(mods.at(0).getString("attribute"), "hp");
|
|
CHECK_EQ(mods.at(0).getDouble("value"), -3.0);
|
|
|
|
// Absent nested arrays report empty rather than failing.
|
|
CHECK_EQ(effects.at(1).get("modifiers").size(), 0u);
|
|
}
|
|
|
|
TEST_CASE("json.keysPreserveDocumentOrder") {
|
|
Document doc = Document::parse(R"({"z": 1, "a": 2, "m": 3})");
|
|
REQUIRE(doc.valid());
|
|
auto keys = doc.root().keys();
|
|
REQUIRE(keys.size() == 3u);
|
|
CHECK_EQ(keys[0], "z");
|
|
CHECK_EQ(keys[1], "a");
|
|
CHECK_EQ(keys[2], "m");
|
|
}
|
|
|
|
TEST_CASE("json.rootArray") {
|
|
Document doc = Document::parse(R"([{"id": "a"}, {"id": "b"}])");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
CHECK(r.isArray());
|
|
CHECK(!r.isObject());
|
|
REQUIRE(r.size() == 2u);
|
|
CHECK_EQ(r.at(1).getString("id"), "b");
|
|
}
|
|
|
|
TEST_CASE("json.escapesAndUnicode") {
|
|
Document doc = Document::parse(
|
|
R"({"esc": "a\"b\\c\/d\be\ff\ng\rh\ti", "bmp": "\u5f00\u59cb", "astral": "\ud83d\ude00"})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
|
|
CHECK_EQ(r.getString("esc"), "a\"b\\c/d\be\ff\ng\rh\ti");
|
|
// BMP escapes decode to UTF-8.
|
|
CHECK_EQ(r.getString("bmp"), "\xe5\xbc\x80\xe5\xa7\x8b");
|
|
// Surrogate pairs combine into one code point (U+1F600).
|
|
CHECK_EQ(r.getString("astral"), "\xf0\x9f\x98\x80");
|
|
}
|
|
|
|
TEST_CASE("json.malformedInputReportsError") {
|
|
struct Case {
|
|
const char* text;
|
|
} bad[] = {
|
|
{"{"},
|
|
{"{\"a\":}"},
|
|
{"[1,]"},
|
|
{"{\"a\" 1}"},
|
|
{"tru"},
|
|
{"{\"a\":1} x"},
|
|
{"\"unterminated"},
|
|
{""},
|
|
{"+1"},
|
|
{"01"},
|
|
{"1."},
|
|
{"1e"},
|
|
{"1e+"},
|
|
{"1e9999"},
|
|
{"\"raw\nnewline\""},
|
|
{"\"\\ud800\""},
|
|
{"\"\\udc00\""},
|
|
{"\"\\ud800\\u0041\""},
|
|
};
|
|
for (const auto& c : bad) {
|
|
std::string err;
|
|
Document doc = Document::parse(c.text, &err);
|
|
CHECK(!doc.valid());
|
|
CHECK(!err.empty());
|
|
// An invalid document still answers safely.
|
|
CHECK(!doc.root());
|
|
CHECK_EQ(doc.root().getInt("anything", 4), 4);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("json.nestingDepthIsBounded") {
|
|
std::string atLimit(256, '[');
|
|
atLimit += '0';
|
|
atLimit.append(256, ']');
|
|
CHECK(Document::parse(atLimit).valid());
|
|
|
|
std::string overLimit(257, '[');
|
|
overLimit += '0';
|
|
overLimit.append(257, ']');
|
|
std::string error;
|
|
CHECK(!Document::parse(overLimit, &error).valid());
|
|
CHECK(!error.empty());
|
|
}
|
|
|
|
TEST_CASE("json.emptyContainers") {
|
|
Document doc = Document::parse(R"({"o": {}, "a": []})");
|
|
REQUIRE(doc.valid());
|
|
Value r = doc.root();
|
|
CHECK(r.get("o").isObject());
|
|
CHECK_EQ(r.get("o").size(), 0u);
|
|
CHECK(r.get("a").isArray());
|
|
CHECK_EQ(r.get("a").size(), 0u);
|
|
CHECK(r.get("o").keys().empty());
|
|
}
|
|
|
|
TEST_CASE("json.documentIsMovable") {
|
|
Document a = Document::parse(R"({"v": 11})");
|
|
REQUIRE(a.valid());
|
|
Document b = std::move(a);
|
|
CHECK(b.valid());
|
|
CHECK_EQ(b.root().getInt("v"), 11);
|
|
}
|