Q-W1 pt1: extract core/json (json::Reader/Writer), collapse wire Cursor family into core/wire, shared readFileBytes — five JSON decoders and three cursor copies deleted, byte-identical formats, 59/59 green
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
// Standalone tests for reasampler::json — no REAPER, no framework. The ONE
|
||||
// lexical JSON layer (Q-W1) behind bank_model / bank_book / view_mode_model /
|
||||
// owned_manifest / tail_control. The consumers' own suites prove the domain
|
||||
// grammars; this suite pins the LEXICAL contract — the escape set, the number
|
||||
// renderings (byte-exact), the parse tolerances, and the reject paths — so a
|
||||
// change here is caught before it silently shifts five persisted-blob formats.
|
||||
//
|
||||
// NOTE: json::Reader BORROWS its input string, so every test binds a named
|
||||
// std::string first — never a temporary.
|
||||
|
||||
#include "../src/core/json/json.h"
|
||||
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace reasampler;
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||
|
||||
// Convenience: parse helpers over a named buffer per call site.
|
||||
static bool intFrom(const std::string& s, int& v) { json::Reader r(s); return r.parseInt(v); }
|
||||
static bool int64From(const std::string& s, std::int64_t& v) { json::Reader r(s); return r.parseInt64(v); }
|
||||
static bool doubleFrom(const std::string& s, double& v) { json::Reader r(s); return r.parseDouble(v); }
|
||||
static bool boolFrom(const std::string& s, bool& v) { json::Reader r(s); return r.parseBool(v); }
|
||||
static bool stringFrom(const std::string& s, std::string& v) { json::Reader r(s); return r.parseString(v); }
|
||||
|
||||
// --- emit: writeEscaped -------------------------------------------------------
|
||||
|
||||
static void testEscapeExactBytes() {
|
||||
// The seven short escapes + \u00XX for remaining control chars, verbatim
|
||||
// pass-through otherwise. Byte-exact: this is the persisted-blob format.
|
||||
std::string out;
|
||||
json::writeEscaped(out, "a\"b\\c\n\t\x01z");
|
||||
CHECK(out == "\"a\\\"b\\\\c\\n\\t\\u0001z\"");
|
||||
}
|
||||
|
||||
static void testEscapeUtf8PassesThrough() {
|
||||
// Multi-byte UTF-8 passes through verbatim; only C0 controls are \u-escaped.
|
||||
std::string out;
|
||||
json::writeEscaped(out, "gr\xC3\xBC n"); // "grü n"
|
||||
CHECK(out == "\"gr\xC3\xBC n\"");
|
||||
}
|
||||
|
||||
// --- emit: numToStr -----------------------------------------------------------
|
||||
|
||||
static void testNumToStrIntForms() {
|
||||
CHECK(json::numToStr(0) == "0");
|
||||
CHECK(json::numToStr(-7) == "-7");
|
||||
CHECK(json::numToStr(INT_MAX) == "2147483647");
|
||||
CHECK(json::numToStr(static_cast<std::int64_t>(1) << 40) == "1099511627776");
|
||||
CHECK(json::numToStr(2000.0) == "2000"); // %.17g drops the trailing .0
|
||||
CHECK(json::numToStr(0.5) == "0.5");
|
||||
}
|
||||
|
||||
static void testDoubleRoundTripsBitForBit() {
|
||||
// %.17g is the shortest form that round-trips every IEEE-754 double.
|
||||
const double v = 3141.592653589793;
|
||||
double back = 0.0;
|
||||
CHECK(doubleFrom(json::numToStr(v), back));
|
||||
CHECK(back == v);
|
||||
}
|
||||
|
||||
// --- emit: Writer object grammar ----------------------------------------------
|
||||
|
||||
static void testWriterEmitsExactObjectBytes() {
|
||||
std::string out;
|
||||
{
|
||||
json::Writer w(out);
|
||||
w.keyRaw("a", json::numToStr(1));
|
||||
w.keyStr("b", "x\"y");
|
||||
w.keyBegin("c");
|
||||
{
|
||||
json::Writer nested(out);
|
||||
nested.keyRaw("d", json::numToStr(2.5));
|
||||
}
|
||||
w.keyBegin("e");
|
||||
json::writeStringArray(out, {"p", "q"});
|
||||
w.keyBegin("f");
|
||||
json::writeIntArray(out, {1, 2});
|
||||
}
|
||||
CHECK(out == "{\"a\":1,\"b\":\"x\\\"y\",\"c\":{\"d\":2.5},"
|
||||
"\"e\":[\"p\",\"q\"],\"f\":[1,2]}");
|
||||
}
|
||||
|
||||
static void testEmptyArraysEmitBrackets() {
|
||||
std::string s, i;
|
||||
json::writeStringArray(s, {});
|
||||
json::writeIntArray(i, {});
|
||||
CHECK(s == "[]");
|
||||
CHECK(i == "[]");
|
||||
}
|
||||
|
||||
// --- Reader: strings ----------------------------------------------------------
|
||||
|
||||
static void testParseStringEscapes() {
|
||||
std::string out;
|
||||
CHECK(stringFrom(" \"a\\\"b\\\\c\\n\\u0041\"", out));
|
||||
CHECK(out == "a\"b\\c\nA");
|
||||
}
|
||||
|
||||
static void testParseStringSurrogatePairToUtf8() {
|
||||
// \uD83D\uDE00 (grinning face) -> F0 9F 98 80.
|
||||
std::string out;
|
||||
CHECK(stringFrom("\"\\ud83d\\ude00\"", out));
|
||||
CHECK(out == "\xF0\x9F\x98\x80");
|
||||
}
|
||||
|
||||
static void testParseStringRejectsMalformed() {
|
||||
std::string out;
|
||||
CHECK(!stringFrom("\"unterminated", out));
|
||||
CHECK(!stringFrom("\"bad\\qescape\"", out));
|
||||
CHECK(!stringFrom("\"\\ud800 alone\"", out)); // unpaired high surrogate
|
||||
CHECK(!stringFrom("\"\\udc00\"", out)); // unpaired low surrogate
|
||||
CHECK(!stringFrom("noquote", out));
|
||||
}
|
||||
|
||||
// --- Reader: numbers ----------------------------------------------------------
|
||||
|
||||
static void testParseIntAcceptsAndRejects() {
|
||||
int v = 0;
|
||||
CHECK(intFrom("42,", v)); CHECK(v == 42);
|
||||
CHECK(intFrom("-7}", v)); CHECK(v == -7);
|
||||
CHECK(intFrom("2147483647]", v)); CHECK(v == INT_MAX);
|
||||
// Out of int range is REJECTED (the unified guard every consumer now shares).
|
||||
CHECK(!intFrom("2147483648,", v));
|
||||
CHECK(!intFrom("1.5,", v));
|
||||
CHECK(!intFrom("x,", v));
|
||||
CHECK(!intFrom("", v));
|
||||
}
|
||||
|
||||
static void testParseInt64RangeAndReject() {
|
||||
std::int64_t v = 0;
|
||||
CHECK(int64From("9223372036854775807,", v));
|
||||
CHECK(v == 9223372036854775807LL);
|
||||
CHECK(!int64From("9223372036854775808,", v)); // ERANGE -> reject
|
||||
}
|
||||
|
||||
static void testParseDoubleRejectsRangeAndGarbage() {
|
||||
double v = 0;
|
||||
CHECK(!doubleFrom("1e999,", v)); // ERANGE
|
||||
CHECK(!doubleFrom("1.5abc,", v)); // trailing bytes
|
||||
CHECK(doubleFrom("2.5}", v)); CHECK(v == 2.5);
|
||||
}
|
||||
|
||||
static void testParseBool() {
|
||||
bool v = false;
|
||||
CHECK(boolFrom("true,", v)); CHECK(v);
|
||||
CHECK(boolFrom("false]", v)); CHECK(!v);
|
||||
CHECK(!boolFrom("TRUE,", v));
|
||||
}
|
||||
|
||||
// --- Reader: structure --------------------------------------------------------
|
||||
|
||||
static void testExpectNullOr() {
|
||||
{
|
||||
const std::string s = "null,";
|
||||
json::Reader r(s);
|
||||
bool wasNull = false;
|
||||
CHECK(r.expectNullOr(wasNull)); CHECK(wasNull); CHECK(r.consume(','));
|
||||
}
|
||||
{
|
||||
const std::string s = "\"x\"";
|
||||
json::Reader r(s);
|
||||
bool wasNull = true;
|
||||
std::string v;
|
||||
CHECK(r.expectNullOr(wasNull)); CHECK(!wasNull);
|
||||
CHECK(r.parseString(v)); CHECK(v == "x");
|
||||
}
|
||||
{
|
||||
const std::string s;
|
||||
json::Reader r(s);
|
||||
bool wasNull = false;
|
||||
CHECK(!r.expectNullOr(wasNull));
|
||||
}
|
||||
}
|
||||
|
||||
static void testParseKeyConsumesColon() {
|
||||
const std::string s = " \"k\" : 1";
|
||||
json::Reader r(s);
|
||||
std::string k;
|
||||
int v = 0;
|
||||
CHECK(r.parseKey(k));
|
||||
CHECK(k == "k");
|
||||
CHECK(r.parseInt(v));
|
||||
CHECK(v == 1);
|
||||
}
|
||||
|
||||
static void testParseArraysAppend() {
|
||||
{
|
||||
const std::string s = "[\"a\",\"b\"]";
|
||||
json::Reader r(s);
|
||||
std::vector<std::string> v;
|
||||
CHECK(r.parseStringArray(v));
|
||||
CHECK(v.size() == 2 && v[0] == "a" && v[1] == "b");
|
||||
}
|
||||
{
|
||||
const std::string s = "[]";
|
||||
json::Reader r(s);
|
||||
std::vector<std::string> v;
|
||||
CHECK(r.parseStringArray(v)); CHECK(v.empty());
|
||||
}
|
||||
{
|
||||
const std::string s = "[1,2]"; // non-string element
|
||||
json::Reader r(s);
|
||||
std::vector<std::string> v;
|
||||
CHECK(!r.parseStringArray(v));
|
||||
}
|
||||
{
|
||||
const std::string s = "[1,2,3]";
|
||||
json::Reader r(s);
|
||||
std::vector<int> v;
|
||||
CHECK(r.parseIntArray(v));
|
||||
CHECK(v.size() == 3 && v[2] == 3);
|
||||
}
|
||||
{
|
||||
const std::string s = "[1,"; // truncated
|
||||
json::Reader r(s);
|
||||
std::vector<int> v;
|
||||
CHECK(!r.parseIntArray(v));
|
||||
}
|
||||
}
|
||||
|
||||
static void testSkipValueOverNestedShapes() {
|
||||
// Skips a nested object whose strings contain structural chars, then the
|
||||
// cursor sits exactly on the next separator.
|
||||
const std::string s = "{\"deep\":[\"}\",{\"x\":\"]\"}]},7";
|
||||
json::Reader r(s);
|
||||
CHECK(r.skipValue());
|
||||
CHECK(r.consume(','));
|
||||
int v = 0;
|
||||
CHECK(r.parseInt(v));
|
||||
CHECK(v == 7);
|
||||
}
|
||||
|
||||
static void testCaptureValueVerbatim() {
|
||||
const std::string s = " {\"a\":[1,\"{\"]} ,tail";
|
||||
json::Reader r(s);
|
||||
std::string raw;
|
||||
CHECK(r.captureValue(raw));
|
||||
CHECK(raw == "{\"a\":[1,\"{\"]}");
|
||||
CHECK(r.consume(','));
|
||||
}
|
||||
|
||||
static void testWriterOutputParsesBack() {
|
||||
// The emitted object is consumable by the Reader — the seam the five
|
||||
// consumers rely on (writer and reader agree on one dialect).
|
||||
std::string out;
|
||||
{
|
||||
json::Writer w(out);
|
||||
w.keyStr("name", "tab\there");
|
||||
w.keyRaw("n", json::numToStr(-3));
|
||||
}
|
||||
json::Reader r(out);
|
||||
CHECK(r.consume('{'));
|
||||
std::string k1, v1;
|
||||
CHECK(r.parseKey(k1) && k1 == "name");
|
||||
CHECK(r.parseString(v1) && v1 == "tab\there");
|
||||
CHECK(r.consume(','));
|
||||
std::string k2;
|
||||
int v2 = 0;
|
||||
CHECK(r.parseKey(k2) && k2 == "n");
|
||||
CHECK(r.parseInt(v2) && v2 == -3);
|
||||
CHECK(r.consume('}'));
|
||||
r.skipWs();
|
||||
CHECK(r.eof());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testEscapeExactBytes();
|
||||
testEscapeUtf8PassesThrough();
|
||||
testNumToStrIntForms();
|
||||
testDoubleRoundTripsBitForBit();
|
||||
testWriterEmitsExactObjectBytes();
|
||||
testEmptyArraysEmitBrackets();
|
||||
testParseStringEscapes();
|
||||
testParseStringSurrogatePairToUtf8();
|
||||
testParseStringRejectsMalformed();
|
||||
testParseIntAcceptsAndRejects();
|
||||
testParseInt64RangeAndReject();
|
||||
testParseDoubleRejectsRangeAndGarbage();
|
||||
testParseBool();
|
||||
testExpectNullOr();
|
||||
testParseKeyConsumesColon();
|
||||
testParseArraysAppend();
|
||||
testSkipValueOverNestedShapes();
|
||||
testCaptureValueVerbatim();
|
||||
testWriterOutputParsesBack();
|
||||
|
||||
if (g_fail == 0) std::printf("json: all tests passed\n");
|
||||
else std::printf("json: %d CHECK(s) FAILED\n", g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user