5f6efb7cc3
view_state v2 writes identities beside the v1 slot array, so a downgrade keeps what it had. An FX gone at restore time is dropped and reported, never restored onto whatever took its place.
240 lines
9.9 KiB
C++
240 lines
9.9 KiB
C++
// Standalone tests for reasampler's per-FX offline restore resolution — no
|
|
// REAPER, no test framework.
|
|
//
|
|
// The property under test: a captured FX-offline state lands on the FX it was
|
|
// captured FROM, whatever that FX's slot has become while the track was parked.
|
|
// Every scenario below is a chain mutation performed while parked.
|
|
|
|
#include "../src/core/view/fx_offline.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace reasampler;
|
|
|
|
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 -----------------------------------------------------------------
|
|
|
|
// The ops the restore planner emits for one track from an identity-keyed
|
|
// snapshot: capture-order entries, each carrying the FX's own identity.
|
|
static std::vector<FxOfflineOp> identityOps(
|
|
const std::vector<std::pair<std::string, bool>>& captured) {
|
|
std::vector<FxOfflineOp> ops;
|
|
for (std::size_t i = 0; i < captured.size(); ++i) {
|
|
ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, captured[i].first,
|
|
static_cast<int>(i), captured[i].second});
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
// The ops a snapshot lifted from a pre-identity project yields: no identities,
|
|
// position is the slot.
|
|
static std::vector<FxOfflineOp> slotOps(const std::vector<bool>& captured) {
|
|
std::vector<FxOfflineOp> ops;
|
|
for (std::size_t i = 0; i < captured.size(); ++i) {
|
|
ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Slot, {},
|
|
static_cast<int>(i), captured[i]});
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static bool hasWrite(const FxRestoreResolution& res, int fxIndex, bool offline) {
|
|
for (const FxOfflineWrite& w : res.writes)
|
|
if (w.fxIndex == fxIndex) return w.offline == offline;
|
|
return false;
|
|
}
|
|
|
|
static bool writesTouch(const FxRestoreResolution& res, int fxIndex) {
|
|
for (const FxOfflineWrite& w : res.writes)
|
|
if (w.fxIndex == fxIndex) return true;
|
|
return false;
|
|
}
|
|
|
|
// -- 1. Chain reordered while parked -----------------------------------------
|
|
|
|
static void testReorderedChainRestoresEachPluginItsOwnState() {
|
|
// Captured with A, B, C in slots 0,1,2 — B was already offline pre-park.
|
|
const std::vector<FxOfflineOp> ops =
|
|
identityOps({{"{A}", false}, {"{B}", true}, {"{C}", false}});
|
|
|
|
// While parked the user dragged C to the front: the chain is now C, A, B.
|
|
const FxRestoreResolution res = resolveFxRestore(ops, {"{C}", "{A}", "{B}"});
|
|
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(hasWrite(res, 1, false)); // A, now at slot 1
|
|
CHECK(hasWrite(res, 2, true)); // B's offline state followed B to slot 2
|
|
CHECK(hasWrite(res, 0, false)); // C, now at slot 0
|
|
|
|
// The slot-keyed reading of the SAME capture is what the old code did: it
|
|
// would have written B's `true` to slot 1, which is now A. Pinning the
|
|
// divergence keeps a well-meant "just use the index" from coming back.
|
|
const FxRestoreResolution bySlot =
|
|
resolveFxRestore(slotOps({false, true, false}), {"{C}", "{A}", "{B}"});
|
|
CHECK(hasWrite(bySlot, 1, true)); // the defect, reproduced deliberately
|
|
}
|
|
|
|
// -- 2. FX deleted while parked ----------------------------------------------
|
|
|
|
static void testDeletedFxDropsExplicitlyAndTouchesNothingElse() {
|
|
const std::vector<FxOfflineOp> ops =
|
|
identityOps({{"{A}", false}, {"{B}", true}, {"{C}", true}});
|
|
|
|
// B was deleted while parked; A and C closed the gap.
|
|
const FxRestoreResolution res = resolveFxRestore(ops, {"{A}", "{C}"});
|
|
|
|
CHECK(res.writes.size() == 2);
|
|
CHECK(res.drops.missingIdentity == 1);
|
|
CHECK(res.drops.slotOutOfRange == 0);
|
|
CHECK(hasWrite(res, 0, false)); // A
|
|
CHECK(hasWrite(res, 1, true)); // C followed its identity to slot 1
|
|
// B's captured `true` did NOT land on whatever moved into slot 1.
|
|
CHECK(res.writes.size() == 2);
|
|
|
|
// The drop is reportable, not silent.
|
|
const std::string msg = describeFxRestoreDrops(res.drops, /*trackCount=*/1);
|
|
CHECK(!msg.empty());
|
|
CHECK(msg.find("1 captured FX offline state(s) on 1 track(s)") != std::string::npos);
|
|
CHECK(msg.find("no longer in the chain") != std::string::npos);
|
|
CHECK(msg.back() == '\n');
|
|
|
|
// Nothing dropped ⇒ nothing said.
|
|
CHECK(describeFxRestoreDrops(FxRestoreDrops{}, 0).empty());
|
|
}
|
|
|
|
static void testDescribeNamesBothDropKinds() {
|
|
FxRestoreDrops drops;
|
|
drops.missingIdentity = 2;
|
|
drops.slotOutOfRange = 3;
|
|
CHECK(drops.total() == 5);
|
|
|
|
const std::string msg = describeFxRestoreDrops(drops, /*trackCount=*/2);
|
|
CHECK(msg.find("5 captured FX offline state(s) on 2 track(s)") != std::string::npos);
|
|
CHECK(msg.find("2 FX no longer in the chain") != std::string::npos);
|
|
CHECK(msg.find("3 from a project saved before FX identity was recorded")
|
|
!= std::string::npos);
|
|
}
|
|
|
|
// -- 3. FX added while parked -------------------------------------------------
|
|
|
|
static void testAddedFxIsNotTouched() {
|
|
const std::vector<FxOfflineOp> ops = identityOps({{"{A}", false}, {"{B}", true}});
|
|
|
|
// D was inserted at the FRONT while parked — the case where an index-keyed
|
|
// restore would have written every captured state onto the wrong plugin.
|
|
const FxRestoreResolution res = resolveFxRestore(ops, {"{D}", "{A}", "{B}"});
|
|
|
|
CHECK(res.writes.size() == 2);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(hasWrite(res, 1, false)); // A
|
|
CHECK(hasWrite(res, 2, true)); // B
|
|
CHECK(!writesTouch(res, 0)); // D is never written at all
|
|
}
|
|
|
|
// -- 4. Two instances of the SAME plugin type --------------------------------
|
|
|
|
static void testTwoInstancesOfOnePluginKeyIndependently() {
|
|
// Two copies of one plugin: distinct instances, distinct identities, and the
|
|
// two carry OPPOSITE captured states — a scheme keyed on plugin type or name
|
|
// could not tell them apart and would restore both the same way.
|
|
const std::vector<FxOfflineOp> ops =
|
|
identityOps({{"{EQ-1}", true}, {"{EQ-2}", false}, {"{COMP}", false}});
|
|
|
|
// Swapped while parked: EQ-2, COMP, EQ-1.
|
|
const FxRestoreResolution res = resolveFxRestore(ops, {"{EQ-2}", "{COMP}", "{EQ-1}"});
|
|
|
|
CHECK(res.writes.size() == 3);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(hasWrite(res, 2, true)); // EQ-1's offline=true followed it to slot 2
|
|
CHECK(hasWrite(res, 0, false)); // EQ-2 stayed online at slot 0
|
|
CHECK(hasWrite(res, 1, false)); // COMP
|
|
}
|
|
|
|
// -- 5. Slot-keyed (pre-identity) snapshots ----------------------------------
|
|
|
|
static void testSlotKeyedSnapshotRestoresByPositionAndBoundsChecks() {
|
|
// All a pre-identity blob's bytes can support: position addressing.
|
|
const FxRestoreResolution res =
|
|
resolveFxRestore(slotOps({false, true}), {"{A}", "{B}", "{C}"});
|
|
CHECK(res.writes.size() == 2);
|
|
CHECK(res.drops.total() == 0);
|
|
CHECK(hasWrite(res, 0, false));
|
|
CHECK(hasWrite(res, 1, true));
|
|
CHECK(!writesTouch(res, 2)); // an FX the snapshot never covered stays untouched
|
|
|
|
// A slot that no longer exists is dropped and counted, never clamped.
|
|
const FxRestoreResolution shrunk =
|
|
resolveFxRestore(slotOps({false, true, true}), {"{A}"});
|
|
CHECK(shrunk.writes.size() == 1);
|
|
CHECK(shrunk.drops.slotOutOfRange == 2);
|
|
CHECK(shrunk.drops.missingIdentity == 0);
|
|
}
|
|
|
|
// -- 6. Degenerate inputs -----------------------------------------------------
|
|
|
|
static void testUnresolvableIdentityNeverFallsBackToItsSlot() {
|
|
// An identity-keyed entry with NO identity (REAPER reported none at capture)
|
|
// is a drop — the slot it happens to carry must not be used as a substitute.
|
|
std::vector<FxOfflineOp> ops = identityOps({{"{A}", false}});
|
|
ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, "", /*slot=*/1, /*offline=*/true});
|
|
|
|
const FxRestoreResolution res = resolveFxRestore(ops, {"{A}", "{B}"});
|
|
CHECK(res.writes.size() == 1);
|
|
CHECK(hasWrite(res, 0, false));
|
|
CHECK(!writesTouch(res, 1)); // {B} would have been the slot-1 victim
|
|
CHECK(res.drops.missingIdentity == 1);
|
|
}
|
|
|
|
static void testLiveFxWithNoIdentityIsNeverARestoreTarget() {
|
|
// The mirror case: a live FX REAPER reports no GUID for cannot be matched by
|
|
// an empty captured identity either.
|
|
const FxRestoreResolution res =
|
|
resolveFxRestore(identityOps({{"{A}", true}}), {"", "{A}"});
|
|
CHECK(res.writes.size() == 1);
|
|
CHECK(hasWrite(res, 1, true));
|
|
CHECK(!writesTouch(res, 0));
|
|
CHECK(res.drops.total() == 0);
|
|
}
|
|
|
|
static void testEmptyInputsProduceNoWrites() {
|
|
CHECK(resolveFxRestore({}, {"{A}"}).writes.empty());
|
|
CHECK(resolveFxRestore({}, {}).drops.total() == 0);
|
|
|
|
// Every FX gone (the whole chain cleared while parked): all dropped, none written.
|
|
const FxRestoreResolution res =
|
|
resolveFxRestore(identityOps({{"{A}", true}, {"{B}", false}}), {});
|
|
CHECK(res.writes.empty());
|
|
CHECK(res.drops.missingIdentity == 2);
|
|
}
|
|
|
|
static void testDuplicateLiveIdentityResolvesToTheFirstSlotOnly() {
|
|
// Not producible by REAPER (one GUID per instance) — pinned so a corrupt or
|
|
// hand-edited chain writes once, deterministically, instead of twice.
|
|
const FxRestoreResolution res =
|
|
resolveFxRestore(identityOps({{"{A}", true}}), {"{A}", "{A}"});
|
|
CHECK(res.writes.size() == 1);
|
|
CHECK(hasWrite(res, 0, true));
|
|
CHECK(!writesTouch(res, 1));
|
|
}
|
|
|
|
int main() {
|
|
testReorderedChainRestoresEachPluginItsOwnState();
|
|
testDeletedFxDropsExplicitlyAndTouchesNothingElse();
|
|
testDescribeNamesBothDropKinds();
|
|
testAddedFxIsNotTouched();
|
|
testTwoInstancesOfOnePluginKeyIndependently();
|
|
testSlotKeyedSnapshotRestoresByPositionAndBoundsChecks();
|
|
testUnresolvableIdentityNeverFallsBackToItsSlot();
|
|
testLiveFxWithNoIdentityIsNeverARestoreTarget();
|
|
testEmptyInputsProduceNoWrites();
|
|
testDuplicateLiveIdentityResolvesToTheFirstSlotOnly();
|
|
|
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
return g_fail ? 1 : 0;
|
|
}
|