Files
reasampler/tests/test_bridge_marshal.cpp
T
daniel f3be4d8cce Q-W6: registration table (OCP) in main.cpp; bank verbs -> shell/bank_ops(Session&); persist.h + wav_trim + namespaces.h shims deleted; 61/61
capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
2026-07-29 13:40:09 -04:00

155 lines
6.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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) added readProjExtStateGrowing — the ONE
// grow-loop retry policy shared by persist / usage_scan / reaper_bridge, rehomed to
// core/wire/ext_state_read.h in Q-W6 and still pinned here alongside the decode — 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 "../src/core/wire/ext_state_read.h" // readProjExtStateGrowing (Q-W6 rehome)
#include <cstdio>
#include <cstring>
#include <string>
using namespace reasampler;
using namespace reasampler::instrument::map;
using namespace reasampler::wire;
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;
}