From 1d44575fcd86df1e914aa59dfa1f2f3da43a8ae5 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 20:32:20 +0200 Subject: [PATCH] test: cover testJson --- tests/testJson.cpp | 160 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/tests/testJson.cpp b/tests/testJson.cpp index f61f9fe..2c4cc69 100644 --- a/tests/testJson.cpp +++ b/tests/testJson.cpp @@ -143,3 +143,163 @@ VF_TEST(brokenSurrogatesAndUnknownEscapesAreRefused) { } } +VF_TEST(everySyntaxErrorNamesItsOwnLineAndColumn) { + const BadDocument cases[] = { + {"", 1, 1, "end of input"}, + {" ", 1, 4, "end of input"}, + {"{\"a\": 1,}", 1, 9, "trailing comma"}, + {"[1, 2,]", 1, 7, "trailing comma"}, + {"{\"a\" 1}", 1, 6, "expected ':'"}, + {"{'a':1}", 1, 2, "quoted member name"}, + {"{\"a\": \"x\" \"b\": 1}", 1, 11, "expected ',' or '}'"}, + {"[1 2]", 1, 4, "expected ',' or ']'"}, + {"01", 1, 2, "leading zero"}, + {"[1.]", 1, 4, "after the decimal point"}, + {"[1e]", 1, 4, "exponent"}, + {"[-]", 1, 3, "digit after the sign"}, + {"\"abc", 1, 5, "end of input inside a string"}, + {"{}garbage", 1, 3, "trailing character"}, + {"[\"a\tb\"]", 1, 4, "control character"}, + {"[\"\xC3\"]", 1, 3, "malformed UTF-8"}, + {"{\n \"a\": [1, 2],\n \"b\": nul\n}", 3, 8, "literal"}, + {"[\n 1,\n 2,\n]", 4, 1, "trailing comma"}, + {"{\n \"a\": @\n}", 2, 7, "unexpected character"}, + }; + for (const BadDocument& bad : cases) { + JsonParseResult parsed = parseJson(bad.text); + CHECK(!parsed.ok); + CHECK_EQ(parsed.error.line, bad.line); + CHECK_EQ(parsed.error.column, bad.column); + bool mentions = parsed.error.message.find(bad.fragment) != std::string::npos; + CHECK(mentions); + if (!mentions || parsed.error.line != bad.line || parsed.error.column != bad.column) + std::printf(" document [%s] gave %s\n", bad.text, parsed.error.text().c_str()); + } +} + +VF_TEST(deepNestingIsReportedAsAnErrorInsteadOfOverflowingTheStack) { + std::string tooDeep = repeated("[", 200) + repeated("]", 200); + JsonParseResult parsed = parseJson(tooDeep); + CHECK(!parsed.ok); + CHECK(parsed.error.message.find("nesting deeper") != std::string::npos); + CHECK_EQ(parsed.error.line, 1); + CHECK_EQ(parsed.error.column, jsonDefaultMaxDepth + 1); + + // An unterminated avalanche must stop at the same limit, not at the end. + JsonParseResult runaway = parseJson(repeated("[", 20000)); + CHECK(!runaway.ok); + CHECK_EQ(runaway.error.column, jsonDefaultMaxDepth + 1); + + JsonParseResult objects = parseJson(repeated("{\"a\":", 100) + "1" + repeated("}", 100)); + CHECK(!objects.ok); + CHECK(objects.error.message.find("nesting deeper") != std::string::npos); + + // Exactly at the limit is still a document. + std::string atLimit = repeated("[", jsonDefaultMaxDepth) + repeated("]", jsonDefaultMaxDepth); + CHECK(parseJson(atLimit).ok); + CHECK(!parseJson("[" + atLimit + "]").ok); + + CHECK(parseJson("[[[1]]]", 3).ok); + CHECK(!parseJson("[[[[1]]]]", 3).ok); +} + +VF_TEST(numbersKeepIntegerExactnessAndFractionsSurviveTheRoundTrip) { + JsonValue whole = mustParse("9223372036854775807"); + CHECK(whole.isInteger()); + CHECK_EQ(whole.intValue(), 9223372036854775807ll); + CHECK_EQ(JsonWriter::compact(whole), std::string("9223372036854775807")); + + JsonValue negative = mustParse("-9223372036854775808"); + CHECK(negative.isInteger()); + CHECK_EQ(negative.intValue(), -9223372036854775807ll - 1ll); + + // Past the 64 bit range the value degrades to a double rather than wrapping. + JsonValue huge = mustParse("99999999999999999999"); + CHECK(!huge.isInteger()); + CHECK(huge.numberValue() > 9.9e19); + + JsonValue fraction = mustParse("[0.1,2.5,-1.5e-7,1e308]"); + CHECK_EQ(JsonWriter::compact(fraction), std::string("[0.1,2.5,-1.5e-07,1e+308]")); + CHECK(parseJson(JsonWriter::compact(fraction)).root == fraction); + + CHECK(!parseJson("1e999").ok); + CHECK(parseJson("1e999").error.message.find("representable") != std::string::npos); + + // An exponent form is a double, and stays one: writing it as a bare 100 + // would read back as an integer and print differently on the next pass. + JsonValue exponent = mustParse("1e2"); + CHECK_EQ(exponent.numberValue(), 100.0); + CHECK_EQ(exponent.intValue(), 100); + CHECK(!exponent.isInteger()); + CHECK_EQ(JsonWriter::compact(exponent), std::string("100.0")); + CHECK_EQ(JsonWriter::compact(mustParse("-0.0")), std::string("-0.0")); + CHECK_EQ(JsonWriter::compact(mustParse("-0")), std::string("0")); +} + +VF_TEST(indentedFormLaysOutOneMemberPerLine) { + JsonValue root = mustParse("{\"a\":[1,2],\"b\":{},\"c\":[]}"); + const std::string expected = + "{\n" + " \"a\": [\n" + " 1,\n" + " 2\n" + " ],\n" + " \"b\": {},\n" + " \"c\": []\n" + "}"; + CHECK_EQ(JsonWriter::indented(root, 2), expected); + CHECK_EQ(JsonWriter::compact(root), std::string("{\"a\":[1,2],\"b\":{},\"c\":[]}")); + CHECK(parseJson(expected).root == root); + + JsonWriter wide; + wide.indentWidth = 4; + CHECK(wide.write(root).find("\n \"a\": [") != std::string::npos); +} + +VF_TEST(objectMembersKeepOrderAndTheLastDuplicateKeyWins) { + JsonValue root = mustParse("{\"z\":1,\"a\":2,\"z\":3}"); + CHECK_EQ(root.size(), static_cast(2)); + CHECK_EQ(root.members()[0].first, std::string("z")); + CHECK_EQ(root.members()[1].first, std::string("a")); + CHECK_EQ(root.intOr("z", 0), 3); + CHECK_EQ(JsonWriter::compact(root), std::string("{\"z\":3,\"a\":2}")); + + JsonValue built = JsonValue::makeObject(); + built.set("z", JsonValue::makeInt(3)); + built.set("a", JsonValue::makeInt(2)); + CHECK(built == root); + + JsonValue reordered = JsonValue::makeObject(); + reordered.set("a", JsonValue::makeInt(2)); + reordered.set("z", JsonValue::makeInt(3)); + CHECK(reordered != root); +} + +VF_TEST(jsonPointerWalksIntoObjectsAndArrays) { + JsonValue root = mustParse( + "{\"foo\":[\"bar\",\"baz\"],\"\":0,\"a/b\":1,\"c%d\":2,\"e^f\":3,\"m~n\":8," + "\"nested\":{\"list\":[{\"leaf\":true}]}}"); + + CHECK(JsonPointer::find(root, "") == &root); + const JsonValue* bar = JsonPointer::find(root, "/foo/0"); + CHECK(bar != nullptr); + CHECK_EQ(bar->stringValue(), std::string("bar")); + CHECK_EQ(JsonPointer::find(root, "/foo/1")->stringValue(), std::string("baz")); + CHECK_EQ(JsonPointer::find(root, "/")->intValue(), 0); + CHECK_EQ(JsonPointer::find(root, "/a~1b")->intValue(), 1); + CHECK_EQ(JsonPointer::find(root, "/m~0n")->intValue(), 8); + CHECK(JsonPointer::find(root, "/nested/list/0/leaf")->boolValue()); + + CHECK(JsonPointer::find(root, "/foo/2") == nullptr); + CHECK(JsonPointer::find(root, "/foo/-") == nullptr); + CHECK(JsonPointer::find(root, "/foo/01") == nullptr); + CHECK(JsonPointer::find(root, "/missing") == nullptr); + CHECK(JsonPointer::find(root, "/foo/0/deeper") == nullptr); + + JsonPointer path("/nested/list/0/leaf"); + CHECK(path.valid()); + CHECK_EQ(path.tokens().size(), static_cast(4)); + CHECK_EQ(path.text(), std::string("/nested/list/0/leaf")); + CHECK_EQ(JsonPointer("/a~1b").text(), std::string("/a~1b")); + CHECK_EQ(JsonPointer("/m~0n").text(), std::string("/m~0n")); +