// Standalone tests for the FX reclaimer's drain — no REAPER, no test framework. The // loop is templated on its host precisely so the four properties that make it safe to // run in the background are assertable here: the debounce, the per-tick budget's // yield-and-resume, the queue being a cache of intent that a model change discards, and // the user's own offline plugins surviving an online. #include "../src/shell/view/fx_reclaim.h" #include #include #include #include #include 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> chains; std::set released; std::vector writeLog; double clock = 0.0; double stepCostMs = 50.0; double nowMs() const { return clock; } bool liveChain(const std::string& guid, std::vector& 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(it->second.size())) return false; if (it->second[static_cast(fxIndex)].offline == offline) return false; it->second[static_cast(fxIndex)].offline = offline; clock += stepCostMs; writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on")); return true; } void releaseFx(const std::string& guid) { released.insert(guid); } 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 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 derive(const ViewModeModel& vm, const std::set& live) { return makeQueue(desiredFxState(vm, vm.activeModeId(), live)); } std::vector onlineChain(const std::vector& guids) { std::vector chain; for (const std::string& g : guids) chain.push_back(FakeFx{g, false}); return chain; } std::vector offlineChain(const std::vector& guids) { std::vector chain; for (const std::string& g : guids) chain.push_back(FakeFx{g, true}); return chain; } // A Design leaf the tool has parked: record held, both halves, nothing user-offlined. ViewModeModel parkedDesignLeaf(const std::vector& fxGuids) { ViewModeModel vm; vm.membership().tag("{T}", kDesignModeId); // active mode is Arrange ⇒ {T} is parked std::vector fx; for (const std::string& g : fxGuids) fx.push_back(FxOfflineState{g, 0}); vm.storeSnapshot("{T}", recordWithFx(fx)); return vm; } // 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& 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. int drainToSettled(std::vector& 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); // did not settle — the drain is not making forward progress return -1; } } // 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}"}); ParkClock clock; std::vector items = derive(vm, {"{T}"}); CHECK(items.size() == 1 && items[0].work.kind == FxWorkKind::Offline); CHECK(drainTick(items, clock, host).writes == 0); CHECK(!drainTick(items, clock, 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}"); items = derive(vm, {"{T}"}); CHECK(items.size() == 1 && items[0].work.kind == FxWorkKind::Online); drainToSettled(items, clock, 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}"}); ParkClock clock2; std::vector items2 = derive(held, {"{T}"}); CHECK(drainTick(items2, clock2, host2).writes == 0); // observed parked at t=0 host2.clock = kParkHoldMs; drainToSettled(items2, clock2, 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: yield and resume ------------------------------------------------ static void testOfflineDrainYieldsOnBudgetAndResumesWhereItStopped() { ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}); host.stepCostMs = 50.0; // dearer than kOfflineBudgetMs ⇒ exactly one plugin per tick ParkClock clock; std::vector items = derive(vm, {"{T}"}); matureHold(items, clock, host); CHECK(drainTick(items, clock, host).writes == 1); CHECK(items[0].work.resumeAfterSlot == 0); CHECK(items[0].work.resumeAfterFxGuid == "{FX-A}"); CHECK(!items[0].done); CHECK(drainTick(items, clock, host).writes == 1); CHECK(items[0].work.resumeAfterSlot == 1); // Slots 2 and 3, one per tick; the tick that writes the last one also exhausts the // item's write list, so it settles in the same pass rather than costing a spare tick. CHECK(drainToSettled(items, clock, host) == 2); // Each slot moved exactly once and in order — no slot re-walked across a yield. const std::vector expected{"{T}:0=off", "{T}:1=off", "{T}:2=off", "{T}:3=off"}; CHECK(host.writeLog == expected); } static void testOnlineDrainGetsTheLargerBudget() { // Same per-plugin cost, same chain length: the only difference is the direction, and // onlining is the one the user is waiting on. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); FakeHost host; host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}", "{FX-C}", "{FX-D}"}); host.stepCostMs = 50.0; ParkClock clock; std::vector items = derive(vm, {"{T}"}); CHECK(items.size() == 1 && items[0].work.kind == FxWorkKind::Online); CHECK(drainTick(items, clock, host).writes == 3); // 120ms budget vs 30ms } 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 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{"{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 items = derive(vm, {"{T}", "{U}"}); drainTick(items, clock, host); // observes both parked at t=0 host.clock = kParkHoldMs + 100.0; drainTick(items, clock, host); // one plugin moves; {T} now carries a cursor CHECK(items[0].work.resumeAfterSlot >= 0); // 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, // cursors, 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].work.resumeAfterSlot == -1); // no drain progress survives a re-derive CHECK(items[0].work.resumeAfterFxGuid.empty()); CHECK(!items[0].done); // ...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 ParkClock clock; std::vector items = derive(vm, {"{T}"}); drainToSettled(items, clock, 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: the cursor cases that decide termination ----------------- static void testResumeCursorAdvancesPastAnFxWithNoIdentity() { // The case Wave 1's slot cursor exists for. Slot 1 is an FX REAPER reports no GUID // for; with the identity alone the cursor would read as "nothing done yet" and the // item would re-walk from slot 0 on every tick. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}", "{FX-C}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}", "", "{FX-C}"}); host.stepCostMs = 50.0; // one plugin per tick on the offline budget ParkClock clock; std::vector items = derive(vm, {"{T}"}); matureHold(items, clock, host); drainTick(items, clock, host); drainTick(items, clock, host); CHECK(items[0].work.resumeAfterFxGuid.empty()); // slot 1 has no identity to record CHECK(items[0].work.resumeAfterSlot == 1); // ...so the slot is what carries it CHECK(drainToSettled(items, clock, host) == 1); // slot 2, and the item is exhausted const std::vector expected{"{T}:0=off", "{T}:1=off", "{T}:2=off"}; CHECK(host.writeLog == expected); } static void testSlotKeyedOnlineRecordCarriesItsCursorAsASlot() { // A record lifted from a project saved before FX identity existed: its targets hold // no identity, so a cursor taken from the LIVE identity would match none of them and // the item would re-walk its whole chain after every yield. 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}"}); host.stepCostMs = kOnlineBudgetMs + 10.0; // one plugin per tick, then yield ParkClock clock; std::vector items = derive(vm, {"{T}"}); CHECK(items.size() == 1 && resumeBySlotOnly(items[0].work)); drainTick(items, clock, host); CHECK(items[0].work.resumeAfterFxGuid.empty()); // NOT "{FX-X}", which matches no target CHECK(items[0].work.resumeAfterSlot == 0); drainToSettled(items, clock, host); const std::vector 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 ParkClock clock; std::vector items = derive(vm, {"{T}"}); matureHold(items, clock, host); CHECK(drainTick(items, clock, host).settled); CHECK(host.writeLog.empty()); CHECK(host.released.empty()); // the next apply's reconcile drops the whole record } int main() { testFastFlipMovesZeroPluginsButAHeldParkStillReclaims(); testOfflineDrainYieldsOnBudgetAndResumesWhereItStopped(); testOnlineDrainGetsTheLargerBudget(); testATickStartedOnOnlineWorkDoesNotSpendIntoOfflineWork(); testModelChangeDiscardsDrainProgressButNotTheParkHold(); testAPluginTheUserOfflinedIsNeverOnlinedByTheDrain(); testResumeCursorAdvancesPastAnFxWithNoIdentity(); testSlotKeyedOnlineRecordCarriesItsCursorAsASlot(); testATrackThatLeftTheProjectCompletesRatherThanStalls(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }