// 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 #include #include #include 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 identityOps( const std::vector>& captured) { std::vector ops; for (std::size_t i = 0; i < captured.size(); ++i) { ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, captured[i].first, static_cast(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 slotOps(const std::vector& captured) { std::vector ops; for (std::size_t i = 0; i < captured.size(); ++i) { ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Slot, {}, static_cast(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 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 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 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 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 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; }