Source
stdlib/tests/parsers_json_dom_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// In-process unit tests for the COMPILED JSON DOM parser/serializer (parsers/json/json.cpp).4
// json.cpp is compiled DIRECTLY into this test binary (see CMakeLists) so its coverage is5
// measured here — the tests below exercise both parse paths (pooled zero-copy views via6
// Parser::parse, self-contained owning Documents via Parser::parse_owning / the free parse()),7
// both Validate modes (checked + trusted), the full error battery of the validating grammar,8
// the depth cap, the iterative serializer (dump: all node kinds, string re-escaping, deep9
// nesting), and to_view. Complements parsers_json_test.cpp, which covers the header-only10
// surface (scanners, builders, tokens, the typed struct reader).12
#include <string>13
#include <string_view>14
#include <variant>15
#include <vector>17
#include <gtest/gtest.h>19
#include "json/json.hpp"21
namespace json = cheatah::parsers::json;23
namespace {25
// Parse with the free (owning, validating) parse() and require success.26
json::Document parse_ok(std::string_view text) {27
bool ok = false;28
json::Document d = json::parse(text, &ok);29
EXPECT_TRUE(ok) << "rejected: " << text;30
return d;31
}33
TEST(ParsersJsonDom, ParsesEveryScalarKind) {34
EXPECT_TRUE(std::holds_alternative<json::Boolean>(parse_ok("true").variant()));35
EXPECT_TRUE(std::get<json::Boolean>(parse_ok("true").variant()).value());36
EXPECT_FALSE(std::get<json::Boolean>(parse_ok("false").variant()).value());37
EXPECT_TRUE(std::holds_alternative<json::Null>(parse_ok("null").variant()));38
EXPECT_DOUBLE_EQ(std::get<json::Number>(parse_ok("-2.5e2").variant()).value(), -250.0);39
// Leading/trailing whitespace is fine; the ok pointer may be omitted entirely.40
EXPECT_DOUBLE_EQ(std::get<json::Number>(json::parse(" 42 ").variant()).value(), 42.0);41
// A plain string becomes an OWNED string in the free (owning) parse.42
const json::Document s = parse_ok(R"("hi")");43
ASSERT_TRUE(std::holds_alternative<json::String<std::string>>(s.variant()));44
EXPECT_EQ(json::to_view(s), "hi");45
// An escaped string is decoded (always into owned storage).46
EXPECT_EQ(json::to_view(parse_ok(R"("a\tb\u00e9")")), "a\tb\xC3\xA9");47
}49
TEST(ParsersJsonDom, ToViewReadsBothBackingsAndRejectsNonStrings) {50
// Owned backing (from the owning parse) — checked above too, via a fresh node here.51
json::Node owned{json::String{std::string("own")}};52
EXPECT_EQ(json::to_view(owned), "own");53
// Viewing backing (as the pooled parse produces).54
json::Node view{json::String{std::string_view("view")}};55
EXPECT_EQ(json::to_view(view), "view");56
// Not a string at all: an empty view, not a crash.57
json::Node num{json::Number{1.0}};58
EXPECT_EQ(json::to_view(num), "");59
}61
TEST(ParsersJsonDom, OwningContainersAndDumpRoundTrip) {62
const std::string src =63
R"({"a":[1,true,null,"s"],"b":{"nested":{"x":-1.5}},"empty_a":[],"empty_o":{},"t":"q\"z"})";64
const json::Document d = parse_ok(src);65
ASSERT_TRUE(std::holds_alternative<json::OwnedObject>(d.variant()));66
const auto& members = std::get<json::OwnedObject>(d.variant()).value();67
ASSERT_EQ(members.size(), 5u);68
EXPECT_EQ(json::to_view(members[0].first), "a");69
ASSERT_TRUE(std::holds_alternative<json::OwnedArray>(members[0].second.variant()));70
EXPECT_EQ(std::get<json::OwnedArray>(members[0].second.variant()).value().size(), 4u);71
// Compact serialization round-trips byte-for-byte (keys ordered, strings re-escaped).72
const std::string compact =73
R"({"a":[1,true,null,"s"],"b":{"nested":{"x":-1.5}},"empty_a":[],"empty_o":{},"t":"q\"z"})";74
EXPECT_EQ(json::dump(d), compact);75
// The appending overload appends — it must not clobber what is already in the buffer.76
std::string out = "prefix:";77
json::dump(d, out);78
EXPECT_EQ(out, "prefix:" + compact);79
}81
TEST(ParsersJsonDom, DumpReescapesStringsAndFormatsNumbers) {82
// Every escape arm of dump_string: quote, backslash, \b \f \n \r \t, and a control83
// byte below 0x20 that has no short form (-> \u0001).84
const json::Document s = parse_ok(R"(["\"\\\b\f\n\r\t\u0001"])");85
EXPECT_EQ(json::dump(s), R"(["\"\\\b\f\n\r\t\u0001"])");86
// Numbers use shortest to_chars form: integral values drop the '.0'.87
EXPECT_EQ(json::dump(parse_ok("[3,-2.5,0.125]")), "[3,-2.5,0.125]");88
EXPECT_EQ(json::dump(parse_ok("true")), "true"); // scalar root, no container frame89
EXPECT_EQ(json::dump(parse_ok("false")), "false");90
EXPECT_EQ(json::dump(parse_ok("null")), "null");91
}93
TEST(ParsersJsonDom, PooledParserYieldsViewsIntoSource) {94
json::Parser p;95
const std::string src = R"({"key":[10,"plain","esc\nq"]})";96
bool ok = false;97
json::Document d = p.parse(src, &ok);98
ASSERT_TRUE(ok);99
ASSERT_TRUE(std::holds_alternative<json::ObjectView>(d.variant()));100
const auto members = std::get<json::ObjectView>(d.variant()).value();101
ASSERT_EQ(members.size(), 1u);102
// The key and the plain string are zero-copy VIEWS into `src`.103
ASSERT_TRUE(104
std::holds_alternative<json::String<std::string_view>>(members[0].first.variant()));105
EXPECT_EQ(json::to_view(members[0].first), "key");106
ASSERT_TRUE(std::holds_alternative<json::ArrayView>(members[0].second.variant()));107
const auto elems = std::get<json::ArrayView>(members[0].second.variant()).value();108
ASSERT_EQ(elems.size(), 3u);109
EXPECT_DOUBLE_EQ(std::get<json::Number>(elems[0].variant()).value(), 10.0);110
const auto& plain = elems[1].variant();111
ASSERT_TRUE(std::holds_alternative<json::String<std::string_view>>(plain));112
const std::string_view pv = std::get<json::String<std::string_view>>(plain).value();113
EXPECT_GE(pv.data(), src.data()); // really points into the source114
EXPECT_LE(pv.data() + pv.size(), src.data() + src.size());115
// The ESCAPED string was decoded into owned storage even on the pooled path.116
ASSERT_TRUE(std::holds_alternative<json::String<std::string>>(elems[2].variant()));117
EXPECT_EQ(json::to_view(elems[2]), "esc\nq");118
// The pooled Document serializes like any other, and the Parser is reusable: a second119
// parse invalidates-and-replaces, and Parser::dump reuses its internal buffer.120
EXPECT_EQ(std::string(p.dump(d)), src);121
json::Document d2 = p.parse("[1,2]", &ok);122
ASSERT_TRUE(ok);123
EXPECT_EQ(std::string(p.dump(d2)), "[1,2]"); // buffer reused across dumps124
}126
TEST(ParsersJsonDom, OwningParseOutlivesItsSource) {127
json::Parser p;128
json::Document d;129
{130
std::string temp = R"({"k":"value with \u20ac","n":[false]})";131
bool ok = false;132
d = p.parse_owning(temp, &ok);133
ASSERT_TRUE(ok);134
temp.assign(temp.size(), 'X'); // scribble over the source: owned nodes must not care135
}136
ASSERT_TRUE(std::holds_alternative<json::OwnedObject>(d.variant()));137
const auto& members = std::get<json::OwnedObject>(d.variant()).value();138
ASSERT_EQ(members.size(), 2u);139
EXPECT_EQ(json::to_view(members[0].first), "k");140
EXPECT_EQ(json::to_view(members[0].second), "value with \xE2\x82\xAC");141
EXPECT_EQ(json::dump(d), R"({"k":"value with €","n":[false]})");142
}144
TEST(ParsersJsonDom, ValidatingParseRejectsMalformed) {145
json::Parser p;146
// Every rejection must ALSO yield a JSON-null document, not a partial tree.147
const char* bad[] = {148
"", // no value at all149
" ", // only whitespace150
"{1:2}", // object key is not a string151
"{\"a\"", // key then end-of-input (no ':')152
"{\"a\" 1}", // missing ':' between key and value153
"{\"a\":}", // missing value154
"{\"ab", // unterminated key string155
"{\"\\uZZ\":1}", // key with a malformed \u escape156
"[", // unterminated right after '['157
"{", // unterminated right after '{'158
"[1", // value then end-of-input (no ',' or ']')159
"[1,", // dangling ',' then end-of-input160
"[1;2]", // ';' is neither ',' nor ']'161
"[1}", // wrong closer for an array162
"{\"a\":1]", // wrong closer for an object163
"tru", // truncated literal true164
"fals", // truncated literal false165
"nul", // truncated literal null166
"x", // not a value at all167
"[\"\\uZZZZ\"]", // string value with a malformed escape168
"\"no end", // unterminated string value169
"1 x", // trailing junk after a complete value170
"[]]", // trailing junk after a complete container171
};172
for (const char* text : bad) {173
bool ok = true;174
json::Document owning = json::parse(text, &ok);175
EXPECT_FALSE(ok) << "free parse accepted: " << text;176
EXPECT_TRUE(std::holds_alternative<json::Null>(owning.variant())) << text;177
ok = true;178
json::Document pooled = p.parse(text, &ok);179
EXPECT_FALSE(ok) << "pooled parse accepted: " << text;180
EXPECT_TRUE(std::holds_alternative<json::Null>(pooled.variant())) << text;181
}182
// Rejection with ok == nullptr must not crash (the result is still null).183
EXPECT_TRUE(std::holds_alternative<json::Null>(json::parse("[oops").variant()));184
}186
TEST(ParsersJsonDom, DepthCapAndDeepDumpRoundTrip) {187
// 1000 levels of array nesting is accepted; the ITERATIVE dump round-trips it without188
// touching the C++ call stack.189
const std::string deep_ok = std::string(1000, '[') + "7" + std::string(1000, ']');190
bool ok = false;191
const json::Document d = json::parse(deep_ok, &ok);192
ASSERT_TRUE(ok);193
EXPECT_EQ(json::dump(d), deep_ok);194
// 1001 levels breaches kMaxParseDepth: rejected, result null — for arrays and objects.195
const std::string deep_bad = std::string(1001, '[') + "7" + std::string(1001, ']');196
ok = true;197
EXPECT_TRUE(std::holds_alternative<json::Null>(json::parse(deep_bad, &ok).variant()));198
EXPECT_FALSE(ok);199
std::string deep_obj;200
for (int i = 0; i < 1001; ++i) deep_obj += "{\"k\":";201
ok = true;202
(void)json::parse(deep_obj + "1", &ok);203
EXPECT_FALSE(ok);204
}206
TEST(ParsersJsonDom, TrustedUncheckedParseMatchesValidated) {207
// Validate=false strips every check at compile time; on WELL-FORMED input all four208
// unchecked entry points must produce the same document as the validating ones.209
const std::string src = R"({"a":[1,"two",{"b":null}],"c":true,"d":"e\tf"})";210
const std::string expect = json::dump(parse_ok(src));211
EXPECT_EQ(json::dump(json::parse<false>(src)), expect); // free, owning212
json::Parser p;213
EXPECT_EQ(std::string(p.dump(p.parse<false>(src))), expect); // pooled views214
EXPECT_EQ(json::dump(p.parse_owning<false>(src)), expect); // reusable owning215
// Scalar through the unchecked path too (no container frames at all).216
EXPECT_EQ(json::dump(json::parse<false>("12.5")), "12.5");217
}219
} // namespace