a927dad2f4
Four collisions answered explicitly: ids reminted, names never overwritten, content deduped before the write, bank name auto-suffixed. Degraded ledger refuses before the picker.
371 lines
16 KiB
C++
371 lines
16 KiB
C++
// Standalone tests for reasampler::package::import_plan — no REAPER, no filesystem,
|
|
// no test framework. Every one of the four collision classes (sample id, bank-folder
|
|
// file name, content hash, bank display name) is exercised here, which is the point of
|
|
// the module: the whole collision rule set is decidable from strings and hashes.
|
|
|
|
#include "../src/core/package/import_plan.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "../src/core/tracking/tracking_authority.h"
|
|
|
|
using namespace reasampler;
|
|
using namespace reasampler::package;
|
|
using reasampler::model::Sample;
|
|
using reasampler::model::SlotMap;
|
|
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if(!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
|
|
static const char* kProjectDir = "/proj";
|
|
static const char* kTag = "1754000000";
|
|
|
|
// --- fixtures ----------------------------------------------------------------
|
|
|
|
static PackageEntry entry(const std::string& fileName, const std::string& id,
|
|
const std::string& hash) {
|
|
PackageEntry e;
|
|
e.fileName = fileName;
|
|
e.byteLength = 64;
|
|
e.byteHash = "0011223344556677";
|
|
e.sample.id = id;
|
|
e.sample.displayName = id;
|
|
e.sample.relativePath = "reasampler_bank/" + fileName;
|
|
e.sample.contentHash = hash;
|
|
return e;
|
|
}
|
|
|
|
static PackageManifest manifestOf(std::vector<PackageEntry> entries,
|
|
const std::string& bankName) {
|
|
PackageManifest m;
|
|
m.bankDisplayName = bankName;
|
|
m.entries = std::move(entries);
|
|
return m;
|
|
}
|
|
|
|
// A book carrying the named banks, in order, each with a caller-supplied id.
|
|
static BankBook bookWithBanks(const std::vector<std::string>& names) {
|
|
BankBook book;
|
|
for (std::size_t i = 0; i < names.size(); ++i)
|
|
book.createBank("bank-" + std::to_string(i), names[i]);
|
|
return book;
|
|
}
|
|
|
|
static const PlannedEntry& landed(const ImportPlan& plan, std::size_t manifestIndex) {
|
|
return plan.entries[manifestIndex];
|
|
}
|
|
|
|
// --- the bank-name probe (collision class 4) ---------------------------------
|
|
|
|
static std::string plannedName(const std::vector<std::string>& existingBanks,
|
|
const std::string& packageBankName) {
|
|
const BankBook book = bookWithBanks(existingBanks);
|
|
return planImport(manifestOf({}, packageBankName), book, kProjectDir, {}, kTag)
|
|
.bankDisplayName;
|
|
}
|
|
|
|
static void testFreeSeedIsKeptVerbatim() {
|
|
CHECK(plannedName({"Percussion"}, "Drums") == "Drums");
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({}, "Drums"), bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(!plan.bankNameAdjusted);
|
|
CHECK(plan.seedBankName == "Drums");
|
|
}
|
|
|
|
static void testFoldedCollisionTakesTheFirstSuffix() {
|
|
// The book's fold is case- and whitespace-insensitive, so "drums" blocks "Drums".
|
|
CHECK(plannedName({"drums"}, "Drums") == "Drums 2");
|
|
CHECK(plannedName({" DRUMS "}, "Drums") == "Drums 2");
|
|
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({}, "Drums"), bookWithBanks({"drums"}), kProjectDir, {}, kTag);
|
|
CHECK(plan.bankNameAdjusted);
|
|
CHECK(plan.seedBankName == "Drums"); // the message needs what was asked for
|
|
}
|
|
|
|
static void testProbeFillsAGap() {
|
|
// First-free-ascending, not highest-plus-one: "Drums 2" is free, so it wins.
|
|
CHECK(plannedName({"Drums", "Drums 3"}, "Drums") == "Drums 2");
|
|
}
|
|
|
|
static void testSeedIsNeverReparsed() {
|
|
// "Drums 2" colliding lands as "Drums 2 2", NOT "Drums 3" — a bare trailing integer
|
|
// cannot be told from a name the user wrote.
|
|
CHECK(plannedName({"Drums 2"}, "Drums 2") == "Drums 2 2");
|
|
CHECK(plannedName({"Kit 808"}, "Kit 808") == "Kit 808 2");
|
|
}
|
|
|
|
static void testBlankRecordedNameFallsBackToTheDefault() {
|
|
CHECK(plannedName({}, "") == kDefaultImportBankName);
|
|
CHECK(plannedName({}, " \t ") == kDefaultImportBankName);
|
|
// And the fallback is a seed like any other, so a second one suffixes.
|
|
CHECK(plannedName({kDefaultImportBankName}, "") ==
|
|
std::string(kDefaultImportBankName) + " 2");
|
|
}
|
|
|
|
static void testPoolExportLandsAsANamedBank() {
|
|
// The destination's pool always exists and always carries the protected name
|
|
// "Pool", so a pool export imports as a NAMED bank "Pool 2" — intended, not a glitch.
|
|
CHECK(plannedName({}, "Pool") == "Pool 2");
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({}, "Pool"), bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(plan.bankNameAdjusted);
|
|
}
|
|
|
|
static void testRepeatedImportsWalkTheSuffixUpwards() {
|
|
CHECK(plannedName({"B"}, "B") == "B 2");
|
|
CHECK(plannedName({"B", "B 2"}, "B") == "B 3");
|
|
}
|
|
|
|
// --- sample ids (collision class 1) ------------------------------------------
|
|
|
|
static void testEveryIdIsRemintedUnderTheImportPrefix() {
|
|
const PackageManifest m = manifestOf({entry("kick.wav", "cap-1-kick.wav", "h1"),
|
|
entry("snare.wav", "cap-2-snare.wav", "h2")},
|
|
"Drums");
|
|
const ImportPlan plan = planImport(m, bookWithBanks({}), kProjectDir, {}, kTag);
|
|
|
|
CHECK(plan.landCount == 2);
|
|
for (const PlannedEntry& e : plan.entries) {
|
|
CHECK(e.sample.id.rfind(kImportIdPrefix, 0) == 0);
|
|
CHECK(e.sample.id != "cap-1-kick.wav");
|
|
CHECK(e.sample.id != "cap-2-snare.wav");
|
|
}
|
|
CHECK(plan.entries[0].sample.id != plan.entries[1].sample.id);
|
|
}
|
|
|
|
static void testReimportingIntoTheSourceProjectRemintsRatherThanCollides() {
|
|
// The package came FROM this project, so its ids are the ones already in use.
|
|
BankBook book = bookWithBanks({"B"});
|
|
Sample existing;
|
|
existing.id = "cap-1-kick.wav";
|
|
existing.relativePath = "reasampler_bank/kick.wav";
|
|
existing.contentHash = "h1";
|
|
book.index("bank-0")->add(existing);
|
|
|
|
const PackageManifest m = manifestOf({entry("kick.wav", "cap-1-kick.wav", "h1")}, "B");
|
|
const ImportPlan plan =
|
|
planImport(m, book, kProjectDir, {"kick.wav"}, kTag);
|
|
|
|
CHECK(plan.bankDisplayName == "B 2");
|
|
CHECK(landed(plan, 0).sample.id != "cap-1-kick.wav");
|
|
// The hash lives in another bank; cross-bank dedup is deliberately not enforced,
|
|
// so the entry still lands rather than collapsing onto B's copy.
|
|
CHECK(landed(plan, 0).action == EntryAction::Land);
|
|
CHECK(landed(plan, 0).renamed);
|
|
}
|
|
|
|
static void testParentIsRemappedWhenItTravelledInThePackage() {
|
|
PackageEntry parent = entry("kick.wav", "cap-parent", "h1");
|
|
PackageEntry child = entry("kick_r2.wav", "cap-child", "h2");
|
|
child.sample.provenance = model::Provenance{"cap-parent", "fx-snapshot"};
|
|
|
|
const ImportPlan plan = planImport(manifestOf({parent, child}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
|
|
CHECK(landed(plan, 1).sample.provenance.has_value());
|
|
CHECK(landed(plan, 1).sample.provenance->parentSampleId ==
|
|
landed(plan, 0).sample.id);
|
|
CHECK(landed(plan, 1).sample.provenance->fxChainSnapshot == "fx-snapshot");
|
|
}
|
|
|
|
static void testParentIsRemappedEvenWhenItFollowsTheChild() {
|
|
// Manifest order does not constrain lineage, so the remap runs after every id is minted.
|
|
PackageEntry child = entry("kick_r2.wav", "cap-child", "h2");
|
|
child.sample.provenance = model::Provenance{"cap-parent", ""};
|
|
PackageEntry parent = entry("kick.wav", "cap-parent", "h1");
|
|
|
|
const ImportPlan plan = planImport(manifestOf({child, parent}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(landed(plan, 0).sample.provenance->parentSampleId == landed(plan, 1).sample.id);
|
|
}
|
|
|
|
static void testForeignParentIsClearedNotCarried() {
|
|
PackageEntry child = entry("kick.wav", "cap-child", "h1");
|
|
child.sample.provenance = model::Provenance{"cap-not-in-this-package", "fx"};
|
|
|
|
const ImportPlan plan = planImport(manifestOf({child}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(landed(plan, 0).sample.provenance.has_value());
|
|
CHECK(landed(plan, 0).sample.provenance->parentSampleId.empty());
|
|
CHECK(landed(plan, 0).sample.provenance->fxChainSnapshot == "fx");
|
|
}
|
|
|
|
// --- bank-folder file names (collision class 2) ------------------------------
|
|
|
|
static void testAFreeBankLegalNameIsKept() {
|
|
const ImportPlan plan = planImport(manifestOf({entry("kick.wav", "a", "h1")}, "Drums"),
|
|
bookWithBanks({}), kProjectDir,
|
|
{"unrelated.wav"}, kTag);
|
|
CHECK(landed(plan, 0).destFileName == "kick.wav");
|
|
CHECK(!landed(plan, 0).renamed);
|
|
CHECK(plan.renameCount == 0);
|
|
CHECK(landed(plan, 0).sample.relativePath == "reasampler_bank/kick.wav");
|
|
}
|
|
|
|
static void testATakenNameIsMintedFreshAndNeverOverwritten() {
|
|
const ImportPlan plan = planImport(manifestOf({entry("kick.wav", "a", "h1")}, "Drums"),
|
|
bookWithBanks({}), kProjectDir,
|
|
{"kick.wav"}, kTag);
|
|
CHECK(landed(plan, 0).destFileName != "kick.wav");
|
|
CHECK(landed(plan, 0).renamed);
|
|
CHECK(plan.renameCount == 1);
|
|
CHECK(landed(plan, 0).sample.relativePath ==
|
|
"reasampler_bank/" + landed(plan, 0).destFileName);
|
|
}
|
|
|
|
static void testTheFolderNameCheckFoldsAsciiCase() {
|
|
// Windows and the default APFS would land "kick.wav" onto "KICK.WAV".
|
|
const ImportPlan plan = planImport(manifestOf({entry("kick.wav", "a", "h1")}, "Drums"),
|
|
bookWithBanks({}), kProjectDir,
|
|
{"KICK.WAV"}, kTag);
|
|
CHECK(landed(plan, 0).destFileName != "kick.wav");
|
|
CHECK(landed(plan, 0).renamed);
|
|
}
|
|
|
|
static void testTwoEntriesNeverLandOnOneName() {
|
|
// Two package names that differ only by case are one destination file.
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({entry("kick.wav", "a", "h1"), entry("Kick.wav", "b", "h2")},
|
|
"Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(plan.landCount == 2);
|
|
CHECK(landed(plan, 0).destFileName != landed(plan, 1).destFileName);
|
|
}
|
|
|
|
static void testAnUnsanitaryNameIsMintedRatherThanLandedVerbatim() {
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({entry("Hit One.wav", "a", "h1")}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(landed(plan, 0).destFileName.find(' ') == std::string::npos);
|
|
CHECK(landed(plan, 0).renamed);
|
|
}
|
|
|
|
// --- content hash (collision class 3) ----------------------------------------
|
|
|
|
static void testAnAlreadyLandedHashCollapsesWithoutAWrite() {
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({entry("kick.wav", "a", "same"),
|
|
entry("kick_copy.wav", "b", "same")},
|
|
"Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
|
|
CHECK(plan.landCount == 1);
|
|
CHECK(plan.collapseCount == 1);
|
|
CHECK(landed(plan, 0).action == EntryAction::Land);
|
|
CHECK(landed(plan, 1).action == EntryAction::Collapse);
|
|
// No name is claimed for it — a dedup that wrote a file would manufacture an orphan.
|
|
CHECK(landed(plan, 1).destFileName.empty());
|
|
// Every manifest entry still yields exactly one planned entry: planImport is total.
|
|
CHECK(plan.entries.size() == 2);
|
|
}
|
|
|
|
static void testAParentPointingAtACollapsedEntryResolvesToTheSurvivor() {
|
|
PackageEntry first = entry("kick.wav", "cap-first", "same");
|
|
PackageEntry dupe = entry("kick_copy.wav", "cap-dupe", "same");
|
|
PackageEntry child = entry("kick_r2.wav", "cap-child", "other");
|
|
child.sample.provenance = model::Provenance{"cap-dupe", ""};
|
|
|
|
const ImportPlan plan = planImport(manifestOf({first, dupe, child}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(landed(plan, 2).sample.provenance->parentSampleId == landed(plan, 0).sample.id);
|
|
}
|
|
|
|
static void testAnEmptyHashNeverCollapses() {
|
|
// Mirrors findByHash: an unhashable entry does not participate in dedup.
|
|
const ImportPlan plan =
|
|
planImport(manifestOf({entry("a.wav", "a", ""), entry("b.wav", "b", "")}, "Drums"),
|
|
bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(plan.landCount == 2);
|
|
CHECK(plan.collapseCount == 0);
|
|
}
|
|
|
|
// --- slots -------------------------------------------------------------------
|
|
|
|
static void testSlotsRideAlongOverTheRemintedIds() {
|
|
PackageManifest m = manifestOf({entry("kick.wav", "cap-a", "h1"),
|
|
entry("snare.wav", "cap-b", "h2")},
|
|
"Drums");
|
|
// A gap the package carried: slot 0 empty, occupants at 1 and 3.
|
|
m.slots = SlotMap::fromEntries({{"cap-a", 1}, {"cap-b", 3}});
|
|
|
|
const ImportPlan plan = planImport(m, bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(plan.slots.slotOf(landed(plan, 0).sample.id) == 1);
|
|
CHECK(plan.slots.slotOf(landed(plan, 1).sample.id) == 3);
|
|
// The package's own ids are gone from the map — a foreign id never enters the index.
|
|
CHECK(plan.slots.slotOf("cap-a") == -1);
|
|
}
|
|
|
|
static void testACollapsedEntryDoesNotDoubleOccupyASlot() {
|
|
PackageManifest m = manifestOf({entry("kick.wav", "cap-a", "same"),
|
|
entry("kick_copy.wav", "cap-b", "same")},
|
|
"Drums");
|
|
m.slots = SlotMap::fromEntries({{"cap-a", 0}, {"cap-b", 1}});
|
|
|
|
const ImportPlan plan = planImport(m, bookWithBanks({}), kProjectDir, {}, kTag);
|
|
CHECK(plan.slots.size() == 1);
|
|
CHECK(plan.slots.slotOf(landed(plan, 0).sample.id) == 0);
|
|
}
|
|
|
|
// --- the ledger gate ---------------------------------------------------------
|
|
|
|
static void testDegradedLedgerStatusesRefuseAndTheUsableOnesDoNot() {
|
|
CHECK(importLedgerRefusal(tracking::LedgerStatus::Unreadable) ==
|
|
LedgerRefusal::Malformed);
|
|
CHECK(importLedgerRefusal(tracking::LedgerStatus::FutureVersion) ==
|
|
LedgerRefusal::FutureVersion);
|
|
CHECK(importLedgerRefusal(tracking::LedgerStatus::Fresh) == LedgerRefusal::None);
|
|
CHECK(importLedgerRefusal(tracking::LedgerStatus::Loaded) == LedgerRefusal::None);
|
|
}
|
|
|
|
static void testAnUndecodableUsageKeyBlocksPruneButNotImport() {
|
|
// The tempting reuse of PruneReport::blockedByTracking would silently refuse an
|
|
// import over a key that only ever governs what a DELETION may touch.
|
|
tracking::OriginLedger ledger;
|
|
wire::UsageFoldResult usage;
|
|
usage.abortPrune = true;
|
|
usage.offendingKeys = {"rsusage_{ABC}"};
|
|
|
|
const tracking::TrackingState state{tracking::LedgerStatus::Loaded, ledger, usage};
|
|
CHECK(tracking::pruneProtection(state).blocked);
|
|
CHECK(importLedgerRefusal(tracking::LedgerStatus::Loaded) == LedgerRefusal::None);
|
|
}
|
|
|
|
int main() {
|
|
testFreeSeedIsKeptVerbatim();
|
|
testFoldedCollisionTakesTheFirstSuffix();
|
|
testProbeFillsAGap();
|
|
testSeedIsNeverReparsed();
|
|
testBlankRecordedNameFallsBackToTheDefault();
|
|
testPoolExportLandsAsANamedBank();
|
|
testRepeatedImportsWalkTheSuffixUpwards();
|
|
|
|
testEveryIdIsRemintedUnderTheImportPrefix();
|
|
testReimportingIntoTheSourceProjectRemintsRatherThanCollides();
|
|
testParentIsRemappedWhenItTravelledInThePackage();
|
|
testParentIsRemappedEvenWhenItFollowsTheChild();
|
|
testForeignParentIsClearedNotCarried();
|
|
|
|
testAFreeBankLegalNameIsKept();
|
|
testATakenNameIsMintedFreshAndNeverOverwritten();
|
|
testTheFolderNameCheckFoldsAsciiCase();
|
|
testTwoEntriesNeverLandOnOneName();
|
|
testAnUnsanitaryNameIsMintedRatherThanLandedVerbatim();
|
|
|
|
testAnAlreadyLandedHashCollapsesWithoutAWrite();
|
|
testAParentPointingAtACollapsedEntryResolvesToTheSurvivor();
|
|
testAnEmptyHashNeverCollapses();
|
|
|
|
testSlotsRideAlongOverTheRemintedIds();
|
|
testACollapsedEntryDoesNotDoubleOccupyASlot();
|
|
|
|
testDegradedLedgerStatusesRefuseAndTheUsableOnesDoNot();
|
|
testAnUndecodableUsageKeyBlocksPruneButNotImport();
|
|
|
|
if (g_fail == 0) std::printf("import_plan: all tests passed\n");
|
|
return g_fail == 0 ? 0 : 1;
|
|
}
|