152 lines
5.7 KiB
C++
152 lines
5.7 KiB
C++
// 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 <cstdio>
|
||
#include <cstring>
|
||
#include <string>
|
||
|
||
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<std::size_t>(cap - 1)
|
||
? stored.size()
|
||
: static_cast<std::size_t>(cap - 1);
|
||
std::memcpy(buf, stored.data(), n);
|
||
buf[n] = '\0';
|
||
return static_cast<int>(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<std::size_t>((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<std::size_t>(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;
|
||
}
|