508 lines
23 KiB
C++
508 lines
23 KiB
C++
// Standalone tests for reasampler::tracking's consolidated answers — no REAPER, no
|
|
// framework. This is the safety-critical file in the territory: it proves that the
|
|
// prune's protected set and the resample's replace-vs-add decision come out of ONE
|
|
// state, that neither ever answers destructively from ambiguity, and that the
|
|
// replace-vs-add universe is a strict subset of the prune-protection universe.
|
|
//
|
|
// The prune half is composed end-to-end against the real pure core
|
|
// (prune_reconcile + BankBook) rather than a mock, so "a tied usage can never reach
|
|
// the orphan set" is proven at the layer that actually deletes.
|
|
|
|
#include "../src/core/tracking/tracking_authority.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
#include "../src/core/model/bank_book.h"
|
|
#include "../src/core/reclaim/prune_reconcile.h"
|
|
|
|
using namespace reasampler;
|
|
using namespace reasampler::tracking;
|
|
using reasampler::wire::DecodedUsage;
|
|
using reasampler::wire::UsageFoldResult;
|
|
using reasampler::wire::UsageHold;
|
|
using reasampler::wire::UsageRecord;
|
|
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if(!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
|
|
// -- fixtures ----------------------------------------------------------------
|
|
|
|
static OriginRecord originOf(const std::string& path, OriginKind kind,
|
|
const std::string& id = "",
|
|
const std::string& parent = "") {
|
|
OriginRecord r;
|
|
r.relativePath = path;
|
|
r.kind = kind;
|
|
r.sampleId = id;
|
|
r.parentSampleId = parent;
|
|
return r;
|
|
}
|
|
|
|
static DecodedUsage usage(const std::string& key, const std::string& trackGuid,
|
|
const std::vector<UsageHold>& holds, bool unioned = false) {
|
|
UsageRecord rec;
|
|
rec.trackGuid = trackGuid;
|
|
rec.ownerNonce = key + "-nonce";
|
|
rec.unioned = unioned;
|
|
rec.holds = holds;
|
|
return DecodedUsage{key, rec};
|
|
}
|
|
|
|
static DecodedUsage unreadable(const std::string& key) {
|
|
return DecodedUsage{key, std::nullopt};
|
|
}
|
|
|
|
static UsageFoldResult foldLive(const std::vector<DecodedUsage>& decoded,
|
|
const std::vector<std::string>& liveTracks) {
|
|
const std::unordered_set<std::string> live(liveTracks.begin(), liveTracks.end());
|
|
return wire::foldUsageRecords(decoded, live, !live.empty());
|
|
}
|
|
|
|
static bool contains(const std::vector<std::string>& v, const std::string& s) {
|
|
return std::find(v.begin(), v.end(), s) != v.end();
|
|
}
|
|
|
|
// Adds a bank entry so the path lands in BankBook::referencedPaths().
|
|
static void addToBank(BankBook& book, const std::string& id, const std::string& path) {
|
|
model::Sample s;
|
|
s.id = id;
|
|
s.displayName = id;
|
|
s.relativePath = path;
|
|
book.activeIndex().add(s);
|
|
}
|
|
|
|
// -- 1. the prune protected set ----------------------------------------------
|
|
// Live-held protected; project-referenced protected; foreign untouchable;
|
|
// system-owned orphan reclaimable — all four from one computation.
|
|
|
|
static void testPruneProtectedSet() {
|
|
// On disk: a referenced capture, a live-held capture, a foreign file, and a
|
|
// system-owned orphan.
|
|
const std::vector<std::string> present = {
|
|
"bank/referenced.wav", "bank/held.wav", "bank/foreign.wav", "bank/orphan.wav"};
|
|
|
|
BankBook book;
|
|
addToBank(book, "S-ref", "bank/referenced.wav");
|
|
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/referenced.wav", OriginKind::Capture, "S-ref"));
|
|
ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held"));
|
|
ledger.record(originOf("bank/orphan.wav", OriginKind::Capture, "S-orphan"));
|
|
// "bank/foreign.wav" is deliberately absent — the system did not create it.
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}})}, {"{T1}"});
|
|
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(!answer.blocked);
|
|
|
|
const std::vector<std::string> orphans = reclaim::pruneOrphans(
|
|
present, reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths),
|
|
answer.ownedPaths);
|
|
|
|
CHECK(orphans.size() == 1);
|
|
CHECK(contains(orphans, "bank/orphan.wav")); // system-owned, unreferenced
|
|
CHECK(!contains(orphans, "bank/referenced.wav")); // a bank references it
|
|
CHECK(!contains(orphans, "bank/held.wav")); // a live instance holds it
|
|
CHECK(!contains(orphans, "bank/foreign.wav")); // never system-created
|
|
}
|
|
|
|
// A capture whose bank entry was REMOVED while an instance kept playing it is still
|
|
// protected — the held-path union, not the index, is what saves it.
|
|
static void testLiveHoldProtectsDeReferencedCapture() {
|
|
const std::vector<std::string> present = {"bank/held.wav"};
|
|
BankBook book; // no entry at all
|
|
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}})}, {"{T1}"});
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
|
|
const std::vector<std::string> orphans = reclaim::pruneOrphans(
|
|
present, reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths),
|
|
answer.ownedPaths);
|
|
CHECK(orphans.empty());
|
|
}
|
|
|
|
// -- 2. unreadable tracking state blocks the prune ---------------------------
|
|
|
|
static void testUnreadableUsageBlocksPruneAndNamesIt() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/orphan.wav", OriginKind::Capture, "S-1"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S-x", "bank/x.wav"}}),
|
|
unreadable("rsusage_BROKEN")},
|
|
{"{T1}"});
|
|
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(answer.blocked);
|
|
CHECK(!answer.ledgerUnreadable);
|
|
CHECK(!answer.ledgerFutureVersion);
|
|
CHECK(answer.unreadableUsageKeys.size() == 1);
|
|
CHECK(answer.unreadableUsageKeys[0] == "rsusage_BROKEN");
|
|
|
|
// The belt-and-braces guard is symmetric: a usage-only block ALSO withholds
|
|
// ownedPaths, even though the ledger itself read fine, so a caller that ignored
|
|
// `blocked` computes an empty orphan set rather than deleting with degraded
|
|
// protection.
|
|
CHECK(answer.ownedPaths.empty());
|
|
CHECK(reclaim::pruneOrphans({"bank/orphan.wav"}, {}, answer.ownedPaths).empty());
|
|
|
|
// heldPaths is the one list that stays populated on a block — it only ever widens
|
|
// the protected set, so withholding it would be the unsafe direction.
|
|
CHECK(contains(answer.heldPaths, "bank/x.wav"));
|
|
}
|
|
|
|
// A ledger written by a NEWER build blocks exactly like a corrupt one, but is
|
|
// reported separately: the operator advice differs (clearing a corrupt blob is
|
|
// repair; clearing a newer build's is destruction).
|
|
static void testFutureVersionLedgerBlocksAndIsReportedSeparately() {
|
|
const LedgerLoad load = loadLedger("{\"v\":99,\"records\":[]}");
|
|
CHECK(load.status == LedgerStatus::FutureVersion);
|
|
|
|
OriginLedger populated;
|
|
populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1"));
|
|
const UsageFoldResult fold = foldLive({}, {});
|
|
|
|
const TrackingState state{load.status, populated, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(answer.blocked);
|
|
CHECK(answer.ledgerFutureVersion);
|
|
CHECK(!answer.ledgerUnreadable);
|
|
CHECK(answer.ownedPaths.empty());
|
|
CHECK(reclaim::pruneOrphans({"bank/would-be-orphan.wav"}, {},
|
|
answer.ownedPaths).empty());
|
|
|
|
// And the replace-vs-add question abstains rather than allowing a replace.
|
|
CHECK(tiedUsageExists(state, "bank/would-be-orphan.wav", "") == Answer::Indeterminate);
|
|
}
|
|
|
|
// An unreadable ledger blocks too, AND leaves ownedPaths empty — so a caller that
|
|
// ignored `blocked` still computes an empty orphan set rather than deleting.
|
|
static void testUnreadableLedgerBlocksAndYieldsNoOrphans() {
|
|
// Deliberately NON-empty: loadLedger() hands back an empty ledger on Unreadable,
|
|
// so an empty fixture here would assert nothing. The guard must suppress records
|
|
// it was given, not merely pass an empty vector through.
|
|
OriginLedger populated;
|
|
populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1"));
|
|
const UsageFoldResult fold = foldLive({}, {});
|
|
|
|
const TrackingState state{LedgerStatus::Unreadable, populated, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(answer.blocked);
|
|
CHECK(answer.ledgerUnreadable);
|
|
CHECK(answer.ownedPaths.empty());
|
|
|
|
// Belt-and-braces: the orphan set computed from this answer is empty even
|
|
// though the file is present, unreferenced, and recorded as owned.
|
|
const std::vector<std::string> orphans = reclaim::pruneOrphans(
|
|
{"bank/would-be-orphan.wav"}, {}, answer.ownedPaths);
|
|
CHECK(orphans.empty());
|
|
}
|
|
|
|
// Both blockers at once must both be reported — the operator needs to fix both.
|
|
static void testBothBlockersReported() {
|
|
// Deliberately NON-empty, like testUnreadableLedgerBlocksAndYieldsNoOrphans: an
|
|
// empty fixture asserts nothing about ownedPaths being withheld, only that it
|
|
// started empty.
|
|
OriginLedger populated;
|
|
populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1"));
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}}),
|
|
unreadable("rsusage_BROKEN")},
|
|
{"{T1}"});
|
|
const TrackingState state{LedgerStatus::Unreadable, populated, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(answer.blocked);
|
|
CHECK(answer.ledgerUnreadable);
|
|
CHECK(answer.unreadableUsageKeys.size() == 1);
|
|
// heldPaths survives a double block: the fold's protect-all set only ever widens
|
|
// what prune protects, so withholding it would be the unsafe direction.
|
|
CHECK(contains(answer.heldPaths, "bank/held.wav"));
|
|
CHECK(answer.ownedPaths.empty());
|
|
|
|
// Belt-and-braces: the orphan set computed from this answer is empty even though
|
|
// the file is present, unreferenced, and recorded as owned.
|
|
const std::vector<std::string> orphans = reclaim::pruneOrphans(
|
|
{"bank/would-be-orphan.wav"}, {}, answer.ownedPaths);
|
|
CHECK(orphans.empty());
|
|
}
|
|
|
|
// A record that exists but whose track hosts no identified instance still protects
|
|
// everything (the zero-identified net) — the prune runs, but reclaims nothing held.
|
|
static void testZeroIdentifiedInstancesStillProtects() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T-GONE}", {UsageHold{"S-held", "bank/held.wav"}})}, {});
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(!answer.blocked);
|
|
CHECK(contains(answer.heldPaths, "bank/held.wav"));
|
|
|
|
const std::vector<std::string> orphans =
|
|
reclaim::pruneOrphans({"bank/held.wav"},
|
|
reclaim::mergeReferenced({}, answer.heldPaths),
|
|
answer.ownedPaths);
|
|
CHECK(orphans.empty());
|
|
}
|
|
|
|
// -- 3. unreadable state never takes the replace branch ----------------------
|
|
|
|
static void testUnreadableStateNeverAnswersNo() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/a.wav", OriginKind::Capture, "S-a"));
|
|
|
|
// Unreadable usage record.
|
|
const UsageFoldResult brokenUsage = foldLive({unreadable("rsusage_BROKEN")}, {"{T1}"});
|
|
const TrackingState usageBad{LedgerStatus::Loaded, ledger, brokenUsage};
|
|
CHECK(tiedUsageExists(usageBad, "bank/a.wav", "") == Answer::Indeterminate);
|
|
|
|
// Unreadable ledger.
|
|
const OriginLedger empty;
|
|
const UsageFoldResult okUsage = foldLive({}, {});
|
|
const TrackingState ledgerBad{LedgerStatus::Unreadable, empty, okUsage};
|
|
CHECK(tiedUsageExists(ledgerBad, "bank/a.wav", "") == Answer::Indeterminate);
|
|
|
|
// An empty capture path is a caller error, not a licence to replace.
|
|
const TrackingState good{LedgerStatus::Loaded, ledger, okUsage};
|
|
CHECK(tiedUsageExists(good, "", "") == Answer::Indeterminate);
|
|
}
|
|
|
|
// -- 4. the tie itself --------------------------------------------------------
|
|
|
|
static void testTiedUsageYesNoAndSelfExclusion() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
|
|
ledger.record(originOf("bank/lonely.wav", OriginKind::Capture, "S-lonely"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}}),
|
|
usage("rsusage_OTHER", "{T2}", {UsageHold{"S-src", "bank/src.wav"}})},
|
|
{"{T1}", "{T2}"});
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
|
|
// Counting every holder: a tie exists.
|
|
CHECK(tiedUsageExists(state, "bank/src.wav", "") == Answer::Yes);
|
|
// Excluding my own key still leaves the other instance's tie.
|
|
CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::Yes);
|
|
// Nobody holds this one.
|
|
CHECK(tiedUsageExists(state, "bank/lonely.wav", "") == Answer::No);
|
|
}
|
|
|
|
// Sole holder excluding itself: definitively No, so the bake may replace in place.
|
|
static void testSoleHolderExcludingItselfAnswersNo() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}})}, {"{T1}"});
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::No);
|
|
}
|
|
|
|
static void testUnionedRecordIsNeverExcludedAsOwn() {
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}},
|
|
/*unioned=*/true)},
|
|
{"{T1}"});
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::Yes);
|
|
}
|
|
|
|
// -- 5. no silent gaps --------------------------------------------------------
|
|
// A system-created file simulated through the same recordCreated path is tracked
|
|
// the instant it exists: an immediately-following prune sees it as owned (and so
|
|
// reclaimable, not foreign), and an immediately-following second creation is
|
|
// unaffected. Its lineage is queryable with no intervening save/load.
|
|
|
|
static void testCreateThenImmediatePruneAndSecondCreate() {
|
|
OriginLedger ledger;
|
|
const UsageFoldResult noUsage = foldLive({}, {});
|
|
|
|
// Create #1 — a resample carrying lineage from birth.
|
|
ledger.record(originOf("bank/bake-1.wav", OriginKind::Resample, "S-1", "S-src"));
|
|
|
|
{
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, noUsage};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(!answer.blocked);
|
|
// Tracked instantly: it is owned, so it is reclaimable rather than foreign.
|
|
CHECK(contains(answer.ownedPaths, "bank/bake-1.wav"));
|
|
const std::vector<std::string> orphans =
|
|
reclaim::pruneOrphans({"bank/bake-1.wav"}, {}, answer.ownedPaths);
|
|
CHECK(orphans.size() == 1);
|
|
// Lineage present with nothing in between.
|
|
CHECK(ledger.find("bank/bake-1.wav")->parentSampleId == "S-src");
|
|
}
|
|
|
|
// Create #2 immediately after — both records intact, neither disturbed.
|
|
ledger.record(originOf("bank/bake-2.wav", OriginKind::Resample, "S-2", "S-1"));
|
|
{
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, noUsage};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(contains(answer.ownedPaths, "bank/bake-1.wav"));
|
|
CHECK(contains(answer.ownedPaths, "bank/bake-2.wav"));
|
|
CHECK(ledger.find("bank/bake-1.wav")->parentSampleId == "S-src");
|
|
CHECK(ledger.find("bank/bake-2.wav")->parentSampleId == "S-1");
|
|
}
|
|
}
|
|
|
|
// -- 6. the pre-existing bank -------------------------------------------------
|
|
// Lifted from a legacy path-only manifest: protections intact, no spurious lineage,
|
|
// and the tie query gives a definite answer rather than abstaining.
|
|
|
|
static void testPreExistingBankKeepsProtectionsAndAnswersDefinitely() {
|
|
const LedgerLoad load = loadLedger("{\"owned\":[\"bank/legacy.wav\"]}");
|
|
CHECK(load.status == LedgerStatus::Loaded);
|
|
CHECK(load.ledger.find("bank/legacy.wav")->parentSampleId.empty());
|
|
|
|
// Foreign files stay untouchable and the legacy file is still reclaimable when
|
|
// nothing references it — exactly the protections it had before the lift.
|
|
const UsageFoldResult noUsage = foldLive({}, {});
|
|
const TrackingState state{load.status, load.ledger, noUsage};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(!answer.blocked);
|
|
|
|
const std::vector<std::string> orphans = reclaim::pruneOrphans(
|
|
{"bank/legacy.wav", "bank/handdropped.wav"}, {}, answer.ownedPaths);
|
|
CHECK(orphans.size() == 1);
|
|
CHECK(contains(orphans, "bank/legacy.wav"));
|
|
CHECK(!contains(orphans, "bank/handdropped.wav"));
|
|
|
|
// Never-recorded is decidable, not indeterminate.
|
|
CHECK(tiedUsageExists(state, "bank/legacy.wav", "") == Answer::No);
|
|
CHECK(tiedUsageExists(state, "bank/handdropped.wav", "") == Answer::No);
|
|
|
|
// And a live hold on the legacy file still ties, lineage record or not.
|
|
const UsageFoldResult held = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S-legacy", "bank/legacy.wav"}})},
|
|
{"{T1}"});
|
|
const TrackingState heldState{load.status, load.ledger, held};
|
|
CHECK(tiedUsageExists(heldState, "bank/legacy.wav", "") == Answer::Yes);
|
|
}
|
|
|
|
// -- 7. the consumers cannot disagree ----------------------------------------
|
|
// Every Yes from the tie query is a path the prune protects, over a constructed
|
|
// record set that mixes live, dead, self-held and unheld paths. The reverse does
|
|
// NOT hold — that asymmetry is the point, so it is asserted too.
|
|
|
|
static void testTiedUniverseIsStrictSubsetOfProtectedUniverse() {
|
|
BankBook book;
|
|
addToBank(book, "S-ref", "bank/bank-only.wav");
|
|
|
|
OriginLedger ledger;
|
|
for (const char* p : {"bank/live-a.wav", "bank/live-b.wav", "bank/self.wav",
|
|
"bank/dead.wav", "bank/bank-only.wav", "bank/unused.wav"})
|
|
ledger.record(originOf(p, OriginKind::Capture, std::string("S") + p));
|
|
|
|
const UsageFoldResult fold = foldLive(
|
|
{usage("rsusage_A", "{T1}", {UsageHold{"S1", "bank/live-a.wav"},
|
|
UsageHold{"S2", "bank/live-b.wav"}}),
|
|
usage("rsusage_ME", "{T1}", {UsageHold{"S3", "bank/self.wav"}}),
|
|
usage("rsusage_DEAD", "{T-GONE}", {UsageHold{"S4", "bank/dead.wav"}})},
|
|
{"{T1}"});
|
|
|
|
const TrackingState state{LedgerStatus::Loaded, ledger, fold};
|
|
const ProtectionAnswer answer = pruneProtection(state);
|
|
CHECK(!answer.blocked);
|
|
|
|
const std::vector<std::string> referenced =
|
|
reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths);
|
|
|
|
const std::vector<std::string> universe = {
|
|
"bank/live-a.wav", "bank/live-b.wav", "bank/self.wav",
|
|
"bank/dead.wav", "bank/bank-only.wav", "bank/unused.wav"};
|
|
|
|
std::size_t tiedCount = 0;
|
|
for (const std::string& path : universe) {
|
|
const Answer tied = tiedUsageExists(state, path, "rsusage_ME");
|
|
if (tied != Answer::Yes) continue;
|
|
++tiedCount;
|
|
// Yes => protected: the path is in the referenced union, so pruneOrphans
|
|
// cannot emit it even though it is present and owned.
|
|
CHECK(contains(referenced, path));
|
|
CHECK(reclaim::pruneOrphans({path}, referenced, answer.ownedPaths).empty());
|
|
}
|
|
CHECK(tiedCount == 2); // live-a, live-b — self is excluded, dead is not live
|
|
|
|
// Strictly narrower: bank-only.wav is protected (a bank references it) and
|
|
// self.wav is protected (a live instance holds it), yet neither is a tie.
|
|
CHECK(contains(referenced, "bank/bank-only.wav"));
|
|
CHECK(tiedUsageExists(state, "bank/bank-only.wav", "rsusage_ME") == Answer::No);
|
|
CHECK(contains(referenced, "bank/self.wav"));
|
|
CHECK(tiedUsageExists(state, "bank/self.wav", "rsusage_ME") == Answer::No);
|
|
}
|
|
|
|
// The resample's own branch off the three answers. Indeterminate must land where Yes does:
|
|
// AddDistinct disturbs no existing holder, so it is the non-destructive side.
|
|
static void testResampleLandingTakesReplaceOnlyOnADefiniteNo() {
|
|
CHECK(resampleLanding(Answer::No) == Landing::Replace);
|
|
CHECK(resampleLanding(Answer::Yes) == Landing::AddDistinct);
|
|
CHECK(resampleLanding(Answer::Indeterminate) == Landing::AddDistinct);
|
|
|
|
// Composed with the query itself, over the three states a real bake meets.
|
|
OriginLedger ledger;
|
|
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
|
|
|
|
// Sole holder, excluding itself -> nothing is tied -> replace.
|
|
const UsageFoldResult sole = foldLive(
|
|
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}})}, {"{T1}"});
|
|
const TrackingState soleState{LedgerStatus::Loaded, ledger, sole};
|
|
CHECK(resampleLanding(tiedUsageExists(soleState, "bank/src.wav", "rsusage_ME")) ==
|
|
Landing::Replace);
|
|
|
|
// A second live instance holds it -> add distinct, so that holder is undisturbed.
|
|
const UsageFoldResult shared = foldLive(
|
|
{usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}}),
|
|
usage("rsusage_OTHER", "{T2}", {UsageHold{"S-src", "bank/src.wav"}})},
|
|
{"{T1}", "{T2}"});
|
|
const TrackingState sharedState{LedgerStatus::Loaded, ledger, shared};
|
|
CHECK(resampleLanding(tiedUsageExists(sharedState, "bank/src.wav", "rsusage_ME")) ==
|
|
Landing::AddDistinct);
|
|
|
|
// A degraded ledger cannot answer -> add distinct rather than take over an entry
|
|
// whose holders are unknown.
|
|
const TrackingState degradedState{LedgerStatus::Unreadable, ledger, sole};
|
|
CHECK(tiedUsageExists(degradedState, "bank/src.wav", "rsusage_ME") ==
|
|
Answer::Indeterminate);
|
|
CHECK(resampleLanding(tiedUsageExists(degradedState, "bank/src.wav", "rsusage_ME")) ==
|
|
Landing::AddDistinct);
|
|
}
|
|
|
|
int main() {
|
|
testResampleLandingTakesReplaceOnlyOnADefiniteNo();
|
|
testPruneProtectedSet();
|
|
testLiveHoldProtectsDeReferencedCapture();
|
|
testUnreadableUsageBlocksPruneAndNamesIt();
|
|
testUnreadableLedgerBlocksAndYieldsNoOrphans();
|
|
testFutureVersionLedgerBlocksAndIsReportedSeparately();
|
|
testBothBlockersReported();
|
|
testZeroIdentifiedInstancesStillProtects();
|
|
testUnreadableStateNeverAnswersNo();
|
|
testTiedUsageYesNoAndSelfExclusion();
|
|
testSoleHolderExcludingItselfAnswersNo();
|
|
testUnionedRecordIsNeverExcludedAsOwn();
|
|
testCreateThenImmediatePruneAndSecondCreate();
|
|
testPreExistingBankKeepsProtectionsAndAnswersDefinitely();
|
|
testTiedUniverseIsStrictSubsetOfProtectedUniverse();
|
|
|
|
if (g_fail == 0) std::printf("tracking_authority: all tests passed\n");
|
|
return g_fail == 0 ? 0 : 1;
|
|
}
|