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:
2026-07-28 19:59:02 -04:00
parent 88e7765ee5
commit 67a41728f3
25 changed files with 1695 additions and 1696 deletions
+51
View File
@@ -0,0 +1,51 @@
// Standalone tests for reasampler::readFileBytes — no REAPER, no framework.
// The ONE whole-file loader (Q-W1, T2-03) shared by both artifacts. Exercises
// the three-way contract: exact bytes back, empty on a missing file, empty on
// an empty file. Uses a scratch file in the test's working directory.
#include "../src/core/util/file_bytes.h"
#include <cstdio>
#include <fstream>
#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)
static const char* kScratch = "file_bytes_scratch.bin";
static void testReadsExactBytesBack() {
// Binary content incl. NUL and 0xFF — the loader must be byte-transparent.
const std::vector<std::uint8_t> payload = {0x00, 0x01, 0xFF, 0x7E, 0x00, 0x0A};
{
std::ofstream f(kScratch, std::ios::binary | std::ios::trunc);
f.write(reinterpret_cast<const char*>(payload.data()),
static_cast<std::streamsize>(payload.size()));
}
CHECK(readFileBytes(kScratch) == payload);
std::remove(kScratch);
}
static void testMissingFileIsEmpty() {
CHECK(readFileBytes("no_such_file_anywhere.bin").empty());
}
static void testEmptyFileIsEmpty() {
{ std::ofstream f(kScratch, std::ios::binary | std::ios::trunc); }
CHECK(readFileBytes(kScratch).empty());
std::remove(kScratch);
}
int main() {
testReadsExactBytesBack();
testMissingFileIsEmpty();
testEmptyFileIsEmpty();
if (g_fail == 0) std::printf("file_bytes: all tests passed\n");
else std::printf("file_bytes: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}
+296
View File
@@ -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;
}
+15
View File
@@ -135,6 +135,20 @@ static void testRoundTripAuto() {
CHECK(back && settingsEqual(*back, s));
}
static void testSerializeByteIdentity() {
// Q-W1 structural-dedupe guard: the core/json-backed writer must emit the
// EXACT bytes the former snprintf writer produced ({"mode":%d,"manualMs":%.17g})
// and re-serializing a round-tripped setting must be byte-identical — the blob
// lives in the .rpp, so a byte shift would dirty every saved project.
TailSetting s; // None + 2000.0 default
CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}");
TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793;
const std::string json = serializeTailSetting(man);
auto back = deserializeTailSetting(json);
CHECK(back.has_value());
CHECK(back && serializeTailSetting(*back) == json); // stable second round-trip
}
static void testDeserializeEmptyIsDefault() {
// An absent/empty stored value (older project) -> nullopt, so the caller falls
// back to the default. This is the graceful-old-project path the brief requires.
@@ -164,6 +178,7 @@ int main() {
testRoundTripNoneDefault();
testRoundTripManualArbitraryMs();
testRoundTripAuto();
testSerializeByteIdentity();
testDeserializeEmptyIsDefault();
testDeserializeMalformedIsDefault();
+190
View File
@@ -0,0 +1,190 @@
// Standalone tests for reasampler::wire — no REAPER, no framework. The ONE
// length-prefixed ext-state wire codec (Q-W1, T2-01b) behind provenance /
// assignment_request / sample_usage / bank_sync. The consumers' own suites
// prove their record grammars round-trip; this suite pins the CODEC contract —
// byte-exact encode, the full hardening (length caps, overflow guards,
// subtraction-first bounds), and the fixed fieldInt range rejection.
//
// NOTE: wire::Cursor BORROWS its input string, so every helper takes a named /
// reference-bound std::string — the Cursor never outlives its buffer.
#include "../src/core/wire/wire.h"
#include <cstdio>
#include <string>
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)
// One length-prefixed field around `v` — the writer-side convention.
static std::string enc(const std::string& v) {
std::string out;
wire::putField(out, v);
return out;
}
// Single-field decode helpers (Cursor + buffer share the call's lifetime).
static bool fieldFrom(const std::string& s, std::string& out) {
wire::Cursor c(s);
return c.field(out);
}
static bool i64From(const std::string& s, std::int64_t& out) {
wire::Cursor c(s);
return c.fieldInt64(out);
}
static bool intFrom(const std::string& s, int& out) {
wire::Cursor c(s);
return c.fieldInt(out);
}
static bool sizeFrom(const std::string& s, std::size_t& out) {
wire::Cursor c(s);
return c.fieldSizeT(out);
}
static bool dblFrom(const std::string& s, double& out) {
wire::Cursor c(s);
return c.fieldDouble(out);
}
// --- putField: byte-exact encode ----------------------------------------------
static void testPutFieldExactBytes() {
std::string out;
wire::putField(out, "abc");
CHECK(out == "3:abc");
wire::putField(out, ""); // empty field is legal: "0:"
CHECK(out == "3:abc0:");
wire::putField(out, "a:b"); // ':' inside a value cannot shift the parse
CHECK(out == "3:abc0:3:a:b");
}
// --- Cursor: round-trip + literal ---------------------------------------------
static void testFieldRoundTripIncludingSeparators() {
std::string out = "magic";
wire::putField(out, "12:34"); // digits + colons in the value
wire::putField(out, "");
wire::putField(out, "tail");
wire::Cursor c(out);
std::string a, b, t;
CHECK(c.literal("magic"));
CHECK(c.field(a) && a == "12:34");
CHECK(c.field(b) && b.empty());
CHECK(c.field(t) && t == "tail");
CHECK(c.ok() && c.atEnd());
}
static void testLiteralMismatchFails() {
const std::string good = "rsprov1x";
const std::string wrong = "rsprov0x";
const std::string truncated = "rspro";
{ wire::Cursor c(good); CHECK(c.literal("rsprov1")); }
{ wire::Cursor c(wrong); CHECK(!c.literal("rsprov1")); CHECK(!c.ok()); }
{ wire::Cursor c(truncated); CHECK(!c.literal("rsprov1")); }
}
// --- Cursor: field hardening ---------------------------------------------------
static void testFieldRejectsMalformedLengths() {
std::string f;
CHECK(!fieldFrom("abc", f)); // no colon
CHECK(!fieldFrom(":x", f)); // empty length
CHECK(!fieldFrom("2x:ab", f)); // non-digit length
CHECK(!fieldFrom("9:ab", f)); // runs past end
// A 200-digit length cannot accumulate past SIZE_MAX (digit-run cap).
CHECK(!fieldFrom(std::string(200, '9') + ":x", f));
// Exactly-20-digit values: SIZE_MAX itself passes the accumulate but fails the
// bounds check; one past SIZE_MAX trips the overflow guard.
CHECK(!fieldFrom("18446744073709551615:x", f));
CHECK(!fieldFrom("18446744073709551616:x", f));
}
static void testFailureLatchesOk() {
// After one failed read every subsequent read fails too — the caller may
// check ok() once at the end (the "never a partial value" discipline).
const std::string s = "3:abc";
wire::Cursor c(s);
std::string f;
CHECK(!c.literal("nope"));
CHECK(!c.field(f));
CHECK(!c.ok());
}
// --- Cursor: fieldInt64 / fieldInt --------------------------------------------
static void testFieldInt64AcceptsAndRejects() {
std::int64_t v = 0;
CHECK(i64From(enc("12345"), v)); CHECK(v == 12345);
CHECK(i64From(enc("-42"), v)); CHECK(v == -42);
CHECK(i64From(enc("9223372036854775807"), v)); // INT64_MAX
CHECK(v == 9223372036854775807LL);
CHECK(!i64From(enc("9223372036854775808"), v)); // overflow
CHECK(!i64From(enc("12345678901234567890"), v)); // 20-digit cap
CHECK(!i64From(enc("-"), v)); // bare sign
CHECK(!i64From(enc("1a"), v)); // non-digit
CHECK(!i64From(enc(""), v)); // empty
}
static void testFieldIntRejectsOutOfIntRange() {
// The fixed form of the old provenance strtol TODO: an out-of-int-range field
// FAILS the parse instead of silently narrowing.
int v = 0;
CHECK(intFrom(enc("2147483647"), v)); CHECK(v == 2147483647);
CHECK(intFrom(enc("-2147483648"), v)); CHECK(v == -2147483647 - 1);
CHECK(!intFrom(enc("2147483648"), v));
CHECK(!intFrom(enc("3000000000"), v));
}
// --- Cursor: fieldSizeT / fieldDouble ------------------------------------------
static void testFieldSizeT() {
std::size_t v = 1;
CHECK(sizeFrom(enc("0"), v)); CHECK(v == 0);
CHECK(sizeFrom(enc("4096"), v)); CHECK(v == 4096);
CHECK(!sizeFrom(enc("-1"), v)); // sign = non-digit
CHECK(!sizeFrom(enc(std::string(21, '9')), v)); // 21-digit cap
CHECK(!sizeFrom(enc("18446744073709551616"), v)); // overflow guard
}
static void testFieldDoubleRoundTrip() {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.17g", 3141.592653589793);
double v = 0;
CHECK(dblFrom(enc(buf), v));
CHECK(v == 3141.592653589793);
CHECK(!dblFrom(enc("1.5x"), v)); // trailing bytes
}
// --- parseUnsignedDecimal (the bank_sync generation core) -----------------------
static void testParseUnsignedDecimal() {
std::int64_t v = 0;
CHECK(wire::parseUnsignedDecimal("0", v) && v == 0);
CHECK(wire::parseUnsignedDecimal("1721947293", v) && v == 1721947293);
CHECK(wire::parseUnsignedDecimal("9223372036854775807", v) && v == 9223372036854775807LL);
CHECK(!wire::parseUnsignedDecimal("", v));
CHECK(!wire::parseUnsignedDecimal("+5", v)); // sign rejected (non-digit)
CHECK(!wire::parseUnsignedDecimal("-5", v));
CHECK(!wire::parseUnsignedDecimal("12a", v));
CHECK(!wire::parseUnsignedDecimal("9223372036854775808", v)); // overflow
CHECK(!wire::parseUnsignedDecimal(std::string(40, '9'), v)); // long run cannot wrap
}
int main() {
testPutFieldExactBytes();
testFieldRoundTripIncludingSeparators();
testLiteralMismatchFails();
testFieldRejectsMalformedLengths();
testFailureLatchesOk();
testFieldInt64AcceptsAndRejects();
testFieldIntRejectsOutOfIntRange();
testFieldSizeT();
testFieldDoubleRoundTrip();
testParseUnsignedDecimal();
if (g_fail == 0) std::printf("wire: all tests passed\n");
else std::printf("wire: %d CHECK(s) FAILED\n", g_fail);
return g_fail == 0 ? 0 : 1;
}