cheatah
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 is
5// measured here — the tests below exercise both parse paths (pooled zero-copy views via
6// 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, deep
9// nesting), and to_view. Complements parsers_json_test.cpp, which covers the header-only
10// 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"
21namespace json = cheatah::parsers::json;
23namespace {
25// Parse with the free (owning, validating) parse() and require success.
26json::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;
33TEST(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");
49TEST(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), "");
61TEST(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);
81TEST(ParsersJsonDom, DumpReescapesStringsAndFormatsNumbers) {
82 // Every escape arm of dump_string: quote, backslash, \b \f \n \r \t, and a control
83 // 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 frame
89 EXPECT_EQ(json::dump(parse_ok("false")), "false");
90 EXPECT_EQ(json::dump(parse_ok("null")), "null");
93TEST(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 source
114 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 second
119 // 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 dumps
126TEST(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 care
135 }
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]})");
144TEST(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 all
149 " ", // only whitespace
150 "{1:2}", // object key is not a string
151 "{\"a\"", // key then end-of-input (no ':')
152 "{\"a\" 1}", // missing ':' between key and value
153 "{\"a\":}", // missing value
154 "{\"ab", // unterminated key string
155 "{\"\\uZZ\":1}", // key with a malformed \u escape
156 "[", // unterminated right after '['
157 "{", // unterminated right after '{'
158 "[1", // value then end-of-input (no ',' or ']')
159 "[1,", // dangling ',' then end-of-input
160 "[1;2]", // ';' is neither ',' nor ']'
161 "[1}", // wrong closer for an array
162 "{\"a\":1]", // wrong closer for an object
163 "tru", // truncated literal true
164 "fals", // truncated literal false
165 "nul", // truncated literal null
166 "x", // not a value at all
167 "[\"\\uZZZZ\"]", // string value with a malformed escape
168 "\"no end", // unterminated string value
169 "1 x", // trailing junk after a complete value
170 "[]]", // trailing junk after a complete container
171 };
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()));
186TEST(ParsersJsonDom, DepthCapAndDeepDumpRoundTrip) {
187 // 1000 levels of array nesting is accepted; the ITERATIVE dump round-trips it without
188 // 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);
206TEST(ParsersJsonDom, TrustedUncheckedParseMatchesValidated) {
207 // Validate=false strips every check at compile time; on WELL-FORMED input all four
208 // 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, owning
212 json::Parser p;
213 EXPECT_EQ(std::string(p.dump(p.parse<false>(src))), expect); // pooled views
214 EXPECT_EQ(json::dump(p.parse_owning<false>(src)), expect); // reusable owning
215 // Scalar through the unchecked path too (no container frames at all).
216 EXPECT_EQ(json::dump(json::parse<false>("12.5")), "12.5");
219} // namespace