Files
reasampler/tests/test_bank_sync.cpp
T

204 lines
9.5 KiB
C++

// Standalone tests for reasampler::instrument::map::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/core/instrument/map/bank_sync.h"
#include <cstdint>
#include <cstdio>
#include <optional>
#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)
// --- 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);
}
// Re-import / dedup-collapse path: the extension ingests a sample that already exists in
// the bank (dedup collapse: the bank_generation counter does NOT advance because no new
// sample was added), but a new assign_request is still written with a HIGHER assign
// generation (the ingest disambiguator, independent of bank_generation).
//
// The consumeDecision must apply the request — its own generation is the "is this new?"
// discriminator, and it is strictly greater than lastConsumed. bank_generation does not
// enter consumeDecision at all; this test proves the two counters are fully independent.
static void testDedupCollapseAssignAppliesWhenBankGenerationUnchanged() {
// Simulate: bank_generation is 5 both before and after the dedup ingest (unchanged).
// The assign_request generation is 300 (new; lastConsumed was 200 from the prior assign).
// The existing sample resolves (it is in the bank — dedup kept it there).
const auto d = consumeDecision(makeReq("pool", "existing-sample-id", 300),
/*lastConsumed*/ 200, /*resolves*/ true,
/*isFocusedTarget*/ true);
CHECK(d.apply);
CHECK(d.bankId == "pool");
CHECK(d.sampleId == "existing-sample-id");
CHECK(d.consumedGeneration == 300); // marker advanced to the new assign generation
// bank_generation (5) is not a parameter here — this test documents its absence from
// consumeDecision: only the assign_request's own generation drives the consume decision.
}
int main() {
testParseAbsentAndMalformed();
testParseValid();
testParseOverflow();
testFormatRoundTrip();
testGenerationChanged();
testNoRequest();
testNotNewerNotReapplied();
testNonTargetStaysEligible();
testUnresolvableDroppedSilently();
testAppliedWhenNewTargetResolvable();
testSameIdNewGenerationReapplies();
testFreshInstanceAppliesFirst();
testDedupCollapseAssignAppliesWhenBankGenerationUnchanged();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}