Files
reasampler/tests/test_sample_usage.cpp
T

265 lines
12 KiB
C++

// Standalone tests for reasampler::sample_usage — no REAPER, no framework.
// The pS-usage seam: instances publish held captures; the extension folds live
// instances' holds into the prune's `referenced` set. Tested hard here because this is
// the prune-protection guarantee: a capture held by a live instance must be IMPOSSIBLE
// to prune (the composed proof at the bottom links prune_reconcile and shows
// pruneOrphans can never emit a held path), while a stale record from a deleted
// instance must NOT permanently block reclaim (the liveness fold).
//
// Covers: wire round-trip (empty / adversarial bytes), malformed -> nullopt, the
// publish plan's four branches (fresh key / clean replace / same-track union /
// cross-track re-mint) + skipWrite idempotence, the liveness fold (live, dead-track,
// empty-guid fallback, de-dup), and the composed pruneOrphans exclusion proof.
#include "../src/sample_usage.h"
#include <cstdio>
#include <string>
#include <unordered_set>
#include <vector>
#include "../src/prune_reconcile.h" // mergeReferenced + pruneOrphans (composed proof)
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
namespace {
UsageRecord makeRecord(const std::string& trackGuid,
std::vector<UsageHold> holds) {
UsageRecord r;
r.trackGuid = trackGuid;
r.holds = std::move(holds);
return r;
}
} // namespace
// --- wire round-trip ----------------------------------------------------------
static void testRoundTrip() {
const UsageRecord rec = makeRecord(
"{12345678-1234-1234-1234-1234567890AB}",
{UsageHold{"cap-1700-kick", "reasampler_bank/kick.wav"},
UsageHold{"cap-1701-snare", "reasampler_bank/snare.wav"}});
const std::string wire = encodeUsageRecord(rec);
auto back = decodeUsageRecord(wire);
CHECK(back.has_value());
CHECK(*back == rec);
CHECK(encodeUsageRecord(*back) == wire); // deterministic re-encode
}
static void testRoundTripEmptyHoldsAndEmptyGuid() {
// An empty-holds record is LEGAL (an instance releasing everything it held), and an
// empty trackGuid is legal (no track context at publish -> any-instance fallback).
const UsageRecord rec = makeRecord("", {});
auto back = decodeUsageRecord(encodeUsageRecord(rec));
CHECK(back.has_value());
CHECK(back->trackGuid.empty());
CHECK(back->holds.empty());
}
static void testRoundTripAdversarialBytes() {
// Ids/paths carrying the wire's own metacharacters must survive whole (the whole
// point of length-prefixing): ':' delimiters, digits, the magic tag itself.
const UsageRecord rec = makeRecord(
"12:34:guid-with-colons",
{UsageHold{"rsusage1-lookalike", "path with spaces/and:colons/7:x.wav"}});
auto back = decodeUsageRecord(encodeUsageRecord(rec));
CHECK(back.has_value());
CHECK(*back == rec);
}
static void testDecodeMalformed() {
CHECK(!decodeUsageRecord("").has_value());
CHECK(!decodeUsageRecord("garbage").has_value());
CHECK(!decodeUsageRecord("rsusage1").has_value()); // truncated after magic
CHECK(!decodeUsageRecord("rsusage2" "0:1:0").has_value()); // wrong magic version
// Truncated mid-holds: claims 2 holds, carries 1.
UsageRecord one = makeRecord("{G}", {UsageHold{"a", "p.wav"}});
std::string wire = encodeUsageRecord(one);
// Rewrite the count field "1:1" -> "1:2" (count is the 2nd field: len 1, value '1').
const std::string needle = "1:1"; // count field for one hold
const std::size_t pos = wire.find(needle, std::string("rsusage1").size() + 4);
CHECK(pos != std::string::npos);
wire[pos + 2] = '2';
CHECK(!decodeUsageRecord(wire).has_value());
// Trailing garbage after a complete record -> reject whole.
CHECK(!decodeUsageRecord(encodeUsageRecord(one) + "x").has_value());
}
// --- publish plan --------------------------------------------------------------
static void testPlanFreshKey() {
const UsageRecord mine = makeRecord("{T1}", {UsageHold{"a", "p.wav"}});
const UsagePublishPlan plan = planUsagePublish(std::nullopt, "", mine);
CHECK(!plan.remint);
CHECK(!plan.skipWrite);
CHECK(plan.wire == encodeUsageRecord(mine));
}
static void testPlanCleanReplaceAndSkip() {
// The key holds exactly what this lifetime wrote -> clean replace; released holds drop.
const UsageRecord prev = makeRecord("{T1}", {UsageHold{"a", "pa.wav"},
UsageHold{"b", "pb.wav"}});
const std::string prevWire = encodeUsageRecord(prev);
const UsageRecord mine = makeRecord("{T1}", {UsageHold{"a", "pa.wav"}});
const UsagePublishPlan plan = planUsagePublish(prevWire, prevWire, mine);
CHECK(!plan.remint);
CHECK(!plan.skipWrite);
auto back = decodeUsageRecord(plan.wire);
CHECK(back.has_value());
CHECK(back->holds.size() == 1); // 'b' genuinely released — NOT unioned back in
// Unchanged play-set -> byte-identical write -> skip (idle reload tick).
const UsagePublishPlan idle = planUsagePublish(prevWire, prevWire, prev);
CHECK(idle.skipWrite);
CHECK(!idle.remint);
}
static void testPlanSameTrackUnion() {
// First publish of a lifetime (lastPublished empty) over a same-track existing value:
// my own last-session record OR a same-track copy-sibling — either way UNION, never
// drop (the fail-safe direction; a sibling's holds must survive my write).
const UsageRecord theirs = makeRecord("{T1}", {UsageHold{"a", "pa.wav"}});
const UsageRecord mine = makeRecord("{T1}", {UsageHold{"b", "pb.wav"},
UsageHold{"a", "pa.wav"}});
const UsagePublishPlan plan =
planUsagePublish(encodeUsageRecord(theirs), "", mine);
CHECK(!plan.remint);
auto back = decodeUsageRecord(plan.wire);
CHECK(back.has_value());
CHECK(back->trackGuid == "{T1}");
CHECK(back->holds.size() == 2); // a (existing-first) + b, de-duped
CHECK(back->holds[0].sampleId == "a");
CHECK(back->holds[1].sampleId == "b");
}
static void testPlanCrossTrackRemint() {
// The key holds a foreign record from ANOTHER track: this state was cloned there
// (FX copy / track duplication) — take a fresh identity, leave theirs untouched.
const UsageRecord theirs = makeRecord("{T1}", {UsageHold{"a", "pa.wav"}});
const UsageRecord mine = makeRecord("{T2}", {UsageHold{"a", "pa.wav"}});
const UsagePublishPlan plan =
planUsagePublish(encodeUsageRecord(theirs), "", mine);
CHECK(plan.remint);
CHECK(plan.wire == encodeUsageRecord(mine)); // written under the NEW key
}
static void testPlanUndecodableExisting() {
// An undecodable existing value protects nothing — overwrite with mine.
const UsageRecord mine = makeRecord("{T1}", {UsageHold{"a", "pa.wav"}});
const UsagePublishPlan plan = planUsagePublish(std::string("corrupt"), "", mine);
CHECK(!plan.remint);
CHECK(plan.wire == encodeUsageRecord(mine));
}
// --- liveness fold ---------------------------------------------------------------
static void testHeldPathsLiveness() {
const std::vector<UsageRecord> records = {
makeRecord("{LIVE}", {UsageHold{"a", "pa.wav"}}),
makeRecord("{DEAD}", {UsageHold{"b", "pb.wav"}}), // deleted track/instance
makeRecord("", {UsageHold{"c", "pc.wav"}}), // no track context
};
const std::unordered_set<std::string> live = {"{LIVE}"};
// Live-track record counts; dead-track record is EXCLUDED (no stale false-protect);
// empty-guid record counts while ANY instance lives (fail-safe fallback).
const std::vector<std::string> withAny = usageHeldPaths(records, live, true);
CHECK(withAny.size() == 2);
CHECK(withAny[0] == "pa.wav");
CHECK(withAny[1] == "pc.wav");
// No instance anywhere -> empty-guid fallback closes too; only live-track survives.
const std::vector<std::string> noAny = usageHeldPaths(records, live, false);
CHECK(noAny.size() == 1);
CHECK(noAny[0] == "pa.wav");
// Zero live instances at all -> nothing protected (a project whose instances were
// all deleted cannot be permanently blocked by leftover records).
const std::vector<std::string> none =
usageHeldPaths(records, std::unordered_set<std::string>{}, false);
CHECK(none.empty());
}
static void testHeldPathsDedupAndEmptyPathSkip() {
const std::vector<UsageRecord> records = {
makeRecord("{T}", {UsageHold{"a", "shared.wav"}, UsageHold{"x", ""}}),
makeRecord("{T}", {UsageHold{"b", "shared.wav"}, UsageHold{"c", "own.wav"}}),
};
const std::unordered_set<std::string> live = {"{T}"};
const std::vector<std::string> paths = usageHeldPaths(records, live, true);
CHECK(paths.size() == 2); // shared.wav de-duped across records; empty path skipped
CHECK(paths[0] == "shared.wav");
CHECK(paths[1] == "own.wav");
}
// --- the composed prune-protection proof -----------------------------------------
// The definition-of-done property at the pure layer: a capture held by a live instance
// lands in the referenced union, and pruneOrphans can NEVER emit it — even when the
// bank no longer references it (deleted from the bank while the instance kept its ref)
// and it is owned + present (the exact preconditions under which it WOULD be reclaimed).
static void testInstanceHoldMakesPathUnprunable() {
const std::vector<std::string> present = {"held.wav", "orphan.wav"};
const std::vector<std::string> owned = {"held.wav", "orphan.wav"};
const std::vector<std::string> bankRefs = {}; // bank does NOT reference either
// Without instance usage both are orphans (the pre-pS-usage behavior).
CHECK(pruneOrphans(present, bankRefs, owned).size() == 2);
// A live instance holds held.wav -> the union protects it; orphan.wav still reclaims.
const std::vector<UsageRecord> records = {
makeRecord("{T}", {UsageHold{"id-held", "held.wav"}})};
const std::unordered_set<std::string> live = {"{T}"};
const std::vector<std::string> referenced =
mergeReferenced(bankRefs, usageHeldPaths(records, live, true));
const std::vector<std::string> orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 1);
CHECK(orphans[0] == "orphan.wav");
// The instance (and its track) deleted -> the record no longer counts -> held.wav
// is reclaimable again (no permanent stale-key block).
const std::vector<std::string> refsAfterDelete = mergeReferenced(
bankRefs, usageHeldPaths(records, std::unordered_set<std::string>{}, false));
CHECK(pruneOrphans(present, refsAfterDelete, owned).size() == 2);
}
static void testMergeReferencedOrderAndDedup() {
const std::vector<std::string> a = {"p1.wav", "p2.wav"};
const std::vector<std::string> b = {"p2.wav", "p3.wav", "p1.wav"};
const std::vector<std::string> merged = mergeReferenced(a, b);
CHECK(merged.size() == 3);
CHECK(merged[0] == "p1.wav");
CHECK(merged[1] == "p2.wav");
CHECK(merged[2] == "p3.wav");
CHECK(mergeReferenced({}, {}).empty());
}
int main() {
testRoundTrip();
testRoundTripEmptyHoldsAndEmptyGuid();
testRoundTripAdversarialBytes();
testDecodeMalformed();
testPlanFreshKey();
testPlanCleanReplaceAndSkip();
testPlanSameTrackUnion();
testPlanCrossTrackRemint();
testPlanUndecodableExisting();
testHeldPathsLiveness();
testHeldPathsDedupAndEmptyPathSkip();
testInstanceHoldMakesPathUnprunable();
testMergeReferencedOrderAndDedup();
if (g_fail == 0) {
std::printf("sample_usage_tests: all tests passed\n");
return 0;
}
std::printf("sample_usage_tests: %d FAILURES\n", g_fail);
return 1;
}