// 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 transport gate, the release reaching disk on // the user's save rather than on the drain, and — via a host that invalidates from inside // a write — that a re-entered invalidation reaches nothing the drain is using. The queue // owns the project pin and the timer's wakeup for the same reason: a compare-only pointer // makes the tab switch assertable. The undo span is here on the same terms: its state // machine is pure, so open-once/close-once and the wait it may or may not outlast are // assertable; only the Undo_BeginBlock2/EndBlock2 calls themselves need a DAW. #include "../src/shell/view/fx_reclaim.h" #include #include #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 ------------------------------------------------------------ // Two project handles. The queue and the flush treat a project pointer as COMPARE-ONLY // and never dereference it, so a fabricated address is a faithful stand-in for REAPER's. ReaProject* const kProjA = reinterpret_cast(static_cast(0x1)); ReaProject* const kProjB = reinterpret_cast(static_cast(0x2)); // 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; // 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 // A Ctrl-Z arriving through the same pump: what fxReclaimAbandonForStateLoad does, // fired on the Nth successful write. The direction probe rides the same hook because // the property it checks is the same one — what a caller re-entering mid-drain sees. int abandonAfterWrite = -1; int probeActivityAfterWrite = -1; int spanClosesMidDrain = 0; FxReclaimActivity activityMidDrain = FxReclaimActivity::Idle; bool rolling = false; // the transport gate's input // Stands in for the shell's one span instance: fx_reclaim.cpp opens it on the first // real write of a drain, exactly here, and widens it to carry the FX bit. Only the // Undo_BeginBlock2/EndBlock2 calls themselves are out of reach without a DAW. UndoSpan* span = nullptr; int spanOpens = 0; double nowMs() const { return clock; } bool transportRolling() const { return rolling; } // Both host entry points refuse once abandoned, exactly as fx_reclaim.cpp's do: this // one ends every REMAINING track, writeOffline below ends the one being written. bool liveChain(const std::string& guid, std::vector& out) { if (reentrant && reentrant->abandoned()) return false; 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) { if (reentrant && reentrant->abandoned()) return false; 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; if (span) { if (span->begin(project, kReclaimUndoLabel, 2)) ++spanOpens; span->widen(2); } it->second[static_cast(fxIndex)].offline = offline; clock += stepCostMs; writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on")); if (reentrant && static_cast(writeLog.size()) == invalidateAfterWrite) { itemsSeenMidDrain = static_cast(reentrant->items().size()); reentrant->discard(); reentrant->invalidate(); } if (reentrant && static_cast(writeLog.size()) == probeActivityAfterWrite) activityMidDrain = reentrant->activity(clock, rolling); if (reentrant && static_cast(writeLog.size()) == abandonAfterWrite) { if (span && span->end()) ++spanClosesMidDrain; // closeSpan, then the stop reentrant->abandon(); } 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. The // drain writes the model and NEVER `persisted`: only saveProject() below does, which // is what makes "the spend rides the user's save" assertable. ViewModeModel* model = nullptr; ReleaseFlush* flush = nullptr; ReaProject* project = kProjA; std::string persisted; void releaseFx(const std::string& guid) { released.insert(guid); if (model) model->releaseSnapshotFx(guid); if (flush) flush->arm(project); } 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; } }; // What the project-save hook does (fxReclaimFlushRelease): write the in-memory model into // this project's view state, but only when it is the project holding the release. Does // NOT clear the arm — see ReleaseFlush — so calling this twice re-issues the same write. void saveProject(FakeHost& host, ReleaseFlush& flush, ReaProject* active) { if (!flush.pendingFor(active)) return; host.persisted = host.model->serialize(); } // -- 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; } // 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>& tracks) { ViewModeModel vm; for (const auto& [guid, fxGuids] : tracks) { vm.membership().tag(guid, kDesignModeId); std::vector 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& 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& 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& 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& live) { FxReclaimQueue q; q.adopt(derive(vm, live)); return q; } // One whole timer tick as the shell makes it: main.cpp's ONE pending() test, then // fxReclaimTick's pin, its re-derivation from the session's model whenever the queue is // stale, and the drain. The re-derivation is what makes the wakeup the load-bearing bit — // it reads whatever model is live right now, whether or not anything re-read it since. void timerTick(FxReclaimQueue& q, const ViewModeModel& vm, const std::set& live, FakeHost& host) { if (!q.pending()) return; if (!q.beginTick(host.project)) return; if (q.stale()) q.adopt(derive(vm, live)); q.run(host); } } // 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 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 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 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> 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 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 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 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); // {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 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 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; ReleaseFlush flush; host.flush = &flush; 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(!flush.pendingFor(kProjA)); // and no save is owed one that never happened // 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(flush.pendingFor(kProjA)); } // -- 6. the release has to outlive the session ------------------------------------ static void testACompletedOnlineReachesDiskOnTheSaveAndNotOnTheDrain() { ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); // The bug the flush 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. FakeHost host; host.model = &vm; host.persisted = vm.serialize(); // the project as it sits on disk, record unspent host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"}); ReleaseFlush flush; host.flush = &flush; FxReclaimQueue q = queueOf(vm, {"{T}"}); drainToSettled(q, host); CHECK(host.released.count("{T}") == 1); // The drain wrote NOTHING durable. That is the whole change: a background tick that // persisted would also have dirtied the project, asking the user to save one they // had only opened. const std::optional beforeSave = ViewModeModel::deserialize(host.persisted); CHECK(beforeSave.has_value()); CHECK(beforeSave->snapshot("{T}") != nullptr); CHECK(derive(*beforeSave, {"{T}"}).size() == 1); // still stale on disk CHECK(flush.pendingFor(kProjA)); // ...and the save knows it is owed // The user's next save is what spends it — and afterwards a reload plans no work, // so nothing re-drives the chain. The arm itself survives the write (see // testTheFlushArmSurvivesASaveAndCanBeReIssued) — this test is about the disk content. saveProject(host, flush, kProjA); CHECK(flush.pendingFor(kProjA)); const std::optional afterSave = ViewModeModel::deserialize(host.persisted); CHECK(afterSave.has_value()); CHECK(afterSave->snapshot("{T}") == nullptr); // both halves spent ⇒ record erased CHECK(derive(*afterSave, {"{T}"}).empty()); } static void testAReleaseKilledBeforeTheFlushIsSimplyReEarned() { // The exposure the save-time flush opens: between the in-memory release and the next // save, the record on disk is still unspent. Quitting there must cost a redo of the // reclaim, never a repair. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); FakeHost host; host.model = &vm; host.persisted = vm.serialize(); host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"}); ReleaseFlush flush; host.flush = &flush; FxReclaimQueue q = queueOf(vm, {"{T}"}); drainToSettled(q, host); CHECK(host.writeLog.size() == 2); // the chain really was driven online CHECK(flush.pendingFor(kProjA)); // Killed here: no save, so the model in memory goes and the disk record survives. const std::optional reopened = ViewModeModel::deserialize(host.persisted); CHECK(reopened.has_value()); ViewModeModel reloaded = *reopened; FakeHost after; after.chains = host.chains; // the DAW state outlives the reload: the chain is online after.model = &reloaded; ReleaseFlush flushAfter; after.flush = &flushAfter; FxReclaimQueue q2 = queueOf(reloaded, {"{T}"}); CHECK(q2.items().size() == 1); // the surviving record re-derives the same Online work CHECK(drainToSettled(q2, after) == 1); CHECK(after.writeLog.empty()); // compare-before-write: nothing left to move CHECK(after.released.count("{T}") == 1); CHECK(flushAfter.pendingFor(kProjA)); // and the spend is owed to the next save again } static void testATickThatSpendsNoRecordOwesNoSaveAnything() { // OFFLINE work spends no record, so there is nothing to make durable — and the // reclaimer must never give the user's next save work it did not earn. ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.model = &vm; host.chains["{T}"] = onlineChain({"{FX-A}"}); ReleaseFlush flush; host.flush = &flush; 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(!flush.pendingFor(kProjA)); saveProject(host, flush, kProjA); CHECK(host.persisted.empty()); } static void testTheFlushArmSurvivesASaveAndCanBeReIssued() { // Whether a write made from inside the save hook reaches the very file being written // now, or lands one save late, is unsettled (ReleaseFlush's [verify — DAW]). The arm // must not treat one issued write as proof of a durable spend: it stays armed, so an // unfavourable answer still gets a retry on the project's NEXT save rather than // stranding the record in memory for the rest of the session. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); FakeHost host; host.model = &vm; ReleaseFlush flush; flush.arm(kProjA); saveProject(host, flush, kProjA); CHECK(!host.persisted.empty()); CHECK(flush.pendingFor(kProjA)); // survives the flush — not disarmed by one write // A second save re-issues the identical write rather than finding nothing to do. host.persisted.clear(); saveProject(host, flush, kProjA); CHECK(!host.persisted.empty()); CHECK(flush.pendingFor(kProjA)); // A different project's save must still leave this arm untouched. saveProject(host, flush, kProjB); CHECK(flush.pendingFor(kProjA)); } // -- 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 none; CHECK(shouldReportDrops(/*sameOwnerAsLast=*/false, none, first.drops)); CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, first.drops, second.drops)); const std::vector 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()); } // -- 8. the pin and the wakeup ------------------------------------------------------ static void testATabSwitchRetargetsTheQueueAndLeavesTheWakeupStanding() { // Project A, one parked leaf owed offline work, derived against A and pinned to it. ViewModeModel a = parkedDesignLeaf({"{FX-A}"}); FakeHost hostA; hostA.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q; q.invalidate(); // what applyMode does on its way out of the switch that parked {T} CHECK(q.pending()); CHECK(q.beginTick(kProjA)); CHECK(q.project() == kProjA); q.adopt(derive(a, {"{T}"})); CHECK(!q.run(hostA).settled); // observes the hold at t=0; still owed CHECK(!q.stale()); // The tab switch. poll()'s Load reapply invalidates on the way in, and NOTHING there // moves the pin, so the tick arrives holding A's work with B active. q.invalidate(); CHECK(q.beginTick(kProjB)); // the tick proceeds — it does not bail on the mismatch CHECK(q.pending()); // and the timer will still call it: the strand's one bit CHECK(q.project() == kProjB); CHECK(q.stale()); // A's work is gone, B's is owed CHECK(q.items().empty()); // B's own parked leaf converges on this very tick and every one after it. ViewModeModel b = parkedDesignLeaves({{"{T2}", {"{FX-B}"}}}); FakeHost hostB; hostB.chains["{T2}"] = onlineChain({"{FX-B}"}); q.adopt(derive(b, {"{T2}"})); CHECK(!q.run(hostB).settled); // B's own hold starts here hostB.clock += kParkHoldMs; const ReclaimRunResult done = q.run(hostB); CHECK(done.settled); CHECK(hostB.writeLog == std::vector{"{T2}:0=off"}); CHECK(hostB.offlineAt("{T2}", 0)); // ...and A's chain was never touched from B: the queue was dropped, not run. CHECK(hostA.writeLog.empty()); // Only now, settled, does the shell stop the ticks. q.goIdle(); CHECK(!q.pending()); CHECK(q.project() == nullptr); } static void testGoingIdleKeepsTheParkHoldWhileARetargetDropsIt() { // A hold is evidence about a track in ONE project, so a tab switch drops it. An // ordinary settle does not: restarting every 3s hold on the next unrelated membership // edit would defer an unchanged track's reclaim indefinitely. ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q; CHECK(q.beginTick(kProjA)); q.adopt(derive(vm, {"{T}"})); q.run(host); // hold starts at t=0 host.clock = kParkHoldMs; CHECK(q.run(host).settled); CHECK(host.writeLog.size() == 1); // Settled: the queue and the pin go, the hold stays. A membership edit elsewhere // re-derives the SAME offline item, which is matured on arrival — one tick, no // second 3s wait, and nothing to write because the chain is already there. q.goIdle(); q.invalidate(); CHECK(q.beginTick(kProjA)); q.adopt(derive(vm, {"{T}"})); const ReclaimRunResult resumed = q.run(host); CHECK(resumed.settled); CHECK(resumed.writes == 0); CHECK(host.writeLog.size() == 1); // The mirror: away to B and back to A, and the same item must wait a fresh hold. q.invalidate(); CHECK(q.beginTick(kProjB)); CHECK(q.beginTick(kProjA)); q.adopt(derive(vm, {"{T}"})); const double back = host.clock; CHECK(!q.run(host).settled); // the hold restarts here, at `back` host.clock = back + kParkHoldMs - 1.0; CHECK(!q.run(host).settled); host.clock = back + kParkHoldMs; CHECK(q.run(host).settled); } static void testOnlyANoProjectTickClearsTheWakeup() { ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q; q.invalidate(); CHECK(q.beginTick(kProjA)); q.adopt(derive(vm, {"{T}"})); q.run(host); host.clock += kParkHoldMs; // An abort leaves the wakeup up: the work is re-owed, and the shell's settle test // never runs on this path. host.reentrant = &q; host.invalidateAfterWrite = 1; const ReclaimRunResult r = q.run(host); CHECK(r.aborted); CHECK(q.pending()); // No active project at all — the one arm with nothing to converge and nothing to // come back to. Everything goes, wakeup included. CHECK(!q.beginTick(nullptr)); CHECK(!q.pending()); CHECK(q.project() == nullptr); CHECK(q.stale()); CHECK(q.items().empty()); } static void testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo() { const std::vector none; const std::vector lost{TrackFxDrops{"{T}", FxRestoreDrops{1, 0, 0}}}; CHECK(shouldReportDrops(/*sameOwnerAsLast=*/false, none, lost)); // first sighting CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost)); // the re-derivation // The regression: every ordinary tick carries no drops at all. Treating that as a // change would overwrite the memo and re-arm the identical report for the next // re-derivation — the shell holds the memo back on exactly this answer. CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, none)); CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/false, lost, none)); CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost)); } // -- 9. the transport gate ------------------------------------------------------------ static void testARollingTransportDefersOfflineWorkAndKeepsTheHold() { ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}"}); FxReclaimQueue q = queueOf(vm, {"{T}"}); CHECK(!q.run(host).settled); // the hold starts at t=0 host.rolling = true; host.clock += kParkHoldMs; // matured — the debounce is no longer what holds it back const ReclaimRunResult gated = q.run(host); CHECK(gated.writes == 0); CHECK(host.writeLog.empty()); CHECK(!gated.aborted); CHECK(!gated.settled); // the work is still owed — a gated item is deferred, not dropped CHECK(!q.stale()); // and the queue it will resume from survives untouched // The hold kept its origin across the gated tick: the drain fires on the FIRST tick // after the transport stops, with no fresh 3s wait. host.rolling = false; CHECK(q.run(host).settled); CHECK(host.writeLog.size() == 2); } static void testOnlineWorkProceedsWhileTheTransportRolls() { // Daniel's ruling: the gate narrows to the offline direction. Onlining is the mode // switch's own unfinished promise — restoreTrack already handed back visibility and // B_MAINSEND/I_FXEN synchronously — so a full gate would leave the user listening to a // dry, FX-enabled chain for the whole rest of the playback pass. Onlining still costs // a plugin load mid-roll; that is the accepted trade, not a regression this test flags. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); FakeHost host; host.model = &vm; host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"}); ReleaseFlush flush; host.flush = &flush; host.rolling = true; FxReclaimQueue q = queueOf(vm, {"{T}"}); CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online); const ReclaimRunResult run = q.run(host); CHECK(run.writes == 2); CHECK(run.settled); CHECK(host.writeLog.size() == 2); CHECK(host.released.count("{T}") == 1); // the record's FX half is spent CHECK(flush.pendingFor(kProjA)); CHECK(!host.offlineAt("{T}", 0) && !host.offlineAt("{T}", 1)); } static void testOfflineWorkStaysGatedWhileOnlineDrainsInTheSameTick() { // A mixed queue while rolling: {Z-back} is owed an online, {A-park}'s hold is already // matured and owed an offline. Only the online direction may move this tick. ViewModeModel vm; vm.membership().tag("{Z-back}", kArrangeModeId); vm.membership().tag("{A-park}", kDesignModeId); 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}"}); ParkClock clock; std::vector items = derive(vm, {"{Z-back}", "{A-park}"}); CHECK(items[0].work.kind == FxWorkKind::Online); matureHold(items, clock, host); // {A-park}'s hold matures with time to spare below host.rolling = true; host.clock += kParkHoldMs; const ReclaimTickResult tick = drainTick(items, clock, host); CHECK(tick.writes == 1); CHECK(host.writeLog == std::vector{"{Z-back}:0=on"}); CHECK(!host.offlineAt("{A-park}", 0)); // matured but gated — untouched this tick CHECK(!tick.settled); // the offline item is still owed // The transport stops: the very next tick reclaims it, with no fresh hold to wait out. host.rolling = false; CHECK(drainTick(items, clock, host).settled); CHECK(host.offlineAt("{A-park}", 0)); } static void testAHoldDropsWhenItsTrackStopsOwingOfflineWorkWhileRolling() { // ParkClock's own contract (fx_reclaim.h) says an entry is dropped the moment its // track stops owing offline work — observe() has to run every tick, rolling or not, // for that to hold once a rolling tick can itself change what a track owes (by // draining its online item). This is the case that contract would otherwise miss. ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; // Offline in the DAW, matching a track truly parked — so bringing it online below is a // real write, not a compare-before-write no-op. host.chains["{T}"] = offlineChain({"{FX-A}"}); ParkClock clock; std::vector items = derive(vm, {"{T}"}); CHECK(drainTick(items, clock, host).writes == 0); // the hold starts at t=0 CHECK(!clock.matured("{T}", 0.0)); // {T} comes back online while the transport rolls; the online direction still drains. host.rolling = true; vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); items = derive(vm, {"{T}"}); CHECK(items[0].work.kind == FxWorkKind::Online); host.clock = 100.0; CHECK(drainTick(items, clock, host).writes == 1); CHECK(!clock.matured("{T}", host.clock)); // dropped: {T} owes no offline work now // {T} parks again, still rolling: the hold must start FRESH from here, not resume the // one interrupted above — proof the drop above was real, not a no-op. vm.setActiveMode(kArrangeModeId); vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}})); items = derive(vm, {"{T}"}); CHECK(items[0].work.kind == FxWorkKind::Offline); const double repark = host.clock; CHECK(!drainTick(items, clock, host).settled); // gated, and not matured either CHECK(!clock.matured("{T}", repark + kParkHoldMs - 1.0)); CHECK(clock.matured("{T}", repark + kParkHoldMs)); host.rolling = false; host.clock = repark + kParkHoldMs; CHECK(drainTick(items, clock, host).writes == 1); // matures once the gate lifts } static void testAGatedTickStillGoesIdleWithNothingLeftToConverge() { // A settled queue must not spin the timer for the length of a playback pass: the // gate's answer is "no writes", not "never finished". ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q = queueOf(vm, {"{T}"}); q.run(host); host.clock += kParkHoldMs; CHECK(q.run(host).settled); host.rolling = true; CHECK(q.run(host).settled); } // -- 10. the undo span ---------------------------------------------------------------- static void testTheSpanOpensOnceAndClosesOnceHoweverManyTimesItIsAsked() { // The whole safety of the design is in these two answers: a caller may say begin or // end unconditionally and still make no second Undo_BeginBlock2 and no unbalanced // Undo_EndBlock2. A leaked open block swallows everything the user does afterwards. UndoSpan span; CHECK(!span.open()); CHECK(!span.end()); // nothing open — the caller owes REAPER nothing CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9)); CHECK(span.open()); CHECK(span.project() == kProjA); CHECK(span.label() == "ReaSampler: activate Design view"); CHECK(span.mask() == 9); // The drain's own begin, arriving under a switch's span: it must adopt nothing — // neither the other project, nor a label that would rename the user's switch. CHECK(!span.begin(kProjB, kReclaimUndoLabel, 2)); CHECK(span.project() == kProjA); CHECK(span.label() == "ReaSampler: activate Design view"); CHECK(span.mask() == 9); span.widen(2); // ...but the FX bit it DID write under still has to reach the point CHECK(span.mask() == 11); span.relabel("ReaSampler: activate Arrange view", 1); CHECK(span.label() == "ReaSampler: activate Arrange view"); CHECK(span.mask() == 1); CHECK(span.end()); CHECK(!span.open()); CHECK(!span.end()); // the second close reaches nothing CHECK(span.project() == nullptr); // And nothing a closed span carries can be revived out from under the next opener. span.widen(4); span.relabel("stale", 8); CHECK(span.mask() == 0); CHECK(span.label().empty()); } static void testTheSpanOutlastsADebounceWaitButNeverATransportDeferral() { // The one policy call: the span may stay open across a wait it can BOUND. The 3s // debounce is the dead time between the switch and its first FX write, and spanning // it is the whole point. A transport deferral lasts as long as the user plays. ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q = queueOf(vm, {"{T}"}); const ReclaimRunResult holding = q.run(host); // the hold starts at t=0 CHECK(!holding.settled); CHECK(!holding.transportDeferred); CHECK(!spanEndsAfter(holding)); // bounded by kParkHoldMs — the span holds host.clock += kParkHoldMs; host.rolling = true; const ReclaimRunResult gated = q.run(host); CHECK(gated.writes == 0); CHECK(!gated.settled); CHECK(gated.transportDeferred); CHECK(spanEndsAfter(gated)); // unbounded — the switch's point closes without it host.rolling = false; const ReclaimRunResult done = q.run(host); CHECK(done.writes == 1); CHECK(done.settled); CHECK(!done.transportDeferred); CHECK(spanEndsAfter(done)); } static void testAnAbortEndsTheSpanSoNoLaterSwitchNestsUnderIt() { 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 aborted = q.run(host); CHECK(aborted.aborted); CHECK(!aborted.settled); CHECK(spanEndsAfter(aborted)); // the model moved; this span's work is superseded } static void testOneSwitchAndItsMultiTickParkingAreOneSpan() { // Three tracks at a cost that admits one per tick, so the parking really does span // several timer callbacks — the case a per-write block turned into one undo point per // FX toggle. The span must open exactly once and close exactly once, on the tick that // settles, under the SWITCH's label rather than the reclaimer's. ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}"}}, {"{T2}", {"{FX-B}"}}, {"{T3}", {"{FX-C}"}}}); FakeHost host; host.chains["{T1}"] = onlineChain({"{FX-A}"}); host.chains["{T2}"] = onlineChain({"{FX-B}"}); host.chains["{T3}"] = onlineChain({"{FX-C}"}); host.stepCostMs = 50.0; // dearer than kOfflineBudgetMs ⇒ one track per tick // What applyMode leaves behind on a real switch: an open span, its label set, and no // close — the reclaimer's to finish. UndoSpan span; CHECK(span.begin(kProjA, "", 0)); span.relabel("ReaSampler: activate Design view", 9); host.span = &span; int closes = 0; FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}", "{T3}"}); auto tick = [&]() { const ReclaimRunResult r = q.run(host); if (spanEndsAfter(r) && span.end()) ++closes; return r; }; CHECK(!tick().settled); // hold starts host.clock += kParkHoldMs; CHECK(!tick().settled); // {T1} CHECK(!tick().settled); // {T2} CHECK(tick().settled); // {T3}, and the queue is exhausted in the same pass CHECK(host.writeLog.size() == 3); CHECK(host.spanOpens == 0); // the switch's span was already open — no second begin CHECK(closes == 1); // ...and exactly one close, on the settling tick CHECK(!span.open()); // Extra ticks after the settle must not produce a second close. tick(); CHECK(closes == 1); } static void testADrainNoSwitchOpenedMintsOnePointOfItsOwn() { // The non-switch origins — a membership edit's reapply, the load tick, a drain // resuming after the transport stopped. Nobody opened a span, so the drain opens its // own: one point for the whole convergence, never one per FX toggle. ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}}, {"{T2}", {"{FX-C}"}}}); FakeHost host; host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"}); host.chains["{T2}"] = onlineChain({"{FX-C}"}); host.stepCostMs = 50.0; UndoSpan span; host.span = &span; int closes = 0; FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"}); auto tick = [&]() { const ReclaimRunResult r = q.run(host); if (spanEndsAfter(r) && span.end()) ++closes; return r; }; CHECK(!tick().settled); // hold starts; nothing written, so nothing opened yet CHECK(!span.open()); CHECK(host.spanOpens == 0); host.clock += kParkHoldMs; CHECK(!tick().settled); // {T1}'s chain: the first write opens the span CHECK(host.spanOpens == 1); CHECK(span.open()); CHECK(span.label() == kReclaimUndoLabel); CHECK(span.project() == kProjA); CHECK(tick().settled); // {T2}, still under the SAME span CHECK(host.spanOpens == 1); CHECK(host.writeLog.size() == 3); CHECK(closes == 1); CHECK(!span.open()); } // -- 11. the state-load terminator ------------------------------------------------------ static void testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack() { // The contrast with testAReentrantInvalidationReachesNothingTheDrainIsUsing above, // which finishes its track's remaining three writes: an ordinary invalidation only // says the model moved, while a state load says the PROJECT is being replaced under // us and the block covering these writes has already closed. So a step is atomic only // up to the abandon — the half-parked chain that leaves is the accepted cost. ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}}, {"{T2}", {"{FX-C}"}}}); FakeHost host; host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"}); host.chains["{T2}"] = onlineChain({"{FX-C}"}); host.stepCostMs = 1.0; // cheap enough that only the abandon can stop the tick UndoSpan span; // what a real switch left open CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9)); host.span = &span; FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"}); q.run(host); // observes both parked at t=0 host.clock += kParkHoldMs; host.reentrant = &q; host.abandonAfterWrite = 1; const ReclaimRunResult r = q.run(host); CHECK(host.writeLog == std::vector{"{T1}:0=off"}); CHECK(host.offlineAt("{T1}", 0)); CHECK(!host.offlineAt("{T1}", 1)); // the same track, mid-chain — the partial park CHECK(!host.offlineAt("{T2}", 0)); // and every track after it, untouched CHECK(host.spanClosesMidDrain == 1); CHECK(!span.open()); CHECK(r.aborted); CHECK(!r.settled); CHECK(spanEndsAfter(r)); // the tick's own close still fires... CHECK(!span.end()); // ...and finds nothing: the terminator got there first // The work is owed again, and the loop is ASLEEP until something re-reads the model — // the reapply a reload drives is what raises the wakeup, and a load that drives none // must raise nothing (testARevertBetweenTicksLeavesTheLoopAsleepOnAStaleModel). CHECK(q.stale()); CHECK(!q.pending()); // ...and the stop belonged to that tick alone. The reloaded project's own drain writes. host.abandonAfterWrite = -1; q.adopt(derive(vm, {"{T1}", "{T2}"})); CHECK(q.run(host).writes == 2); // {T1}'s remaining slot, and {T2}'s whole chain CHECK(host.offlineAt("{T1}", 1)); CHECK(host.offlineAt("{T2}", 0)); } static void testARevertBetweenTicksLeavesTheLoopAsleepOnAStaleModel() { // File > Revert is the load that re-reads NOTHING: it keeps the same ReaProject, so the // session's identity poll answers NoOp, no reload follows and therefore no reapply // does either. The terminator is the only thing that runs, so whether it raises the // wakeup decides the whole outcome. Online work is the sharp case — undebounced, so ONE // woken tick both writes FX and spends the record's FX half, against the very model the // user reverted away from. The contrast with the mid-drain terminator test above: there // the load arrives through a write's own message pump, here between two ticks entirely. ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"}); vm.setActiveMode(kDesignModeId); vm.releaseSnapshotFlags("{T}"); FakeHost host; host.model = &vm; host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"}); ReleaseFlush flush; host.flush = &flush; FxReclaimQueue q; CHECK(q.beginTick(kProjA)); // where an earlier tick left it: pinned, nothing owed q.invalidate(); // applyMode, leaving the switch that brought {T} back CHECK(q.pending()); q.abandon(); CHECK(!q.pending()); CHECK(q.stale()); CHECK(q.items().empty()); CHECK(q.project() == kProjA); // the pin stays: it is what guards the debounce clock // Timer ticks keep arriving. The queue is empty AND stale, so it is the wakeup alone // that keeps the drain out — timerTick re-derives from the stale model exactly as the // shell would, which is what a raise here would have handed the drain. for (int i = 0; i < 3; ++i) timerTick(q, vm, {"{T}"}, host); CHECK(host.writeLog.empty()); CHECK(host.released.empty()); CHECK(!flush.pendingFor(kProjA)); // ...and the chain is untouched — still parked offline, as the revert left it. CHECK(host.offlineAt("{T}", 0) && host.offlineAt("{T}", 1)); // The contrast, so the assertions above are about the wakeup and not about this queue // being unable to converge: a load that DOES re-read the model reaches the reclaimer // through its reapply's invalidate, and the very same tick over the very same model // both writes and spends. q.invalidate(); timerTick(q, vm, {"{T}"}, host); CHECK(host.writeLog.size() == 2); CHECK(!host.offlineAt("{T}", 0) && !host.offlineAt("{T}", 1)); CHECK(host.released.count("{T}") == 1); CHECK(flush.pendingFor(kProjA)); } // -- 12. the direction the drain is moving ---------------------------------------------- static void testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold() { // The panel bug this surface exists for: `pending` is true from the instant of the // switch, but for the whole kParkHoldMs the only thing eligible is the RETURNING // chains' online work — an indicator driven off the queue's mere contents would name // parking while the drain is doing the opposite. 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; FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"}); CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online); CHECK(q.run(host).writes == 1); // the online item only — the offline is still held CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); host.clock += kParkHoldMs; CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline); CHECK(q.run(host).settled); CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); } static void testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures() { ViewModeModel vm = parkedDesignLeaf({"{FX-A}"}); FakeHost host; host.chains["{T}"] = onlineChain({"{FX-A}"}); FxReclaimQueue q = queueOf(vm, {"{T}"}); // Idle, not Offline: nothing may move, and the wait is the reason the span is allowed // to stay open across it. Reporting Offline here is what would light the indicator for // 3s before a single plugin moves. CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); CHECK(!q.run(host).settled); // the hold's origin is set by this tick's observe CHECK(host.writeLog.empty()); CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); host.clock += kParkHoldMs; CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline); CHECK(q.run(host).writes == 1); } static void testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline() { ViewModeModel vm; vm.membership().tag("{Z-back}", kArrangeModeId); vm.membership().tag("{A-park}", kDesignModeId); 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; host.rolling = true; FxReclaimQueue q = queueOf(vm, {"{Z-back}", "{A-park}"}); // Online is not transport-gated, so it both drains and reads through a rolling // transport — the second half of the panel bug: this work had no indicator at all. CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Online); CHECK(q.run(host).writes == 1); host.clock += kParkHoldMs; // The offline half is eligible now and still may not move, so it reads Idle rather // than naming a direction nothing is travelling in. CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); CHECK(q.run(host).transportDeferred); CHECK(host.writeLog.size() == 1); host.rolling = false; CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Offline); CHECK(q.run(host).writes == 1); } static void testTheDirectionHoldsThroughTheDrainsOwnMessagePump() { // A write pumps, and a repaint landing in there polls this. The queue object holds // NOTHING for the drain's whole duration (itemsSeenMidDrain == 0, asserted above), so // without the latch the indicator would read Idle at exactly the moment the plugin // loads the user is waiting through are happening. ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}}}); FakeHost host; host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"}); host.stepCostMs = 1.0; FxReclaimQueue q = queueOf(vm, {"{T1}"}); q.run(host); host.clock += kParkHoldMs; host.reentrant = &q; host.probeActivityAfterWrite = 1; CHECK(q.run(host).settled); CHECK(host.activityMidDrain == FxReclaimActivity::Offline); CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); // latch released } // -- 13. the sleep invariant ------------------------------------------------------------ // !pending() => !span.open(), at EVERY terminator. A span left open with the wakeup down // has nothing left that could ever close it: the timer gates every tick on pending(), and // the block then swallows the user's edits until a save, a state load, the next switch or // unload. The check below is written as that implication and applied to each pure move // that can put the loop to sleep, rather than falling out of one scenario. static bool asleepWithTheSpanOpen(const FxReclaimQueue& q, const UndoSpan& span) { return !q.pending() && span.open(); } static void testNoTerminatorLeavesTheSpanOpenWithTheLoopAsleep() { // FIRST, the predicate's own teeth: the violation is expressible, so a CHECK that it // does not occur is not vacuously true. { FxReclaimQueue q; UndoSpan span; span.begin(kProjA, "ReaSampler: activate Design view", 9); q.goIdle(); // the sleep WITHOUT the close — exactly the leak this test forbids CHECK(asleepWithTheSpanOpen(q, span)); } // 1. Every verdict a tick can come back with, swept whole. The shell closes on // spanEndsAfter and sleeps on settled, in that order (fxReclaimTick); the property // is that the second can never fire where the first did not. for (int bits = 0; bits < 8; ++bits) { ReclaimRunResult run; run.settled = (bits & 1) != 0; run.aborted = (bits & 2) != 0; run.transportDeferred = (bits & 4) != 0; FxReclaimQueue q; UndoSpan span; q.invalidate(); CHECK(q.beginTick(kProjA)); span.begin(kProjA, "ReaSampler: activate Design view", 9); if (spanEndsAfter(run)) span.end(); if (!run.aborted && run.settled) q.goIdle(); CHECK(!asleepWithTheSpanOpen(q, span)); } // 2. The state-load terminator. `abandon` is the one move that clears the wakeup with // work still owed, which is what makes the close-FIRST ordering load-bearing rather // than tidy. Proved both ways, not asserted by construction: the WRONG order below // (abandon, then close) is caught mid-sequence by the same predicate the RIGHT order // is checked against — swapping the two lines here would make the first CHECK below // fail, which is the teeth `fxReclaimAbandonForStateLoad` closing FIRST relies on. { FxReclaimQueue q; UndoSpan span; q.invalidate(); span.begin(kProjA, "ReaSampler: activate Design view", 9); q.abandon(); // WRONG order: wakeup cleared before the span closes CHECK(asleepWithTheSpanOpen(q, span)); // the exact leak the real order avoids span.end(); } { FxReclaimQueue q; UndoSpan span; q.invalidate(); span.begin(kProjA, "ReaSampler: activate Design view", 9); span.end(); // fxReclaimAbandonForStateLoad closes before it abandons q.abandon(); CHECK(!q.pending()); CHECK(!asleepWithTheSpanOpen(q, span)); } // 3. The project moving out from under the span. No active project sleeps the loop // (beginTick's nullptr arm), a tab switch does not — but both close, because the // span is keyed on its OWN project and neither one is it. kProjA is the control: a // tick landing back on the SAME project must NOT close the span, or a live switch's // block would drop the flags-and-items mask it opened with mid-drain. Sweeping all // three, with a branch-dependent CHECK, is what gives the guard real teeth — the // original two-value sweep always took the close branch, so the no-close branch was // never exercised. for (ReaProject* arrived : {kProjA, static_cast(nullptr), kProjB}) { FxReclaimQueue q; UndoSpan span; q.invalidate(); q.beginTick(kProjA); span.begin(kProjA, "ReaSampler: activate Design view", 9); if (span.project() != arrived) span.end(); q.beginTick(arrived); if (arrived == kProjA) CHECK(span.open()); // same project: nothing to close else CHECK(!span.open()); CHECK(!asleepWithTheSpanOpen(q, span)); } // 4. The terminators with no queue move at all — a project save, an extension unload, // the next real switch superseding. Each is the idempotent close alone, with no // guard and nothing else to sweep, so it reduces to end()'s own idempotency // (testTheSpanOpensOnceAndClosesOnceHoweverManyTimesItIsAsked) rather than adding a // separate claim here. } int main() { testFastFlipMovesZeroPluginsButAHeldParkStillReclaims(); testAWholeChainMovesInOneStepAndTheYieldFallsBetweenTracks(); testOnlineDrainGetsTheLargerBudget(); testATickStartedOnOnlineWorkDoesNotSpendIntoOfflineWork(); testModelChangeDiscardsDrainProgressButNotTheParkHold(); testAPluginTheUserOfflinedIsNeverOnlinedByTheDrain(); testSlotKeyedOnlineRecordRestoresByPosition(); testATrackThatLeftTheProjectCompletesRatherThanStalls(); testAReentrantInvalidationReachesNothingTheDrainIsUsing(); testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled(); testAnInvalidationDuringAnOnlineDropsThatTicksRelease(); testACompletedOnlineReachesDiskOnTheSaveAndNotOnTheDrain(); testAReleaseKilledBeforeTheFlushIsSimplyReEarned(); testATickThatSpendsNoRecordOwesNoSaveAnything(); testTheFlushArmSurvivesASaveAndCanBeReIssued(); testADropIsReportedOnceAcrossRepeatedRederivations(); testAnOfflineTickReportsNoDrops(); testATabSwitchRetargetsTheQueueAndLeavesTheWakeupStanding(); testGoingIdleKeepsTheParkHoldWhileARetargetDropsIt(); testOnlyANoProjectTickClearsTheWakeup(); testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo(); testARollingTransportDefersOfflineWorkAndKeepsTheHold(); testOnlineWorkProceedsWhileTheTransportRolls(); testOfflineWorkStaysGatedWhileOnlineDrainsInTheSameTick(); testAHoldDropsWhenItsTrackStopsOwingOfflineWorkWhileRolling(); testAGatedTickStillGoesIdleWithNothingLeftToConverge(); testTheSpanOpensOnceAndClosesOnceHoweverManyTimesItIsAsked(); testTheSpanOutlastsADebounceWaitButNeverATransportDeferral(); testAnAbortEndsTheSpanSoNoLaterSwitchNestsUnderIt(); testOneSwitchAndItsMultiTickParkingAreOneSpan(); testADrainNoSwitchOpenedMintsOnePointOfItsOwn(); testAStateLoadStopsTheDrainWhereItStandsRatherThanFinishingTheTrack(); testARevertBetweenTicksLeavesTheLoopAsleepOnAStaleModel(); testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold(); testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures(); testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline(); testTheDirectionHoldsThroughTheDrainsOwnMessagePump(); testNoTerminatorLeavesTheSpanOpenWithTheLoopAsleep(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }