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:
+1
-1
@@ -402,7 +402,7 @@ add_library(reaper_reasampler MODULE
|
||||
src/bank_book.cpp
|
||||
src/owned_manifest.cpp
|
||||
)
|
||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest app_version provenance)
|
||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile app_version provenance)
|
||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
||||
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
||||
|
||||
@@ -451,6 +451,10 @@ constexpr const char* kIdBankCopySel = "BANK_COPY_SELECTED";
|
||||
constexpr const char* kIdBankRemoveSel = "BANK_REMOVE_SELECTED";
|
||||
constexpr const char* kIdBankPoolFull = "BANK_POOL_FULLHEIGHT";
|
||||
constexpr const char* kIdBankBanksFull = "BANK_BANKS_FULLHEIGHT";
|
||||
// Phase R (Reclaim), R2: the FOREVER-STABLE "Prune bank folder" id. Registered NOW so
|
||||
// in-DAW dry-run verification is possible; R2 behaviour is REPORT-ONLY (no deletion),
|
||||
// and R3 extends the confirm-and-delete step behind this SAME id — never a throwaway id.
|
||||
constexpr const char* kIdBankPruneFolder = "BANK_PRUNE_FOLDER";
|
||||
|
||||
int g_cmdBankCreate = 0;
|
||||
int g_cmdBankRename = 0;
|
||||
@@ -463,6 +467,7 @@ int g_cmdBankCopySel = 0;
|
||||
int g_cmdBankRemoveSel = 0;
|
||||
int g_cmdBankPoolFull = 0;
|
||||
int g_cmdBankBanksFull = 0;
|
||||
int g_cmdBankPruneFolder = 0;
|
||||
|
||||
gaccel_register_t g_accelBankCreate{};
|
||||
gaccel_register_t g_accelBankRename{};
|
||||
@@ -475,6 +480,7 @@ gaccel_register_t g_accelBankCopySel{};
|
||||
gaccel_register_t g_accelBankRemoveSel{};
|
||||
gaccel_register_t g_accelBankPoolFull{};
|
||||
gaccel_register_t g_accelBankBanksFull{};
|
||||
gaccel_register_t g_accelBankPruneFolder{};
|
||||
|
||||
// Persists the book after a bank mutation. Mirrors the CAPTURE path (main.cpp
|
||||
// RunCapture), NOT the Design-View path: a bank change is held in-session and written
|
||||
@@ -791,6 +797,31 @@ void doBankRemoveSelected() {
|
||||
if (removed > 0) persistBankOp("ReaSampler: remove sample(s)");
|
||||
}
|
||||
|
||||
// Prune bank folder — Phase R (Reclaim), R2: REPORT-ONLY dry run. Asks the session for
|
||||
// the orphan set of the resolved CURRENT bank folder ((owned ∩ present) − referenced,
|
||||
// unioned across every bank) and prints the truthful reclaim report — count, reclaimable
|
||||
// bytes, and the file list. DELETES NOTHING, writes no ext-state, opens no undo point
|
||||
// (pruneDryRun is read-only across the persist seam). R3 extends this SAME action id to
|
||||
// confirm-and-delete behind the dry-run guardrail; the report path here is what R3 wraps.
|
||||
void doBankPruneFolder() {
|
||||
const PruneReport report = g_session->pruneDryRun();
|
||||
|
||||
if (report.count == 0) {
|
||||
ShowConsoleMsg("ReaSampler prune (dry run): no orphaned files to reclaim.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string msg = "ReaSampler prune (dry run): " + std::to_string(report.count) +
|
||||
" orphaned file(s), " + std::to_string(report.totalBytes) +
|
||||
" bytes reclaimable. (Dry run -- nothing deleted.)\n";
|
||||
for (const std::string& rel : report.orphans) msg += " " + rel + "\n";
|
||||
if (report.truncated) {
|
||||
msg += " ... (" + std::to_string(report.count - report.orphans.size()) +
|
||||
" more not shown)\n";
|
||||
}
|
||||
ShowConsoleMsg(msg.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Persists a completed bank-index verb as a SINGLE batched REAPER undo point (R-B) —
|
||||
@@ -851,6 +882,10 @@ void bankRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session)
|
||||
"toggle pool full-height");
|
||||
g_cmdBankBanksFull = registerAction(rec, kIdBankBanksFull, g_accelBankBanksFull,
|
||||
"toggle banks full-height");
|
||||
// Phase R, R2: the "Prune bank folder" action (report-only in this wave; R3 extends
|
||||
// the confirm-and-delete step behind this SAME forever-stable id).
|
||||
g_cmdBankPruneFolder = registerAction(rec, kIdBankPruneFolder, g_accelBankPruneFolder,
|
||||
"prune bank folder");
|
||||
}
|
||||
|
||||
bool bankHandleCommand(int command) {
|
||||
@@ -867,6 +902,7 @@ bool bankHandleCommand(int command) {
|
||||
if (command == g_cmdBankRemoveSel) { doBankRemoveSelected(); return true; }
|
||||
if (command == g_cmdBankPoolFull) { bankPanelToggledPoolFullHeight(); return true; }
|
||||
if (command == g_cmdBankBanksFull) { bankPanelToggledBanksFullHeight(); return true; }
|
||||
if (command == g_cmdBankPruneFolder) { doBankPruneFolder(); return true; }
|
||||
|
||||
return false; // not ours — caller's hookcommand keeps looking
|
||||
}
|
||||
@@ -874,6 +910,8 @@ bool bankHandleCommand(int command) {
|
||||
void bankUnregisterActions(reaper_plugin_info_t* rec) {
|
||||
// Mirror-unregister with '-'-prefixed strings, reverse of registration order. Each
|
||||
// '-command_id' re-presents the same interned channel-qualified id (channelIdFor).
|
||||
rec->Register("-gaccel", (void*)&g_accelBankPruneFolder);
|
||||
rec->Register("-command_id", (void*)channelIdFor(kIdBankPruneFolder));
|
||||
rec->Register("-gaccel", (void*)&g_accelBankBanksFull);
|
||||
rec->Register("-command_id", (void*)channelIdFor(kIdBankBanksFull));
|
||||
rec->Register("-gaccel", (void*)&g_accelBankPoolFull);
|
||||
|
||||
@@ -191,6 +191,13 @@ BankPaths deriveBankPaths(const std::string& projectDir,
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string bankRelativeForName(const std::string& fileName) {
|
||||
if (fileName.empty()) return {};
|
||||
// The SAME expression deriveBankPaths uses for relativePath, kept in one place so
|
||||
// the two spellings can never drift (Phase R spelling-consistency invariant).
|
||||
return std::string(kBankSubfolder) + "/" + fileName;
|
||||
}
|
||||
|
||||
std::string resolveBankFile(const std::string& projectDir,
|
||||
const std::string& relativePath) {
|
||||
// No default-location fallback (CLAUDE.md invariant): an empty project dir or
|
||||
|
||||
@@ -92,6 +92,18 @@ BankPaths deriveBankPaths(const std::string& projectDir,
|
||||
const std::string& baseName,
|
||||
const std::string& uniqueTag);
|
||||
|
||||
// The project-relative index spelling for a bank file KNOWN ONLY by its file name —
|
||||
// the forward derivation the Phase R prune shell uses to spell an ENUMERATED folder
|
||||
// entry the SAME way deriveBankPaths spelled it at capture time. By construction it
|
||||
// is the identical expression deriveBankPaths().relativePath uses (kBankSubfolder +
|
||||
// "/" + fileName), so a file the capture path created and a directory listing of that
|
||||
// same file resolve to the byte-identical relative string — the safety-critical
|
||||
// spelling-consistency the prune core's exact-string match depends on (a divergence
|
||||
// here could make a referenced file look like an orphan). fileName is a bare entry
|
||||
// name (no directory component); the caller supplies forward-slash-free names from the
|
||||
// folder enumeration. Empty in -> empty out.
|
||||
std::string bankRelativeForName(const std::string& fileName);
|
||||
|
||||
// --- Persist-side path arithmetic (M4) --------------------------------------
|
||||
//
|
||||
// The index stores relative paths only; on project load the persist shell must
|
||||
|
||||
@@ -77,10 +77,13 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "app_version.h"
|
||||
#include "capture_paths.h"
|
||||
#include "prune_reconcile.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
@@ -243,6 +246,67 @@ bool ReaSamplerSession::saveToActiveProject() {
|
||||
|
||||
namespace {
|
||||
|
||||
// The dry-run file-list display cap: the orphan COUNT and reclaimed SIZE are always
|
||||
// exact (tallied over the full orphan set), but the enumerated file list handed to the
|
||||
// console is clipped to this many entries so a project with thousands of orphans does
|
||||
// not flood the report. PruneReport::truncated flags the clip. R3's confirm surface can
|
||||
// choose its own presentation; this is purely the Wave-2 dry-run readout ceiling.
|
||||
constexpr std::size_t kPruneListDisplayCap = 64;
|
||||
|
||||
} // namespace
|
||||
|
||||
PruneReport ReaSamplerSession::pruneDryRun() const {
|
||||
PruneReport report;
|
||||
|
||||
std::string rppPath;
|
||||
void* proj = readActiveProject(rppPath);
|
||||
if (!proj || rppPath.empty()) return report; // no active/saved project -> nothing
|
||||
|
||||
// Resolve the CURRENT bank folder the same way the index does (M4): project dir of
|
||||
// the live .rpp + the fixed bank subfolder. Never a stored absolute path, so a
|
||||
// Save-As relocation is followed automatically. resolveBankFile is the shared M4
|
||||
// arithmetic; feeding it the bank subfolder as the "relative path" yields the folder.
|
||||
const std::string projectDir = projectDirOf(rppPath);
|
||||
const std::string bankDir = resolveBankFile(projectDir, kBankSubfolder);
|
||||
if (bankDir.empty()) return report; // unresolvable (no project dir) -> nothing
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(bankDir, ec) || !fs::is_directory(bankDir, ec)) {
|
||||
return report; // no bank folder captured yet -> nothing to reclaim
|
||||
}
|
||||
|
||||
// Enumerate the folder into project-relative index-spelled paths, spelled the SAME
|
||||
// way the capture path spelled them (bankRelativeForName == deriveBankPaths's
|
||||
// convention) so the pure core's exact-string match lines up with referencedPaths()
|
||||
// and the manifest. Non-recursive: the bank folder is flat (capture writes files
|
||||
// directly here); skip any subdirectory. Size is stat'd here and cached by relative
|
||||
// path so the report's byte tally reuses the same on-disk read.
|
||||
std::vector<std::string> present;
|
||||
std::unordered_map<std::string, std::uint64_t> sizeByRel;
|
||||
for (const auto& entry : fs::directory_iterator(bankDir, ec)) {
|
||||
if (ec) break;
|
||||
std::error_code fec;
|
||||
if (!entry.is_regular_file(fec)) continue; // skip subdirs / specials
|
||||
const std::string name = entry.path().filename().string();
|
||||
const std::string rel = bankRelativeForName(name);
|
||||
if (rel.empty()) continue;
|
||||
present.push_back(rel);
|
||||
const std::uintmax_t sz = entry.file_size(fec);
|
||||
sizeByRel[rel] = fec ? 0 : static_cast<std::uint64_t>(sz);
|
||||
}
|
||||
|
||||
// The decision lives in the pure core — read-only inputs from the session's book and
|
||||
// manifest (NO save, NO MarkProjectDirty, NO mutation). referencedPaths() unions
|
||||
// across the whole book (pool included); owned().paths() is the manifest set. The
|
||||
// count / byte-sum / display-truncation tally is the pure buildPruneReport, so this
|
||||
// shell only enumerates, resolves, and stats — no report logic re-implemented here.
|
||||
const std::vector<std::string> orphans =
|
||||
pruneOrphans(present, book_.referencedPaths(), owned_.paths());
|
||||
return buildPruneReport(orphans, sizeByRel, kPruneListDisplayCap);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Load the Design-View model from a project's view_state key, or return a fresh
|
||||
// default. An absent/empty key (older project with no view state) yields a
|
||||
// default-constructed model (Arrange + Design seeded, active = Arrange) — graceful,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "bank_book.h"
|
||||
#include "bank_model.h"
|
||||
#include "owned_manifest.h"
|
||||
#include "prune_reconcile.h"
|
||||
#include "tail_control.h"
|
||||
#include "view_mode_model.h"
|
||||
|
||||
@@ -179,6 +180,21 @@ public:
|
||||
// no dangling no-effect undo entry is opened on an unsaved project.
|
||||
bool saveToActiveProject();
|
||||
|
||||
// Compute the Phase R prune dry-run for the ACTIVE project (Wave 2 — REPORT ONLY,
|
||||
// deletes nothing). Enumerates the resolved CURRENT bank folder (the SAME M4 project-
|
||||
// relative machinery the index/persist use — never a stale absolute path, so it is
|
||||
// correct across a Save-As relocation), spells every enumerated entry with the index's
|
||||
// own convention (bankRelativeForName — byte-identical to the capture path's spelling),
|
||||
// and feeds the R1 pure core with (present, book().referencedPaths(), owned().paths()).
|
||||
// Returns the orphan count + reclaimable bytes + the (possibly display-truncated) file
|
||||
// list. The decision stays in the pure core — this method only enumerates, resolves,
|
||||
// and stats. READ-ONLY across the whole persist seam: it writes NO ext-state, calls no
|
||||
// save / MarkProjectDirty, and mutates neither the book, the manifest, nor any file.
|
||||
//
|
||||
// Yields an empty report (count 0) when there is no active/saved project or no bank
|
||||
// folder on disk yet — an unsaved or never-captured project has nothing to reclaim.
|
||||
PruneReport pruneDryRun() const;
|
||||
|
||||
// Poll the active project. Detects a project load (active project changed)
|
||||
// and a Save-As (active project's .rpp path changed) and reacts accordingly.
|
||||
// Intended to be driven by REAPER's "timer" register. Idempotent per tick.
|
||||
|
||||
@@ -33,4 +33,23 @@ std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
|
||||
return orphans;
|
||||
}
|
||||
|
||||
PruneReport buildPruneReport(
|
||||
const std::vector<std::string>& orphans,
|
||||
const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
|
||||
std::size_t displayCap) {
|
||||
PruneReport report;
|
||||
report.count = orphans.size();
|
||||
for (const std::string& o : orphans) {
|
||||
// Sum EVERY orphan's bytes (the exact reclaimable total), not just the displayed
|
||||
// ones. A path with no stat'd size contributes 0 — never dropped, never negative.
|
||||
const auto it = sizeByPath.find(o);
|
||||
report.totalBytes += (it != sizeByPath.end()) ? it->second : 0;
|
||||
// Display list is capped (0 = uncapped). count stays exact above regardless.
|
||||
if (displayCap == 0 || report.orphans.size() < displayCap)
|
||||
report.orphans.push_back(o);
|
||||
}
|
||||
report.truncated = report.orphans.size() < report.count;
|
||||
return report;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -41,11 +41,36 @@
|
||||
// match here (e.g. case-insensitive compare) would be the unsafe direction — it could
|
||||
// let one spelling of a referenced file be treated as an orphan under another.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The dry-run prune result (Phase R, Wave 2 — report only, no deletion). The thin
|
||||
// prune shell (persist) fills this from pruneOrphans() + a per-file size stat and hands
|
||||
// it to the report surface; R3 will act on the SAME set behind the confirm guardrail.
|
||||
// REAPER-free / filesystem-free by design (the shell does the I/O; this is just the
|
||||
// tallied outcome), so the count/size aggregation is unit-testable outside the DAW.
|
||||
//
|
||||
// * count — number of orphan files (== orphans.size(); the AUTHORITATIVE tally,
|
||||
// exact even when `orphans` below is a truncated display list).
|
||||
// * totalBytes — sum of the on-disk sizes of the orphan files, in bytes (reclaimable
|
||||
// space). A file the stat could not size contributes 0 (never negative).
|
||||
// * orphans — the orphan file list as project-relative index-spelled paths, in
|
||||
// folder-enumeration order (deterministic). MAY be truncated for a large
|
||||
// set (the shell's display cap); `count` stays exact regardless, and
|
||||
// `truncated` says whether the list was clipped.
|
||||
// * truncated — true iff `orphans` holds fewer than `count` entries (a large set was
|
||||
// clipped for display); false when the list is complete.
|
||||
struct PruneReport {
|
||||
std::size_t count = 0;
|
||||
std::uint64_t totalBytes = 0;
|
||||
std::vector<std::string> orphans;
|
||||
bool truncated = false;
|
||||
};
|
||||
|
||||
// Computes the prune orphan set: (owned ∩ present) − referenced.
|
||||
//
|
||||
// Returns the subset of `present` that is BOTH owned AND unreferenced, in the ORDER
|
||||
@@ -60,4 +85,24 @@ std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
|
||||
const std::vector<std::string>& referenced,
|
||||
const std::vector<std::string>& owned);
|
||||
|
||||
// Tallies a dry-run PruneReport from a computed orphan set and a per-path size lookup.
|
||||
// PURE (no I/O): the shell does the folder stat and passes the sizes in `sizeByPath`;
|
||||
// this owns the count / byte-sum / display-truncation decision so it is unit-testable.
|
||||
//
|
||||
// * count == orphans.size() (the authoritative tally, exact regardless of the cap).
|
||||
// * totalBytes == the sum of sizeByPath[o] over EVERY orphan o (not just the displayed
|
||||
// ones); a path missing from sizeByPath contributes 0 (an orphan whose
|
||||
// size could not be stat'd — never negative, never dropped from the sum).
|
||||
// * orphans == the first `displayCap` orphans in input order (the deterministic
|
||||
// folder-enumeration order pruneOrphans preserved); the whole set when
|
||||
// count <= displayCap. displayCap == 0 means "no display cap" (whole set).
|
||||
// * truncated == count > orphans.size() (a large set was clipped for display).
|
||||
//
|
||||
// Kept separate from pruneOrphans so the safety-critical set algebra stays a pure function
|
||||
// of three sets, while the presentation tally (which the R2 dry-run and R3 confirm both
|
||||
// need) is its own small, testable step.
|
||||
PruneReport buildPruneReport(const std::vector<std::string>& orphans,
|
||||
const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
|
||||
std::size_t displayCap);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -547,6 +547,29 @@ static void testHashWavContentDomainSeparationFromWholeFile() {
|
||||
CHECK(contentHash != wholeHash);
|
||||
}
|
||||
|
||||
// --- bankRelativeForName spelling consistency (Phase R, R2) -----------------
|
||||
//
|
||||
// The safety-critical property: the relative spelling the prune shell derives for an
|
||||
// ENUMERATED folder entry (bankRelativeForName) must be byte-identical to the spelling
|
||||
// the capture path stored in the index (deriveBankPaths().relativePath) for the same
|
||||
// file name. A divergence here could make a referenced file look like an orphan.
|
||||
|
||||
static void testBankRelativeForNameMatchesDerivePathSpelling() {
|
||||
// For a file the capture path created, deriveBankPaths produced relativePath;
|
||||
// a directory listing yields the bare file name. bankRelativeForName(name) must
|
||||
// reproduce the SAME string, or the pure core's exact-string match misfires.
|
||||
const BankPaths p = deriveBankPaths("/proj", "kick", "001");
|
||||
// p.fileName is the on-disk entry name a folder enumeration would return.
|
||||
CHECK(bankRelativeForName(p.fileName) == p.relativePath);
|
||||
}
|
||||
|
||||
static void testBankRelativeForNameConventionAndEdge() {
|
||||
// The convention verbatim: "reasampler_bank/<name>" (the one place the spelling lives).
|
||||
CHECK(bankRelativeForName("a.wav") == "reasampler_bank/a.wav");
|
||||
// Empty in -> empty out (a defensive guard; a real enumeration never yields "").
|
||||
CHECK(bankRelativeForName("").empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testNormalizeSlashes();
|
||||
testNormalizeSlashesCaseFolding();
|
||||
@@ -585,6 +608,8 @@ int main() {
|
||||
testHashWavContentEmptyFallsBackToHashBytes();
|
||||
testHashWavContentListMetaSkipped();
|
||||
testHashWavContentDomainSeparationFromWholeFile();
|
||||
testBankRelativeForNameMatchesDerivePathSpelling();
|
||||
testBankRelativeForNameConventionAndEdge();
|
||||
|
||||
if (g_fail == 0) std::printf("capture_paths: all tests passed\n");
|
||||
else std::printf("capture_paths: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user