Files
reasampler/tests/test_fx_reclaim.cpp
T

663 lines
28 KiB
C++

// Standalone tests for the FX reclaimer's drain — no REAPER, no test framework. The
// loop is templated on its host precisely so the properties that make it safe to run in
// the background are assertable here: the debounce, the per-tick budget's between-track
// yield, the queue being a cache of intent that a model change discards, the user's own
// offline plugins surviving an online, the release reaching the persisted model, and —
// via a host that invalidates from inside a write — that a re-entered invalidation
// reaches nothing the drain is using.
#include "../src/shell/view/fx_reclaim.h"
#include <cstdio>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
namespace {
// -- a fake project ------------------------------------------------------------
// One live FX: its identity (empty when REAPER reports none) and whether it is offline.
struct FakeFx {
std::string guid;
bool offline = false;
};
// Stands in for the REAPER half of drainTick's Host contract. The clock advances only
// on a REAL write, mirroring the live host: a compare-before-write that finds the FX
// already where it wants it costs a read, not a plugin load.
struct FakeHost {
std::map<std::string, std::vector<FakeFx>> chains;
std::set<std::string> released;
std::vector<std::string> writeLog;
double clock = 0.0;
double stepCostMs = 50.0;
// Models a re-entered timer landing inside TrackFX_SetOffline's message pump: on the
// Nth successful write, the write itself does to the queue what a nested applyMode
// used to do — clear it and mark it stale.
FxReclaimQueue* reentrant = nullptr;
int invalidateAfterWrite = -1;
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
double nowMs() const { return clock; }
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
auto it = chains.find(guid);
if (it == chains.end()) return false;
out.clear();
for (const FakeFx& fx : it->second) out.push_back(fx.guid);
return true;
}
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
auto it = chains.find(guid);
if (it == chains.end()) return false;
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
if (it->second[static_cast<std::size_t>(fxIndex)].offline == offline) return false;
it->second[static_cast<std::size_t>(fxIndex)].offline = offline;
clock += stepCostMs;
writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on"));
if (reentrant && static_cast<int>(writeLog.size()) == invalidateAfterWrite) {
itemsSeenMidDrain = static_cast<int>(reentrant->items().size());
reentrant->discard();
reentrant->invalidate();
}
return true;
}
// `model` is the live host's session model and `persisted` stands in for view_state
// on disk — the blob a reopen or an undo/redo loadFromProject rebuilds from. Set them
// and the release actually lands, so a persist ordered BEFORE it would serialize a
// record that is still held and fail the reload assertions.
ViewModeModel* model = nullptr;
std::string persisted;
int persists = 0;
void releaseFx(const std::string& guid) {
released.insert(guid);
if (model) model->releaseSnapshotFx(guid);
}
void persistModel() {
++persists;
if (model) persisted = model->serialize();
}
bool offlineAt(const std::string& guid, std::size_t slot) const {
auto it = chains.find(guid);
return it != chains.end() && slot < it->second.size() && it->second[slot].offline;
}
};
// -- model helpers -------------------------------------------------------------
TrackSnapshot recordWithFx(std::vector<FxOfflineState> fx) {
TrackSnapshot snap;
snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 1; snap.fxEnable = 1;
snap.fxOffline = std::move(fx);
return snap;
}
// What the shell does on every invalidation: throw the queue away and recompute it.
std::vector<ReclaimItem> derive(const ViewModeModel& vm, const std::set<std::string>& live) {
return makeQueue(desiredFxState(vm, vm.activeModeId(), live));
}
std::vector<FakeFx> onlineChain(const std::vector<std::string>& guids) {
std::vector<FakeFx> chain;
for (const std::string& g : guids) chain.push_back(FakeFx{g, false});
return chain;
}
std::vector<FakeFx> offlineChain(const std::vector<std::string>& guids) {
std::vector<FakeFx> chain;
for (const std::string& g : guids) chain.push_back(FakeFx{g, true});
return chain;
}
// Design leaves the tool has parked: record held, both halves, nothing user-offlined.
// Active mode stays Arrange, so every one of them is parked.
ViewModeModel parkedDesignLeaves(const std::map<std::string, std::vector<std::string>>& tracks) {
ViewModeModel vm;
for (const auto& [guid, fxGuids] : tracks) {
vm.membership().tag(guid, kDesignModeId);
std::vector<FxOfflineState> fx;
for (const std::string& g : fxGuids) fx.push_back(FxOfflineState{g, 0});
vm.storeSnapshot(guid, recordWithFx(fx));
}
return vm;
}
ViewModeModel parkedDesignLeaf(const std::vector<std::string>& fxGuids) {
return parkedDesignLeaves({{"{T}", fxGuids}});
}
// Puts the queue's offline items past their debounce the way the live loop does: the
// hold starts at the FIRST observation, so advancing the clock before that one just
// moves the origin and matures nothing.
void matureHold(std::vector<ReclaimItem>& items, ParkClock& clock, FakeHost& host) {
clock.observe(items, host.nowMs());
host.clock += kParkHoldMs;
}
// Runs ticks until the queue settles, bounded so a non-terminating drain fails the test
// rather than hanging CTest. The two overloads are the two entry points: the pure drain
// over the caller's own storage, and the owned queue the shell actually runs.
int drainToSettled(std::vector<ReclaimItem>& items, ParkClock& clock, FakeHost& host,
int maxTicks = 64) {
for (int t = 1; t <= maxTicks; ++t)
if (drainTick(items, clock, host).settled) return t;
CHECK(false);
return -1;
}
int drainToSettled(FxReclaimQueue& queue, FakeHost& host, int maxTicks = 64) {
for (int t = 1; t <= maxTicks; ++t)
if (queue.run(host).settled) return t;
CHECK(false); // did not settle — the drain is not making forward progress
return -1;
}
FxReclaimQueue queueOf(const ViewModeModel& vm, const std::set<std::string>& live) {
FxReclaimQueue q;
q.adopt(derive(vm, live));
return q;
}
} // namespace
// -- 1. debounce ---------------------------------------------------------------
static void testFastFlipMovesZeroPluginsButAHeldParkStillReclaims() {
// A→B: {T} parks. The reclaimer must not touch it yet.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
FakeHost host;
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Offline);
CHECK(q.run(host).writes == 0);
CHECK(!q.run(host).settled); // waiting out its hold, not finished
// B→A half a second later: the flags come back and the FX half is still held, so the
// reclaimer is now owed an ONLINE — over a chain it never got round to offlining.
host.clock = 500.0;
vm.setActiveMode(kDesignModeId);
vm.releaseSnapshotFlags("{T}");
q.adopt(derive(vm, {"{T}"}));
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
drainToSettled(q, host);
// The whole point of the 3s hold: a fast A→B→A flip moved NO plugins.
CHECK(host.writeLog.empty());
CHECK(!host.offlineAt("{T}", 0) && !host.offlineAt("{T}", 1) && !host.offlineAt("{T}", 2));
CHECK(host.released.count("{T}") == 1); // the chain is verifiably online; record spent
// The contrast, so the assertion above is about the hold and not about the drain
// being inert: the same park held past 3s DOES reclaim.
ViewModeModel held = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
FakeHost host2;
host2.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
FxReclaimQueue q2 = queueOf(held, {"{T}"});
CHECK(q2.run(host2).writes == 0); // observed parked at t=0
host2.clock = kParkHoldMs;
drainToSettled(q2, host2);
CHECK(host2.writeLog.size() == 3);
CHECK(host2.offlineAt("{T}", 0) && host2.offlineAt("{T}", 1) && host2.offlineAt("{T}", 2));
CHECK(host2.released.empty()); // an OFFLINE never releases — the record is still owed
}
// -- 2. budget: the step is a whole track ----------------------------------------
static void testAWholeChainMovesInOneStepAndTheYieldFallsBetweenTracks() {
// {T1}'s chain costs 4x the offline budget on its own. The budget cannot split it —
// a step is one track — so the tick overruns to finish it and yields only once the
// NEXT track would start. Slot 1 carries no identity, which the drain no longer has
// to reason about at all now that nothing resumes mid-chain.
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "", "{FX-C}", "{FX-D}"}},
{"{T2}", {"{FX-E}"}},
{"{T3}", {"{FX-F}"}}});
FakeHost host;
host.chains["{T1}"] = onlineChain({"{FX-A}", "", "{FX-C}", "{FX-D}"});
host.chains["{T2}"] = onlineChain({"{FX-E}"});
host.chains["{T3}"] = onlineChain({"{FX-F}"});
host.stepCostMs = 50.0; // dearer than kOfflineBudgetMs ⇒ one TRACK per tick
ParkClock clock;
std::vector<ReclaimItem> items = derive(vm, {"{T1}", "{T2}", "{T3}"});
CHECK(items.size() == 3);
matureHold(items, clock, host);
CHECK(drainTick(items, clock, host).writes == 4); // all of {T1}, 200ms over a 30ms budget
CHECK(items[0].done && !items[1].done && !items[2].done);
const std::vector<std::string> firstTick{"{T1}:0=off", "{T1}:1=off", "{T1}:2=off",
"{T1}:3=off"};
CHECK(host.writeLog == firstTick);
// {T2} then {T3}, one track per tick; the tick that finishes the last one exhausts
// the queue in the same pass rather than costing a spare tick.
CHECK(drainToSettled(items, clock, host) == 2);
std::vector<std::string> expected = firstTick;
expected.push_back("{T2}:0=off");
expected.push_back("{T3}:0=off");
CHECK(host.writeLog == expected);
}
static void testOnlineDrainGetsTheLargerBudget() {
// Same per-plugin cost, same track count, one plugin each so neither direction is
// distorted by an overrunning chain: the only difference is the direction, and
// onlining is the one the user is waiting on.
const std::map<std::string, std::vector<std::string>> tracks{
{"{T1}", {"{FX-1}"}}, {"{T2}", {"{FX-2}"}}, {"{T3}", {"{FX-3}"}}, {"{T4}", {"{FX-4}"}}};
ViewModeModel back = parkedDesignLeaves(tracks);
back.setActiveMode(kDesignModeId);
FakeHost onlineHost;
for (const auto& [guid, fx] : tracks) {
back.releaseSnapshotFlags(guid);
onlineHost.chains[guid] = offlineChain(fx);
}
onlineHost.stepCostMs = 50.0;
ParkClock onlineClock;
std::vector<ReclaimItem> onlineItems = derive(back, {"{T1}", "{T2}", "{T3}", "{T4}"});
CHECK(onlineItems.size() == 4 && onlineItems[0].work.kind == FxWorkKind::Online);
CHECK(drainTick(onlineItems, onlineClock, onlineHost).writes == 3); // 120ms buys three
ViewModeModel parked = parkedDesignLeaves(tracks);
FakeHost offlineHost;
for (const auto& [guid, fx] : tracks) offlineHost.chains[guid] = onlineChain(fx);
offlineHost.stepCostMs = 50.0;
ParkClock offlineClock;
std::vector<ReclaimItem> offlineItems = derive(parked, {"{T1}", "{T2}", "{T3}", "{T4}"});
matureHold(offlineItems, offlineClock, offlineHost);
CHECK(drainTick(offlineItems, offlineClock, offlineHost).writes == 1); // 30ms buys one
}
static void testATickStartedOnOnlineWorkDoesNotSpendIntoOfflineWork() {
ViewModeModel vm;
vm.membership().tag("{Z-back}", kArrangeModeId); // active; owed an online
vm.membership().tag("{A-park}", kDesignModeId); // parked; owed an offline
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
FakeHost host;
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
host.stepCostMs = 1.0; // cheap enough that only the kind break can stop the tick
ParkClock clock;
std::vector<ReclaimItem> items = derive(vm, {"{Z-back}", "{A-park}"});
CHECK(items.size() == 2);
CHECK(items[0].work.kind == FxWorkKind::Online); // fx_state's ordering
matureHold(items, clock, host); // the parked track's hold is no longer the blocker
CHECK(drainTick(items, clock, host).writes == 1);
CHECK(host.writeLog == std::vector<std::string>{"{Z-back}:0=on"});
CHECK(!host.offlineAt("{A-park}", 0)); // opportunistic reclaim waits for its own tick
CHECK(drainTick(items, clock, host).writes == 1);
CHECK(host.offlineAt("{A-park}", 0));
}
// -- 3. invalidation: the queue is a cache of intent ----------------------------
static void testModelChangeDiscardsDrainProgressButNotTheParkHold() {
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"});
vm.membership().tag("{U}", kDesignModeId); // a second parked leaf, left alone below
vm.storeSnapshot("{U}", recordWithFx({{"{FX-U}", 0}}));
FakeHost host;
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}"});
host.chains["{U}"] = onlineChain({"{FX-U}"});
host.stepCostMs = 50.0;
ParkClock clock;
std::vector<ReclaimItem> items = derive(vm, {"{T}", "{U}"});
drainTick(items, clock, host); // observes both parked at t=0
host.clock = kParkHoldMs + 100.0;
drainTick(items, clock, host); // {T}'s whole chain moves, then the tick yields
CHECK(items[0].done && !items[1].done);
// The model changes: {T} is retagged into the active mode and restored, while {U}
// stays exactly as parked as it was. The queue is discarded whole — direction,
// completion marks and all — and re-derived.
vm.membership().tag("{T}", kArrangeModeId);
vm.releaseSnapshotFlags("{T}");
items = derive(vm, {"{T}", "{U}"});
CHECK(items.size() == 2);
CHECK(items[0].work.kind == FxWorkKind::Online);
CHECK(items[0].work.trackGuid == "{T}");
CHECK(!items[0].done && !items[1].done); // no drain progress survives a re-derive
// ...but {U}'s hold does survive it. {U} never stopped being parked, and restarting
// its 3s on every unrelated model change would defer its reclaim indefinitely.
clock.observe(items, host.clock);
CHECK(clock.matured("{U}", host.clock));
CHECK(nextEligible(items, clock, host.clock) == 0); // online first, then {U} available
// The mirror: a track that LEAVES the offline set and comes back starts a fresh hold.
// {T} was dropped from the clock by the observation above, while it was online.
vm.membership().tag("{T}", kDesignModeId);
items = derive(vm, {"{T}", "{U}"});
clock.observe(items, host.clock);
CHECK(!clock.matured("{T}", host.clock));
CHECK(!clock.matured("{T}", host.clock + kParkHoldMs - 1.0));
CHECK(clock.matured("{T}", host.clock + kParkHoldMs));
CHECK(clock.matured("{U}", host.clock)); // and {U}'s own hold still is not restarted
}
// -- 4. the user's own offline plugins ------------------------------------------
static void testAPluginTheUserOfflinedIsNeverOnlinedByTheDrain() {
// {FX-B} was offline before the park because the USER put it there; the record is
// the only account of that, and an online must honor it.
ViewModeModel vm;
vm.membership().tag("{T}", kDesignModeId);
vm.setActiveMode(kDesignModeId);
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}}));
vm.releaseSnapshotFlags("{T}");
FakeHost host;
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}", "{FX-C}"}); // fully parked
FxReclaimQueue q = queueOf(vm, {"{T}"});
drainToSettled(q, host);
CHECK(!host.offlineAt("{T}", 0));
CHECK(host.offlineAt("{T}", 1)); // still offline — never blanket-onlined
CHECK(!host.offlineAt("{T}", 2));
for (const std::string& entry : host.writeLog) CHECK(entry != "{T}:1=on");
CHECK(host.writeLog.size() == 2);
CHECK(host.released.count("{T}") == 1);
}
// -- forward progress -----------------------------------------------------------
static void testSlotKeyedOnlineRecordRestoresByPosition() {
// A record lifted from a project saved before FX identity existed: its targets hold
// no identity at all, so position is the only thing that can address them.
ViewModeModel vm;
vm.membership().tag("{T}", kDesignModeId);
vm.setActiveMode(kDesignModeId);
TrackSnapshot snap = recordWithFx({{"", 0}, {"", 1}, {"", 0}});
snap.fxKeying = FxKeying::Slot;
vm.storeSnapshot("{T}", snap);
vm.releaseSnapshotFlags("{T}");
FakeHost host;
host.chains["{T}"] = offlineChain({"{FX-X}", "{FX-Y}", "{FX-Z}"});
// Dearer per plugin than the whole online budget: the step still runs the chain to
// the end, because the budget cannot split one track's move.
host.stepCostMs = kOnlineBudgetMs + 10.0;
FxReclaimQueue q = queueOf(vm, {"{T}"});
CHECK(q.items().size() == 1);
CHECK(drainToSettled(q, host) == 1);
const std::vector<std::string> expected{"{T}:0=on", "{T}:2=on"};
CHECK(host.writeLog == expected); // slot 1 stays offline, as the record captured
CHECK(host.offlineAt("{T}", 1));
}
static void testATrackThatLeftTheProjectCompletesRatherThanStalls() {
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
FakeHost host; // no chain registered ⇒ liveChain reports the track gone
FxReclaimQueue q = queueOf(vm, {"{T}"});
CHECK(!q.run(host).settled); // observes the hold at t=0; not matured yet
host.clock += kParkHoldMs;
CHECK(q.run(host).settled);
CHECK(host.writeLog.empty());
CHECK(host.released.empty()); // the next apply's reconcile drops the whole record
}
// -- 5. re-entrancy: an invalidation arriving with a drain on the stack ------------
static void testAReentrantInvalidationReachesNothingTheDrainIsUsing() {
// The second of {T1}'s four writes re-enters and does what a nested applyMode used to
// do to the shared queue: clear it. The drain is mid-item at that moment.
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}},
{"{T2}", {"{FX-E}"}}});
FakeHost host;
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"});
host.chains["{T2}"] = onlineChain({"{FX-E}"});
host.stepCostMs = 50.0;
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"});
q.run(host); // observes both parked at t=0
host.clock += kParkHoldMs;
host.reentrant = &q;
host.invalidateAfterWrite = 2;
const ReclaimRunResult r = q.run(host);
// The ownership claim: for the whole length of the drain the queue object holds
// nothing, so there is no element for a re-entrant caller to destroy.
CHECK(host.itemsSeenMidDrain == 0);
// The drain finished the track it was on out of its own copy — a step is atomic.
const std::vector<std::string> whole{"{T1}:0=off", "{T1}:1=off", "{T1}:2=off",
"{T1}:3=off"};
CHECK(host.writeLog == whole);
CHECK(r.writes == 4);
// ...and then refused to commit work derived from a model that has since moved.
CHECK(r.aborted);
CHECK(!r.settled);
CHECK(q.stale());
CHECK(q.items().empty());
}
static void testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled() {
// One item, so the drain would otherwise report settled and the shell would go idle —
// clearing the pending flag the re-entrant invalidation had just raised and stranding
// its work until the next mode switch.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
FakeHost host;
host.chains["{T}"] = onlineChain({"{FX-A}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
q.run(host);
host.clock += kParkHoldMs;
host.reentrant = &q;
host.invalidateAfterWrite = 1;
const ReclaimRunResult r = q.run(host);
CHECK(r.writes == 1);
CHECK(!r.settled); // the one bit that decides whether the shell clears its wakeup
CHECK(r.aborted);
CHECK(q.stale()); // and the next tick re-derives rather than finding an empty queue
// The contrast, so the assertion above is about the invalidation and not about this
// queue being unable to settle: the same drain settles once nothing re-enters it.
host.reentrant = nullptr;
q.adopt(derive(vm, {"{T}"}));
CHECK(q.run(host).settled);
}
static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
// Releasing spends the record's FX half — the only account of which plugins the USER
// had offline before the park. An invalidation mid-drain is the signal that the model
// underneath may have been replaced (a project load replaces it in place), so the
// release must not land on it.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
vm.setActiveMode(kDesignModeId);
vm.releaseSnapshotFlags("{T}");
FakeHost host;
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
host.reentrant = &q;
host.invalidateAfterWrite = 1;
const ReclaimRunResult r = q.run(host);
CHECK(r.aborted);
CHECK(r.writes == 2); // the item still completed — a step is atomic
CHECK(host.released.empty()); // but its record was NOT spent
CHECK(host.persists == 0); // and nothing durable claims it was
// Re-owed, not lost: the re-derived item finds the chain already where the record
// wants it, writes nothing more, and releases then.
host.reentrant = nullptr;
q.adopt(derive(vm, {"{T}"}));
CHECK(q.run(host).settled);
CHECK(host.writeLog.size() == 2);
CHECK(host.released.count("{T}") == 1);
CHECK(host.persists == 1);
}
// -- 6. the release has to outlive the session ------------------------------------
static void testACompletedOnlineIsPersistedSoAReloadPlansNoWork() {
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
vm.setActiveMode(kDesignModeId);
vm.releaseSnapshotFlags("{T}");
// The bug this closes, stated as the baseline: the model as it stands BEFORE the
// release re-derives the same Online work when reloaded, and re-driving it would
// revert whatever per-FX offline the user set after the reclaim had finished.
const std::optional<ViewModeModel> stale = ViewModeModel::deserialize(vm.serialize());
CHECK(stale.has_value());
CHECK(derive(*stale, {"{T}"}).size() == 1);
FakeHost host;
host.model = &vm;
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
drainToSettled(q, host);
CHECK(host.released.count("{T}") == 1);
CHECK(host.persists == 1); // once per tick that released, not once per release
const std::optional<ViewModeModel> reloaded = ViewModeModel::deserialize(host.persisted);
CHECK(reloaded.has_value());
CHECK(reloaded->snapshot("{T}") == nullptr); // both halves spent ⇒ record erased
CHECK(derive(*reloaded, {"{T}"}).empty());
}
static void testATickThatSpendsNoRecordPersistsNothing() {
// OFFLINE work spends no record, so there is nothing to make durable — and a
// background reclaim must not dirty the project for work it can simply redo.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
FakeHost host;
host.model = &vm;
host.chains["{T}"] = onlineChain({"{FX-A}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
q.run(host); // observes the hold at t=0
host.clock += kParkHoldMs;
drainToSettled(q, host);
CHECK(host.writeLog.size() == 1); // it did move the plugin
CHECK(host.released.empty());
CHECK(host.persists == 0);
CHECK(host.persisted.empty());
}
// -- 7. captured FX state the online direction cannot place -------------------------
static void testADropIsReportedOnceAcrossRepeatedRederivations() {
// {FX-B} was deleted while the track was parked, so its captured state has nowhere
// to land. Swallowing that leaves the user no trace of it at all.
ViewModeModel vm;
vm.membership().tag("{T}", kDesignModeId);
vm.setActiveMode(kDesignModeId);
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}, {"{FX-B}", 1}}));
vm.releaseSnapshotFlags("{T}");
FakeHost host;
host.model = &vm;
host.chains["{T}"] = offlineChain({"{FX-A}"}); // {FX-B} gone from the chain
FxReclaimQueue q = queueOf(vm, {"{T}"});
host.reentrant = &q;
host.invalidateAfterWrite = 1; // aborts, so the identical item re-derives next tick
const ReclaimRunResult first = q.run(host);
CHECK(first.aborted);
CHECK(first.drops.size() == 1);
CHECK(first.drops[0].trackGuid == "{T}");
CHECK(first.drops[0].drops.missingIdentity == 1);
CHECK(first.drops[0].drops.total() == 1);
CHECK(host.released.empty()); // the abort still refuses to spend the record
host.reentrant = nullptr;
q.adopt(derive(vm, {"{T}"}));
const ReclaimRunResult second = q.run(host);
CHECK(second.settled);
CHECK(second.drops == first.drops); // the re-derivation resolves the very same set
// The gate the shell's console report sits behind: first sighting prints, that
// identical re-derivation does not, a different set does.
const std::vector<TrackFxDrops> none;
CHECK(shouldReportDrops(/*sameOwnerAsLast=*/false, none, first.drops));
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, first.drops, second.drops));
const std::vector<TrackFxDrops> elsewhere{TrackFxDrops{"{U}", FxRestoreDrops{1, 0, 0}}};
CHECK(shouldReportDrops(/*sameOwnerAsLast=*/true, first.drops, elsewhere));
// The record IS spent with a drop outstanding: holding it for an FX the chain no
// longer has would replan work that can never resolve.
CHECK(host.released.count("{T}") == 1);
CHECK(!host.offlineAt("{T}", 0)); // and the placeable half did land
}
static void testAnOfflineTickReportsNoDrops() {
// Offline targets are the live chain's own slots, so nothing can fail to resolve.
// Keeps the drop assertions above honest: they must catch a real degrade, not an
// accumulator that fills on every tick.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
FakeHost host;
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}"});
FxReclaimQueue q = queueOf(vm, {"{T}"});
q.run(host);
host.clock += kParkHoldMs;
const ReclaimRunResult r = q.run(host);
CHECK(r.settled);
CHECK(r.writes == 2);
CHECK(r.drops.empty());
}
int main() {
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
testAWholeChainMovesInOneStepAndTheYieldFallsBetweenTracks();
testOnlineDrainGetsTheLargerBudget();
testATickStartedOnOnlineWorkDoesNotSpendIntoOfflineWork();
testModelChangeDiscardsDrainProgressButNotTheParkHold();
testAPluginTheUserOfflinedIsNeverOnlinedByTheDrain();
testSlotKeyedOnlineRecordRestoresByPosition();
testATrackThatLeftTheProjectCompletesRatherThanStalls();
testAReentrantInvalidationReachesNothingTheDrainIsUsing();
testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled();
testAnInvalidationDuringAnOnlineDropsThatTicksRelease();
testACompletedOnlineIsPersistedSoAReloadPlansNoWork();
testATickThatSpendsNoRecordPersistsNothing();
testADropIsReportedOnceAcrossRepeatedRederivations();
testAnOfflineTickReportsNoDrops();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}