9b6f725747
A Ctrl-Z mid-span used to close the pre-undo block over post-undo state; it now terminates at BeginLoadProjectState and stops the drain where it stands. fxReclaimActivity reports Online/Offline/Idle; fxReclaimPending is unchanged.
1425 lines
61 KiB
C++
1425 lines
61 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 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 <cstdint>
|
|
#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 ------------------------------------------------------------
|
|
|
|
// 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<ReaProject*>(static_cast<std::uintptr_t>(0x1));
|
|
ReaProject* const kProjB = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(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<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
|
|
|
|
// 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<std::string>& 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<int>(it->second.size())) return false;
|
|
if (it->second[static_cast<std::size_t>(fxIndex)].offline == offline) return false;
|
|
|
|
if (span) {
|
|
if (span->begin(project, kReclaimUndoLabel, 2)) ++spanOpens;
|
|
span->widen(2);
|
|
}
|
|
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();
|
|
}
|
|
if (reentrant && static_cast<int>(writeLog.size()) == probeActivityAfterWrite)
|
|
activityMidDrain = reentrant->activity(clock, rolling);
|
|
if (reentrant && static_cast<int>(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<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;
|
|
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<ViewModeModel> 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<ViewModeModel> 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<ViewModeModel> 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<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());
|
|
}
|
|
|
|
// -- 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<std::string>{"{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<TrackFxDrops> none;
|
|
const std::vector<TrackFxDrops> 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<ReclaimItem> 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<std::string>{"{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<ReclaimItem> 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<std::string>{"{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 wakeup stands and the work is owed again: going idle here would strand the
|
|
// half-parked chain for the rest of the session.
|
|
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));
|
|
}
|
|
|
|
// -- 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
|
|
}
|
|
|
|
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();
|
|
|
|
testTheDirectionReadsOnlineWhileTheParksOwnOfflineWorkWaitsOutItsHold();
|
|
testAnOfflineOnlyQueueReadsIdleUntilItsHoldMatures();
|
|
testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline();
|
|
testTheDirectionHoldsThroughTheDrainsOwnMessagePump();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|