Files
reasampler/tests/test_prune_reconcile.cpp
T
daniel 3cfa9239d2 feat(prune): R1 prune-reconcile pure core — (owned ∩ present) − referenced
Add REAPER-free/filesystem-free prune orphan-set core + additive
BankBook::referencedPaths() union query, with full unit coverage.
2026-07-26 19:09:39 -04:00

284 lines
13 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::pruneOrphans (Phase R, Wave 1) and the additive
// BankBook::referencedPaths() union query it consumes — no REAPER, no framework.
//
// The safety-critical computation: orphans = (owned ∩ present) referenced. Every
// test below would FAIL if the set algebra were wrong (a missing ∩ present, a missing
// referenced, or a non-union referenced-set) — see the negative assertions.
//
// Covers (brief-named):
// 1. Prune null test: all present files referenced -> empty orphan set.
// 2. Formula: orphans = (owned ∩ present) referenced, mixed populations.
// 3. Copied file referenced by a SECOND bank survives (union semantics).
// 4. Present-but-unowned (hand-dropped) file is never reclaimed.
// 5. Edge cases: empty folder, empty book/referenced-set, empty manifest.
// 6. Owned-but-absent file (manifest entry, no file on disk) -> no orphan, no error.
// Plus: determinism (present-order output), duplicate-`present` de-dup, exact-string
// (non-normalizing) match, and the referencedPaths() union query directly.
#include "../src/bank_book.h"
#include "../src/prune_reconcile.h"
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
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)
// --- helpers -----------------------------------------------------------------
static bool contains(const std::vector<std::string>& v, const std::string& s) {
return std::find(v.begin(), v.end(), s) != v.end();
}
// A minimal Sample carrying just the fields prune cares about (id + relativePath).
static Sample sampleAt(const std::string& id, const std::string& relPath,
const std::string& hash = "") {
Sample s;
s.id = id;
s.relativePath = relPath;
s.contentHash = hash;
return s;
}
// --- 1. prune null test: all present referenced -> empty --------------------
static void testNullTestAllReferenced() {
const std::vector<std::string> present = {"bank/a.wav", "bank/b.wav", "bank/c.wav"};
const std::vector<std::string> owned = {"bank/a.wav", "bank/b.wav", "bank/c.wav"};
const std::vector<std::string> referenced= {"bank/a.wav", "bank/b.wav", "bank/c.wav"};
const auto orphans = pruneOrphans(present, referenced, owned);
// The core invariant: a folder whose every file is referenced deletes NOTHING.
CHECK(orphans.empty());
}
// --- 2. formula: (owned ∩ present) referenced, mixed populations ----------
static void testFormulaMixedPopulations() {
// present : a b c d (on disk)
// owned : a b d e (system created; e is owned-but-absent)
// ref'd : a (still in an index)
// expected orphans: owned ∩ present = {a,b,d}; minus referenced {a} = {b,d}
// - c present but NOT owned -> excluded (hand-dropped)
// - e owned but NOT present -> excluded (no file on disk)
// - a owned+present but referenced -> excluded
const std::vector<std::string> present = {"bank/a.wav", "bank/b.wav",
"bank/c.wav", "bank/d.wav"};
const std::vector<std::string> owned = {"bank/a.wav", "bank/b.wav",
"bank/d.wav", "bank/e.wav"};
const std::vector<std::string> referenced = {"bank/a.wav"};
const auto orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 2);
CHECK(contains(orphans, "bank/b.wav"));
CHECK(contains(orphans, "bank/d.wav"));
// Negative assertions — each would fire if a clause of the formula were dropped:
CHECK(!contains(orphans, "bank/a.wav")); // referenced
CHECK(!contains(orphans, "bank/c.wav")); // ∩ owned (hand-dropped)
CHECK(!contains(orphans, "bank/e.wav")); // ∩ present (absent)
}
// --- 3. copied file referenced by a second bank survives (union) ------------
static void testCopiedFileSurvivesViaUnion() {
// A file present + owned, whose ONLY index reference lives in a second (non-active)
// bank via a copy. The union across the whole book must keep it out of the orphan
// set. Built through the real BankBook so referencedPaths()'s union is exercised.
BankBook book;
book.createBank("b1", "Drums");
// Add to the pool, then copy into b1 (same file referenced by two banks).
book.index(std::string(kPoolBankId))->add(sampleAt("s1", "bank/shared.wav", "h1"));
CHECK(book.copySample("s1", kPoolBankId, "b1") == TransferResult::Copied);
const auto referenced = book.referencedPaths();
// Union de-dups: one path though two banks reference it.
CHECK(referenced.size() == 1);
CHECK(referenced[0] == "bank/shared.wav");
const std::vector<std::string> present = {"bank/shared.wav"};
const std::vector<std::string> owned = {"bank/shared.wav"};
const auto orphans = pruneOrphans(present, referenced, owned);
// Referenced by b1 (the copy) -> never an orphan even if the pool later dropped it.
CHECK(orphans.empty());
}
// A sharper union test: remove the file from the pool but keep the copy in b1. The
// file must STILL survive (referenced by b1 alone) — the whole point of union safety.
static void testUnionSurvivesWhenOnlySecondBankReferences() {
BankBook book;
book.createBank("b1", "Drums");
book.index(std::string(kPoolBankId))->add(sampleAt("s1", "bank/shared.wav", "h1"));
CHECK(book.copySample("s1", kPoolBankId, "b1") == TransferResult::Copied);
// Drop the pool's reference; b1 still references the file.
CHECK(book.removeSample("s1", kPoolBankId) == RemoveResult::Removed);
const auto referenced = book.referencedPaths();
CHECK(referenced.size() == 1);
CHECK(contains(referenced, "bank/shared.wav"));
const auto orphans = pruneOrphans({"bank/shared.wav"}, referenced, {"bank/shared.wav"});
CHECK(orphans.empty()); // still referenced by b1 -> not reclaimable
}
// --- 4. present-but-unowned (hand-dropped) file never reclaimed -------------
static void testHandDroppedNeverReclaimed() {
// A file on disk that no index references AND the manifest does not own. It is a
// hand-dropped file — prune must never reclaim it (ownership attribution, fork R-D).
const std::vector<std::string> present = {"bank/user_drop.wav", "bank/ours.wav"};
const std::vector<std::string> owned = {"bank/ours.wav"}; // NOT user_drop
const std::vector<std::string> referenced = {}; // neither referenced
const auto orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 1);
CHECK(contains(orphans, "bank/ours.wav")); // ours, unreferenced -> orphan
CHECK(!contains(orphans, "bank/user_drop.wav")); // hand-dropped -> protected
}
// --- 5. edge cases: empty folder / book / manifest --------------------------
static void testEmptyFolder() {
// No files present -> nothing to reclaim regardless of owned/referenced.
const auto orphans = pruneOrphans({}, {"bank/a.wav"}, {"bank/a.wav"});
CHECK(orphans.empty());
}
static void testEmptyReferencedSet() {
// Empty book (nothing referenced): every present ∩ owned file is an orphan.
const std::vector<std::string> present = {"bank/a.wav", "bank/b.wav"};
const std::vector<std::string> owned = {"bank/a.wav", "bank/b.wav"};
const auto orphans = pruneOrphans(present, /*referenced*/ {}, owned);
CHECK(orphans.size() == 2);
CHECK(contains(orphans, "bank/a.wav"));
CHECK(contains(orphans, "bank/b.wav"));
}
static void testEmptyManifest() {
// Empty manifest (owns nothing): nothing is reclaimable even if present+unreferenced.
const std::vector<std::string> present = {"bank/a.wav", "bank/b.wav"};
const std::vector<std::string> referenced = {};
const auto orphans = pruneOrphans(present, referenced, /*owned*/ {});
CHECK(orphans.empty());
}
static void testAllEmpty() {
const auto orphans = pruneOrphans({}, {}, {});
CHECK(orphans.empty());
}
// --- 6. owned-but-absent file -> no orphan, no error ------------------------
static void testOwnedButAbsentNoOrphan() {
// A manifest entry whose file is NOT on disk (e.g. deleted out-of-band). It must
// yield no orphan entry (∩ present excludes it) and no error/crash.
const std::vector<std::string> present = {"bank/here.wav"};
const std::vector<std::string> owned = {"bank/here.wav", "bank/gone.wav"};
const std::vector<std::string> referenced = {};
const auto orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 1);
CHECK(contains(orphans, "bank/here.wav"));
CHECK(!contains(orphans, "bank/gone.wav")); // owned but absent -> not an orphan
}
// --- determinism + duplicate-present + exact-string match -------------------
static void testOutputPreservesPresentOrder() {
// Output order follows `present` order (deterministic dry-run file list), NOT the
// owned/referenced order.
const std::vector<std::string> present = {"bank/z.wav", "bank/y.wav", "bank/x.wav"};
const std::vector<std::string> owned = {"bank/x.wav", "bank/y.wav", "bank/z.wav"};
const std::vector<std::string> referenced = {};
const auto orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 3);
CHECK(orphans[0] == "bank/z.wav");
CHECK(orphans[1] == "bank/y.wav");
CHECK(orphans[2] == "bank/x.wav");
}
static void testDuplicatePresentDeduped() {
// A defensive property: a duplicated `present` spelling appears once in the output.
const std::vector<std::string> present = {"bank/a.wav", "bank/a.wav"};
const std::vector<std::string> owned = {"bank/a.wav"};
const auto orphans = pruneOrphans(present, /*referenced*/ {}, owned);
CHECK(orphans.size() == 1);
CHECK(orphans[0] == "bank/a.wav");
}
static void testExactStringMatchNotNormalized() {
// Safety-critical: the core matches VERBATIM (mirror of the model's exact-string
// convention). A referenced file spelled with backslashes is a DIFFERENT string
// from the forward-slash present spelling — the core does NOT normalize them to
// equal. This documents the invariant: consistent spelling is the R2 shell's
// contract. If the core silently normalized, this would (wrongly) treat the file
// as referenced and the assertion below would fail.
const std::vector<std::string> present = {"bank/a.wav"};
const std::vector<std::string> owned = {"bank/a.wav"};
const std::vector<std::string> referenced = {"bank\\a.wav"}; // different spelling
const auto orphans = pruneOrphans(present, referenced, owned);
CHECK(orphans.size() == 1); // not matched -> still an orphan
CHECK(orphans[0] == "bank/a.wav");
}
// --- BankBook::referencedPaths() union query directly -----------------------
static void testReferencedPathsUnionAcrossBanksAndPool() {
BankBook book;
book.createBank("b1", "Drums");
book.createBank("b2", "Bass");
book.index(std::string(kPoolBankId))->add(sampleAt("p1", "bank/pool.wav", "hp"));
book.index("b1")->add(sampleAt("d1", "bank/drum.wav", "hd"));
book.index("b2")->add(sampleAt("s1", "bank/bass.wav", "hb"));
const auto refs = book.referencedPaths();
// Union includes the pool AND every named bank.
CHECK(refs.size() == 3);
CHECK(contains(refs, "bank/pool.wav"));
CHECK(contains(refs, "bank/drum.wav"));
CHECK(contains(refs, "bank/bass.wav"));
// Pool-first ordinal order.
CHECK(refs[0] == "bank/pool.wav");
}
static void testReferencedPathsEmptyBook() {
BankBook book; // pool only, no samples
CHECK(book.referencedPaths().empty());
}
static void testReferencedPathsSkipsEmptyPath() {
// A sample with an empty relativePath references no file — it must not appear.
BankBook book;
book.index(std::string(kPoolBankId))->add(sampleAt("p1", "", "hp"));
book.index(std::string(kPoolBankId))->add(sampleAt("p2", "bank/real.wav", "hr"));
const auto refs = book.referencedPaths();
CHECK(refs.size() == 1);
CHECK(refs[0] == "bank/real.wav");
}
int main() {
testNullTestAllReferenced();
testFormulaMixedPopulations();
testCopiedFileSurvivesViaUnion();
testUnionSurvivesWhenOnlySecondBankReferences();
testHandDroppedNeverReclaimed();
testEmptyFolder();
testEmptyReferencedSet();
testEmptyManifest();
testAllEmpty();
testOwnedButAbsentNoOrphan();
testOutputPreservesPresentOrder();
testDuplicatePresentDeduped();
testExactStringMatchNotNormalized();
testReferencedPathsUnionAcrossBanksAndPool();
testReferencedPathsEmptyBook();
testReferencedPathsSkipsEmptyPath();
if (g_fail == 0) std::printf("prune_reconcile_tests: ALL PASS\n");
else std::printf("prune_reconcile_tests: %d FAILURE(S)\n", g_fail);
return g_fail == 0 ? 0 : 1;
}