331 lines
14 KiB
C++
331 lines
14 KiB
C++
// Standalone tests for reasampler::view's derived FX state — no REAPER, no test
|
|
// framework. Two halves: what desiredFxState decides is owed, and how one work
|
|
// item resolves against the chain as it stands (the chain mutates while a track
|
|
// is parked, which is the whole reason a slot cannot be the key).
|
|
|
|
#include "../src/core/view/fx_state.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
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)
|
|
|
|
// -- helpers -----------------------------------------------------------------
|
|
|
|
namespace {
|
|
|
|
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;
|
|
}
|
|
|
|
// A Design leaf parked with three identified FX, the middle one offline before
|
|
// the park because the USER put it there.
|
|
ViewModeModel parkedWithThreeFx() {
|
|
ViewModeModel vm;
|
|
vm.membership().tag("{T}", kDesignModeId);
|
|
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}}));
|
|
return vm;
|
|
}
|
|
|
|
const FxTrackWork* workFor(const std::vector<FxTrackWork>& work, const std::string& guid) {
|
|
for (const FxTrackWork& w : work)
|
|
if (w.trackGuid == guid) return &w;
|
|
return nullptr;
|
|
}
|
|
|
|
bool writeAt(const FxRestoreResolution& res, int fxIndex, bool offline) {
|
|
for (const FxOfflineWrite& w : res.writes)
|
|
if (w.fxIndex == fxIndex) return w.offline == offline;
|
|
return false;
|
|
}
|
|
|
|
bool anyWriteAt(const FxRestoreResolution& res, int fxIndex) {
|
|
for (const FxOfflineWrite& w : res.writes)
|
|
if (w.fxIndex == fxIndex) return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// -- desiredFxState: what is owed --------------------------------------------
|
|
|
|
static void testActiveTrackIsOwedOnlineInactiveIsOwedOffline() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
vm.membership().tag("{P}", kArrangeModeId);
|
|
vm.storeSnapshot("{P}", recordWithFx({{"{FX-P}", 0}}));
|
|
|
|
// Active mode is Arrange: {T} (Design) stays parked, {P} is coming back.
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kArrangeModeId, {"{T}", "{P}"});
|
|
CHECK(work.size() == 2);
|
|
CHECK(workFor(work, "{P}") && workFor(work, "{P}")->kind == FxWorkKind::Online);
|
|
CHECK(workFor(work, "{T}") && workFor(work, "{T}")->kind == FxWorkKind::Offline);
|
|
|
|
// Offline work names no targets: only the shell can read the live chain, and
|
|
// that chain IS the target set.
|
|
CHECK(workFor(work, "{T}") && workFor(work, "{T}")->targets.empty());
|
|
CHECK(workFor(work, "{P}") && workFor(work, "{P}")->targets.size() == 1);
|
|
}
|
|
|
|
static void testOnlineWorkSortsBeforeOfflineWork() {
|
|
// GUIDs chosen so plain GUID order would put every Offline track FIRST — the
|
|
// ordering under test has to beat the map's own order to pass.
|
|
ViewModeModel vm;
|
|
vm.membership().tag("{A-park}", kDesignModeId);
|
|
vm.membership().tag("{B-park}", kDesignModeId);
|
|
vm.membership().tag("{Y-back}", kArrangeModeId);
|
|
vm.membership().tag("{Z-back}", kArrangeModeId);
|
|
for (const char* g : {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"})
|
|
vm.storeSnapshot(g, recordWithFx({{"{FX}", 0}}));
|
|
|
|
const std::vector<FxTrackWork> work =
|
|
desiredFxState(vm, kArrangeModeId, {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"});
|
|
CHECK(work.size() == 4);
|
|
// Onlining restores state the user owns and is 2-6x the cost of offlining;
|
|
// offlining is opportunistic reclaim and may be deferred indefinitely.
|
|
CHECK(work[0].trackGuid == "{Y-back}" && work[0].kind == FxWorkKind::Online);
|
|
CHECK(work[1].trackGuid == "{Z-back}" && work[1].kind == FxWorkKind::Online);
|
|
CHECK(work[2].trackGuid == "{A-park}" && work[2].kind == FxWorkKind::Offline);
|
|
CHECK(work[3].trackGuid == "{B-park}" && work[3].kind == FxWorkKind::Offline);
|
|
|
|
// Deterministic: a re-derivation yields the identical list, so a drain's
|
|
// position in it survives one.
|
|
CHECK(desiredFxState(vm, kArrangeModeId, {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"}) ==
|
|
work);
|
|
}
|
|
|
|
static void testUserOfflinedPluginIsNeverBlanketOnlined() {
|
|
// The property this whole split exists for. {FX-B} was offline before the
|
|
// park because the user offlined it; coming back must leave it offline.
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
CHECK(work[0].kind == FxWorkKind::Online);
|
|
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(writeAt(res, 0, false));
|
|
CHECK(writeAt(res, 1, true)); // stays offline — NOT blanket-onlined
|
|
CHECK(writeAt(res, 2, false));
|
|
}
|
|
|
|
static void testReleasedFxHalfIsOwedNothing() {
|
|
// The chain is already back online; the record survives only for its flags.
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
vm.releaseSnapshotFx("{T}");
|
|
|
|
CHECK(desiredFxState(vm, kDesignModeId, {"{T}"}).empty());
|
|
CHECK(fxRestoreTargets(vm, "{T}").empty());
|
|
|
|
// ...and the mirror: a record whose FLAG half is spent still owes its FX.
|
|
ViewModeModel vm2 = parkedWithThreeFx();
|
|
CHECK(vm2.setActiveMode(kDesignModeId));
|
|
vm2.releaseSnapshotFlags("{T}");
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm2, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
CHECK(work.size() == 1 && work[0].targets.size() == 3);
|
|
}
|
|
|
|
static void testDeadAndUnparkedTracksAreOwedNothing() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
// The track is gone from the project: there is nothing to write to.
|
|
CHECK(desiredFxState(vm, kArrangeModeId, {}).empty());
|
|
|
|
// A track with no park record is owed nothing either way — that is also how a
|
|
// REFUSED park stays refused: it wrote no record, so no reclaim targets it.
|
|
ViewModeModel bare;
|
|
bare.membership().tag("{U}", kDesignModeId);
|
|
CHECK(desiredFxState(bare, kArrangeModeId, {"{U}"}).empty());
|
|
}
|
|
|
|
// -- resolveFxWork: against the chain as it stands ----------------------------
|
|
|
|
static void testOfflineWorkTargetsEveryLiveSlot() {
|
|
ViewModeModel vm = parkedWithThreeFx(); // active mode Arrange ⇒ {T} parks
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kArrangeModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
// Two plugins added while parked, one of which REAPER reports no GUID for.
|
|
// Reclaim has to reach those too, which is why offline work keys by slot.
|
|
const FxRestoreResolution res =
|
|
resolveFxWork(work[0], {"{FX-A}", "", "{FX-B}", "{FX-C}", "{FX-NEW}"});
|
|
CHECK(res.writes.size() == 5);
|
|
CHECK(res.drops.total() == 0);
|
|
for (int i = 0; i < 5; ++i) CHECK(writeAt(res, i, true));
|
|
}
|
|
|
|
static void testReorderRestoreLandsEachPluginItsOwnState() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
// Reordered to C, A, B while parked.
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-C}", "{FX-A}", "{FX-B}"});
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(writeAt(res, 1, false)); // A
|
|
CHECK(writeAt(res, 2, true)); // B's captured offline followed B, not slot 1
|
|
CHECK(writeAt(res, 0, false)); // C
|
|
}
|
|
|
|
static void testDeletedFxIsDroppedExplicitly() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
// B deleted while parked.
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 2);
|
|
CHECK(res.drops.missingIdentity == 1);
|
|
CHECK(writeAt(res, 0, false)); // A
|
|
CHECK(writeAt(res, 1, false)); // C — and NOT B's captured `true`
|
|
CHECK(!describeFxRestoreDrops(res.drops, 1).empty());
|
|
}
|
|
|
|
static void testAddedFxIsLeftAlone() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
const FxRestoreResolution res =
|
|
resolveFxWork(work[0], {"{FX-NEW}", "{FX-A}", "{FX-B}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(!anyWriteAt(res, 0)); // the added FX is never written
|
|
CHECK(writeAt(res, 1, false));
|
|
CHECK(writeAt(res, 2, true));
|
|
CHECK(writeAt(res, 3, false));
|
|
}
|
|
|
|
static void testSlotKeyedRecordStillRestoresByPosition() {
|
|
// A record lifted from a project saved before FX identity existed: position
|
|
// is all its bytes can support, and the targets carry that keying through.
|
|
ViewModeModel vm;
|
|
vm.membership().tag("{T}", kDesignModeId);
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
TrackSnapshot snap = recordWithFx({{"", 0}, {"", 1}, {"", 0}});
|
|
snap.fxKeying = FxKeying::Slot;
|
|
vm.storeSnapshot("{T}", snap);
|
|
|
|
const std::vector<FxOfflineOp> targets = fxRestoreTargets(vm, "{T}");
|
|
CHECK(targets.size() == 3);
|
|
CHECK(!targets.empty() && targets[0].keying == FxKeying::Slot);
|
|
|
|
const std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-X}", "{FX-Y}", "{FX-Z}"});
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(writeAt(res, 0, false));
|
|
CHECK(writeAt(res, 1, true));
|
|
CHECK(writeAt(res, 2, false));
|
|
}
|
|
|
|
// -- the resume point ---------------------------------------------------------
|
|
|
|
static void testResumePointSkipsWhatTheDrainAlreadyDid() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
work[0].resumeAfterFxGuid = "{FX-A}";
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 2);
|
|
CHECK(!anyWriteAt(res, 0)); // A is done
|
|
CHECK(writeAt(res, 1, true));
|
|
CHECK(writeAt(res, 2, false));
|
|
|
|
// Resuming after the LAST identity leaves nothing owed — the item is finished.
|
|
work[0].resumeAfterFxGuid = "{FX-C}";
|
|
CHECK(resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.empty());
|
|
}
|
|
|
|
static void testResumePointThatLeftTheChainRestartsRatherThanStrands() {
|
|
ViewModeModel vm = parkedWithThreeFx();
|
|
CHECK(vm.setActiveMode(kDesignModeId));
|
|
std::vector<FxTrackWork> work = desiredFxState(vm, kDesignModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
// The drain's cursor named a plugin the user has since deleted. Every write is
|
|
// compare-before-write, so re-walking the item is cheap and correct; honoring a
|
|
// cursor that matches nothing would strand the rest of the chain.
|
|
work[0].resumeAfterFxGuid = "{FX-GONE}";
|
|
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(writeAt(res, 1, true));
|
|
|
|
// Same rule on the offline side, where the cursor keys into the live chain.
|
|
ViewModeModel parked = parkedWithThreeFx(); // Arrange active ⇒ {T} parks
|
|
std::vector<FxTrackWork> offlineWork = desiredFxState(parked, kArrangeModeId, {"{T}"});
|
|
CHECK(offlineWork.size() == 1);
|
|
if (offlineWork.empty()) return;
|
|
offlineWork[0].resumeAfterFxGuid = "{FX-A}";
|
|
CHECK(resolveFxWork(offlineWork[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.size() == 2);
|
|
offlineWork[0].resumeAfterFxGuid = "{FX-GONE}";
|
|
CHECK(resolveFxWork(offlineWork[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.size() == 3);
|
|
}
|
|
|
|
static void testResumeAfterUnidentifiedFxMakesForwardProgress() {
|
|
// The drain completed slot 1, an FX REAPER reports no GUID for. Before the
|
|
// slot-cursor fix, an empty resumeAfterFxGuid was indistinguishable from
|
|
// "nothing done yet" and this item would restart at slot 0 every tick,
|
|
// forever — the non-terminating drain this test pins against.
|
|
ViewModeModel parked = parkedWithThreeFx(); // Arrange active ⇒ {T} parks (Offline, slot-keyed)
|
|
std::vector<FxTrackWork> work = desiredFxState(parked, kArrangeModeId, {"{T}"});
|
|
CHECK(work.size() == 1);
|
|
if (work.empty()) return;
|
|
|
|
work[0].resumeAfterFxGuid = "";
|
|
work[0].resumeAfterSlot = 1;
|
|
const FxRestoreResolution res =
|
|
resolveFxWork(work[0], {"{FX-A}", "", "{FX-B}", "{FX-C}"});
|
|
CHECK(res.writes.size() == 2); // slots 2 and 3 only — 0 and 1 already done
|
|
CHECK(!anyWriteAt(res, 0));
|
|
CHECK(!anyWriteAt(res, 1));
|
|
CHECK(anyWriteAt(res, 2));
|
|
CHECK(anyWriteAt(res, 3));
|
|
}
|
|
|
|
int main() {
|
|
testActiveTrackIsOwedOnlineInactiveIsOwedOffline();
|
|
testOnlineWorkSortsBeforeOfflineWork();
|
|
testUserOfflinedPluginIsNeverBlanketOnlined();
|
|
testReleasedFxHalfIsOwedNothing();
|
|
testDeadAndUnparkedTracksAreOwedNothing();
|
|
|
|
testOfflineWorkTargetsEveryLiveSlot();
|
|
testReorderRestoreLandsEachPluginItsOwnState();
|
|
testDeletedFxIsDroppedExplicitly();
|
|
testAddedFxIsLeftAlone();
|
|
testSlotKeyedRecordStillRestoresByPosition();
|
|
|
|
testResumePointSkipsWhatTheDrainAlreadyDid();
|
|
testResumePointThatLeftTheChainRestartsRatherThanStrands();
|
|
testResumeAfterUnidentifiedFxMakesForwardProgress();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|