// Standalone tests for reasampler::instrument::map::bridge_marshal — no VST3, no REAPER, no test // framework. Same fast assert loop as the sibling pure tests: assert the REAPER // bridge-read marshalling (GetProjExtState result decode) directly, so the DAW-facing // shell only has to invoke the API. // // Covers: decodeGetProjExtState hit/absent/zero-return/empty-buffer (the stale-buffer // guard). The S1 spike's extractJsonStringField string-scan reader was retired in S4 // (the instrument now parses the bank through the shared bank_book JSON path), so its // cases are gone with it. Q-W5 (rider T2-04) adds readProjExtStateGrowing — the ONE // grow-loop retry policy shared by persist / usage_scan / reaper_bridge — covered // against a fake read: absent, small-fit, grow-then-fit, empty-complete (composed with // the decode guard), and the 16 MB overflow give-up. The overflow case is // prune-safety-adjacent (usage_scan folds it to abortPrune), so it is pinned here. #include "../src/core/instrument/map/bridge_marshal.h" #include #include #include using namespace reasampler; using namespace reasampler::instrument::map; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- decodeGetProjExtState ---------------------------------------------------- static void testDecodeHit() { // REAPER reports a non-zero length and filled the buffer: that IS the value. auto v = decodeGetProjExtState(5, "hello"); CHECK(v.has_value()); CHECK(v && *v == "hello"); } static void testDecodeAbsentKey() { // REAPER returns 0 for an absent key. Even if a caller passed a dirty buffer, the // decoder must NOT surface it — the zero return means "no value". auto v = decodeGetProjExtState(0, "stale-bytes-from-a-prior-read"); CHECK(!v.has_value()); } static void testDecodeNegativeReturn() { auto v = decodeGetProjExtState(-1, "whatever"); CHECK(!v.has_value()); } static void testDecodeEmptyBuffer() { // Positive return but empty buffer — treat as no value (defensive). auto v = decodeGetProjExtState(3, ""); CHECK(!v.has_value()); } // --- readProjExtStateGrowing (the T2-04 shared grow-loop policy) ------------- // A fake GetProjExtState: honors the buffer contract (writes at most cap-1 chars + // NUL — REAPER clips to the caller's capacity) and returns the stored value's length. static int fakeRead(const std::string& stored, char* buf, int cap) { const std::size_t n = stored.size() < static_cast(cap - 1) ? stored.size() : static_cast(cap - 1); std::memcpy(buf, stored.data(), n); buf[n] = '\0'; return static_cast(stored.size()); } static void testGrowingAbsent() { // rv <= 0 on the first attempt: the key holds no value — Absent, no retry. int calls = 0; const auto r = readProjExtStateGrowing([&](char*, int) { ++calls; return 0; }); CHECK(r.status == GrowingExtStateRead::Status::Absent); CHECK(r.apiReturn == 0); CHECK(calls == 1); } static void testGrowingSmallValueFitsFirstAttempt() { const std::string stored = "hello"; int calls = 0; const auto r = readProjExtStateGrowing([&](char* buf, int cap) { ++calls; return fakeRead(stored, buf, cap); }); CHECK(r.status == GrowingExtStateRead::Status::Complete); CHECK(r.value == stored); CHECK(r.apiReturn == 5); CHECK(calls == 1); } static void testGrowingRetriesUntilStrictFit() { // A value whose C string exactly fills the first buffer (size+1 == cap) is // AMBIGUOUS — it may have been clipped — so the policy must retry at the next // capacity, where it fits strictly and returns whole. const std::string stored(static_cast((1 << 16) - 1), 'x'); int calls = 0; const auto r = readProjExtStateGrowing([&](char* buf, int cap) { ++calls; return fakeRead(stored, buf, cap); }); CHECK(r.status == GrowingExtStateRead::Status::Complete); CHECK(r.value == stored); CHECK(calls == 2); } static void testGrowingEmptyCompleteComposesWithDecodeGuard() { // rv > 0 but an empty buffer: the loop reports a Complete empty value, and the // bridge path's decodeGetProjExtState(apiReturn, value) still rejects it — the // stale/empty-buffer guard survives the T2-04 rewire unchanged. const auto r = readProjExtStateGrowing([&](char* buf, int) { buf[0] = '\0'; return 3; }); CHECK(r.status == GrowingExtStateRead::Status::Complete); CHECK(r.value.empty()); CHECK(!decodeGetProjExtState(r.apiReturn, r.value).has_value()); } static void testGrowingOverflowGivesUpAtCeiling() { // Every attempt clips (the value never fits under 16 MB): Overflow — which is // "unreadable WHOLE", never Absent. usage_scan folds this to the prune fail-safe // abort, so the distinction is load-bearing. Caps run 2^16..2^24 step ×4 = 5 tries. int calls = 0; const auto r = readProjExtStateGrowing([&](char* buf, int cap) { ++calls; std::memset(buf, 'a', static_cast(cap - 1)); buf[cap - 1] = '\0'; return cap; // reports a length that never strictly fits }); CHECK(r.status == GrowingExtStateRead::Status::Overflow); CHECK(calls == 5); } int main() { testDecodeHit(); testDecodeAbsentKey(); testDecodeNegativeReturn(); testDecodeEmptyBuffer(); testGrowingAbsent(); testGrowingSmallValueFitsFirstAttempt(); testGrowingRetriesUntilStrictFit(); testGrowingEmptyCompleteComposesWithDecodeGuard(); testGrowingOverflowGivesUpAtCeiling(); if (g_fail == 0) std::printf("bridge_marshal: all tests passed\n"); return g_fail != 0; }