Q-W5: persist → session/ext_state_io/prune_fs (deletion authority concentrated); one GetProjExtState grow-loop in bridge_marshal (T2-04, ×3 rewired); bank_book JSON codec → bank_book_json via private static nameKey; persist.h stays umbrella. 61/61 green.
This commit is contained in:
@@ -6,11 +6,17 @@
|
||||
// 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.
|
||||
// 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;
|
||||
@@ -46,12 +52,100 @@ static void testDecodeEmptyBuffer() {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user