66d47fb410
Extension stamps a monotonic bank_generation counter, bumped at content mutations; the VST3 instrument polls it off-thread on an editor timer and consumes assignment requests, refreshing playback hands-free.
180 lines
7.9 KiB
C++
180 lines
7.9 KiB
C++
// Standalone tests for reasampler::vst::bank_sync — no REAPER, no VST3, no framework.
|
|
// The S9 bank-generation change-detection + the S8 assignment-request consume DECISION
|
|
// (the yes/no maths the instrument's off-audio-thread poll runs). The shell owns the
|
|
// cadence + side effects; this proves the decision rules without a host.
|
|
//
|
|
// Covers: parseBankGeneration (absent/malformed/overflow/negative/valid whole-string),
|
|
// formatBankGeneration round-trip, bankGenerationChanged, and every consumeDecision rule
|
|
// (no request / not-newer / non-target / unresolvable-drop / apply), asserting both the
|
|
// apply flag AND the advanced-marker value so a stale request is never re-evaluated.
|
|
|
|
#include "../src/vst/bank_sync.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
using namespace reasampler;
|
|
using namespace reasampler::vst;
|
|
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if(!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
|
|
// --- parseBankGeneration -----------------------------------------------------
|
|
|
|
static void testParseAbsentAndMalformed() {
|
|
// Absent / empty -> generation 0 (the pre-S9 default; a project with no stamp).
|
|
CHECK(parseBankGeneration("") == 0);
|
|
CHECK(parseBankGeneration("") == kBankGenerationAbsent);
|
|
// Malformed -> 0, never a crash, never a partial value.
|
|
CHECK(parseBankGeneration("abc") == 0);
|
|
CHECK(parseBankGeneration("12x") == 0); // trailing garbage rejects whole
|
|
CHECK(parseBankGeneration("x12") == 0); // leading garbage
|
|
CHECK(parseBankGeneration("1 2") == 0); // embedded space
|
|
CHECK(parseBankGeneration("+5") == 0); // sign rejected
|
|
CHECK(parseBankGeneration("-5") == 0); // negative rejected
|
|
CHECK(parseBankGeneration(" 5") == 0); // leading space
|
|
CHECK(parseBankGeneration("5.0") == 0); // decimal point
|
|
}
|
|
|
|
static void testParseValid() {
|
|
CHECK(parseBankGeneration("0") == 0);
|
|
CHECK(parseBankGeneration("1") == 1);
|
|
CHECK(parseBankGeneration("42") == 42);
|
|
CHECK(parseBankGeneration("00042") == 42); // leading zeros are still digits -> 42
|
|
CHECK(parseBankGeneration("9007199254740993") == 9007199254740993LL); // > 2^53
|
|
}
|
|
|
|
static void testParseOverflow() {
|
|
// A 19-digit int64-max is fine; anything past it rejects to 0 (never wraps).
|
|
CHECK(parseBankGeneration("9223372036854775807") == 9223372036854775807LL); // INT64_MAX
|
|
CHECK(parseBankGeneration("9223372036854775808") == 0); // INT64_MAX + 1 -> reject
|
|
CHECK(parseBankGeneration("99999999999999999999") == 0); // 20 nines -> reject
|
|
}
|
|
|
|
static void testFormatRoundTrip() {
|
|
CHECK(formatBankGeneration(0) == "0");
|
|
CHECK(formatBankGeneration(1) == "1");
|
|
CHECK(formatBankGeneration(123456789) == "123456789");
|
|
// Round-trips: format then parse yields the original for the valid domain.
|
|
for (std::int64_t g : {std::int64_t{0}, std::int64_t{1}, std::int64_t{7},
|
|
std::int64_t{9007199254740993LL}}) {
|
|
CHECK(parseBankGeneration(formatBankGeneration(g)) == g);
|
|
}
|
|
}
|
|
|
|
// --- bankGenerationChanged ---------------------------------------------------
|
|
|
|
static void testGenerationChanged() {
|
|
CHECK(!bankGenerationChanged(0, 0)); // pre-S9 idle: no stamp seen, no stamp now
|
|
CHECK(bankGenerationChanged(0, 1)); // first bump after a pre-S9 baseline -> change
|
|
CHECK(bankGenerationChanged(5, 6)); // normal increment
|
|
CHECK(!bankGenerationChanged(6, 6)); // idle poll (coalesced): no change
|
|
CHECK(bankGenerationChanged(6, 3)); // a project switch/reload can lower it -> change
|
|
}
|
|
|
|
// --- consumeDecision ---------------------------------------------------------
|
|
|
|
static AssignmentRequest makeReq(const std::string& bank, const std::string& sample,
|
|
std::int64_t gen) {
|
|
AssignmentRequest r;
|
|
r.bankId = bank;
|
|
r.sampleId = sample;
|
|
r.generation = gen;
|
|
return r;
|
|
}
|
|
|
|
// Rule 1a: no pending request -> nothing to do, marker unchanged.
|
|
static void testNoRequest() {
|
|
const auto d = consumeDecision(std::nullopt, /*lastConsumed*/ 5,
|
|
/*resolves*/ true, /*isFocusedTarget*/ true);
|
|
CHECK(!d.apply);
|
|
CHECK(d.consumedGeneration == 5); // marker held
|
|
}
|
|
|
|
// Rule 1b: a request no newer than what we already consumed (re-open case) -> no re-apply.
|
|
static void testNotNewerNotReapplied() {
|
|
// The persisted marker equals the request generation: the user already got this assign,
|
|
// possibly changed away from it. It MUST NOT re-apply on re-open.
|
|
const auto same = consumeDecision(makeReq("b", "s", 100), 100, true, true);
|
|
CHECK(!same.apply);
|
|
CHECK(same.consumedGeneration == 100); // unchanged
|
|
// An older request (a stale value lingering) is likewise ignored.
|
|
const auto older = consumeDecision(makeReq("b", "s", 90), 100, true, true);
|
|
CHECK(!older.apply);
|
|
CHECK(older.consumedGeneration == 100);
|
|
}
|
|
|
|
// Rule 2: a NEW request but this instance is not the target -> do not apply AND do not
|
|
// advance the marker (must stay eligible if focus later lands here — no thundering herd).
|
|
static void testNonTargetStaysEligible() {
|
|
const auto d = consumeDecision(makeReq("b", "s", 200), /*lastConsumed*/ 100,
|
|
/*resolves*/ true, /*isFocusedTarget*/ false);
|
|
CHECK(!d.apply);
|
|
CHECK(d.consumedGeneration == 100); // marker NOT advanced -> still eligible later
|
|
}
|
|
|
|
// Rule 3: a NEW request, target, but unresolvable -> DROP silently. Marker advances so it
|
|
// is never re-evaluated, but no selection change (assignment_request.h reader requirement).
|
|
static void testUnresolvableDroppedSilently() {
|
|
const auto d = consumeDecision(makeReq("b", "deleted-sample", 200),
|
|
/*lastConsumed*/ 100, /*resolves*/ false,
|
|
/*isFocusedTarget*/ true);
|
|
CHECK(!d.apply); // no selection change
|
|
CHECK(d.consumedGeneration == 200); // consumed-as-seen: never re-evaluated
|
|
CHECK(d.sampleId.empty()); // nothing to apply
|
|
}
|
|
|
|
// Rule 4: a NEW request, target, resolvable -> APPLY selection + advance the marker.
|
|
static void testAppliedWhenNewTargetResolvable() {
|
|
const auto d = consumeDecision(makeReq("bank-7", "cap-42", 200),
|
|
/*lastConsumed*/ 100, /*resolves*/ true,
|
|
/*isFocusedTarget*/ true);
|
|
CHECK(d.apply);
|
|
CHECK(d.bankId == "bank-7");
|
|
CHECK(d.sampleId == "cap-42");
|
|
CHECK(d.consumedGeneration == 200);
|
|
}
|
|
|
|
// Re-assigning the SAME sample id under a NEW generation must re-apply (the generation is
|
|
// the disambiguator; a recapture/re-drop of the same id is a fresh assign, not a no-op).
|
|
static void testSameIdNewGenerationReapplies() {
|
|
// First consume at gen 100.
|
|
const auto first = consumeDecision(makeReq("b", "s", 100), 50, true, true);
|
|
CHECK(first.apply);
|
|
CHECK(first.consumedGeneration == 100);
|
|
// Same id, higher generation, marker now at 100 -> applies again.
|
|
const auto second = consumeDecision(makeReq("b", "s", 150), 100, true, true);
|
|
CHECK(second.apply);
|
|
CHECK(second.sampleId == "s");
|
|
CHECK(second.consumedGeneration == 150);
|
|
}
|
|
|
|
// A fresh instance (lastConsumed == 0) applies a first assign — the default marker must not
|
|
// swallow the first request.
|
|
static void testFreshInstanceAppliesFirst() {
|
|
const auto d = consumeDecision(makeReq("b", "s", 1), 0, true, true);
|
|
CHECK(d.apply);
|
|
CHECK(d.consumedGeneration == 1);
|
|
}
|
|
|
|
int main() {
|
|
testParseAbsentAndMalformed();
|
|
testParseValid();
|
|
testParseOverflow();
|
|
testFormatRoundTrip();
|
|
testGenerationChanged();
|
|
testNoRequest();
|
|
testNotNewerNotReapplied();
|
|
testNonTargetStaysEligible();
|
|
testUnresolvableDroppedSilently();
|
|
testAppliedWhenNewTargetResolvable();
|
|
testSameIdNewGenerationReapplies();
|
|
testFreshInstanceAppliesFirst();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|