feat(prune): R2 dry-run prune shell + persist wiring, report-only

Add ReaSamplerSession::pruneDryRun enumerating the resolved current bank
folder, feeding the R1 core, and returning a PruneReport (count/bytes/list).
New pure bankRelativeForName + buildPruneReport keep spelling and tally
testable. Register forever-stable BANK_PRUNE_FOLDER action, report-only. No deletion.
This commit is contained in:
2026-07-26 18:19:41 -04:00
parent 46176f3f04
commit c1473c42e1
10 changed files with 297 additions and 1 deletions
+70
View File
@@ -19,8 +19,10 @@
#include "../src/prune_reconcile.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <string>
#include <unordered_map>
#include <vector>
using namespace reasampler;
@@ -259,6 +261,69 @@ static void testReferencedPathsSkipsEmptyPath() {
CHECK(refs[0] == "bank/real.wav");
}
// --- buildPruneReport: count / byte-sum / truncation (Phase R, R2) ----------
static void testReportCountSizeAndFullList() {
// A known orphan set with known sizes -> exact count, exact byte sum, full list
// (under the cap). Order follows the orphan input order (deterministic).
const std::vector<std::string> orphans = {"bank/a.wav", "bank/b.wav", "bank/c.wav"};
const std::unordered_map<std::string, std::uint64_t> sizes = {
{"bank/a.wav", 100}, {"bank/b.wav", 250}, {"bank/c.wav", 50}};
const PruneReport r = buildPruneReport(orphans, sizes, /*displayCap=*/64);
CHECK(r.count == 3);
CHECK(r.totalBytes == 400); // 100 + 250 + 50 — would fail if sizes mis-summed
CHECK(r.orphans.size() == 3);
CHECK(!r.truncated);
CHECK(r.orphans[0] == "bank/a.wav"); // input order preserved
CHECK(r.orphans[1] == "bank/b.wav");
CHECK(r.orphans[2] == "bank/c.wav");
}
static void testReportMissingSizeCountsZeroNotDropped() {
// An orphan with no stat'd size contributes 0 to the sum but is STILL listed/counted.
const std::vector<std::string> orphans = {"bank/a.wav", "bank/nosize.wav"};
const std::unordered_map<std::string, std::uint64_t> sizes = {{"bank/a.wav", 10}};
const PruneReport r = buildPruneReport(orphans, sizes, 64);
CHECK(r.count == 2); // both counted (missing size must not drop the orphan)
CHECK(r.totalBytes == 10); // nosize contributes 0
CHECK(r.orphans.size() == 2);
}
static void testReportTruncatesListButKeepsExactCountAndSize() {
// More orphans than the display cap: list clips to the cap, but count and byte sum
// stay EXACT over the whole set, and truncated flags the clip.
std::vector<std::string> orphans;
std::unordered_map<std::string, std::uint64_t> sizes;
for (int i = 0; i < 10; ++i) {
const std::string p = "bank/f" + std::to_string(i) + ".wav";
orphans.push_back(p);
sizes[p] = 5; // 10 files * 5 bytes = 50 total
}
const PruneReport r = buildPruneReport(orphans, sizes, /*displayCap=*/3);
CHECK(r.count == 10); // exact, not clipped
CHECK(r.totalBytes == 50); // summed over ALL 10, not just the shown 3
CHECK(r.orphans.size() == 3); // list clipped to the cap
CHECK(r.truncated);
CHECK(r.orphans[0] == "bank/f0.wav"); // first-N in input order
}
static void testReportUncappedWhenCapZero() {
// displayCap == 0 means "no cap": the whole list is emitted, truncated stays false.
const std::vector<std::string> orphans = {"bank/a.wav", "bank/b.wav"};
const PruneReport r = buildPruneReport(orphans, /*sizes*/ {}, /*displayCap=*/0);
CHECK(r.count == 2);
CHECK(r.orphans.size() == 2);
CHECK(!r.truncated);
}
static void testReportEmptyOrphanSet() {
const PruneReport r = buildPruneReport({}, {}, 64);
CHECK(r.count == 0);
CHECK(r.totalBytes == 0);
CHECK(r.orphans.empty());
CHECK(!r.truncated);
}
int main() {
testNullTestAllReferenced();
testFormulaMixedPopulations();
@@ -276,6 +341,11 @@ int main() {
testReferencedPathsUnionAcrossBanksAndPool();
testReferencedPathsEmptyBook();
testReferencedPathsSkipsEmptyPath();
testReportCountSizeAndFullList();
testReportMissingSizeCountsZeroNotDropped();
testReportTruncatesListButKeepsExactCountAndSize();
testReportUncappedWhenCapZero();
testReportEmptyOrphanSet();
if (g_fail == 0) std::printf("prune_reconcile_tests: ALL PASS\n");
else std::printf("prune_reconcile_tests: %d FAILURE(S)\n", g_fail);