// Standalone tests for the deferred FX-park queue's re-entrancy rule and the // snapshot-lifecycle contract that rides on it — no REAPER, no test framework. // // The properties under test: a mode switch leaves its per-FX offline work here, // so a second switch arriving before the first drained must leave every track in // the state the SECOND switch specifies — never the first's, never both replayed; // a cancel must not strand the pre-park FX state it was the last record of; and // a pre-park snapshot is never taken from a chain a park has already touched. #include "../src/shell/view/view_fx_park.h" #include "core/view/view_mode_model.h" // makeRestorePlan — the ops' only producer #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 applyMode hands a restore: one per FX captured in the track's snapshot. static std::vector ops(const std::string& fxGuid, bool offline) { return {FxOfflineOp{"{TRACK}", FxKeying::Identity, fxGuid, 0, offline}}; } static const FxParkIntent* intentFor(const FxParkQueue& q, const std::string& guid) { for (const FxParkIntent& i : q.pending()) if (i.guid == guid) return &i; return nullptr; } // -- tests ------------------------------------------------------------------- static void testParkEnqueuesOneIntentCarryingNoOps() { FxParkQueue q; q.park("{A}"); CHECK(q.pending().size() == 1); const FxParkIntent* held = intentFor(q, "{A}"); CHECK(held != nullptr); CHECK(held && held->park); CHECK(held && held->restoreOps.empty()); } static void testParkReportsNothingCancelledWhenNoIntentWasPending() { FxParkQueue q; CHECK(q.park("{A}").empty()); } static void testParkOnItsOwnPendingParkReportsNothingCancelled() { FxParkQueue q; q.park("{A}"); CHECK(q.park("{A}").empty()); } static void testRestoreOnAnUndrainedParkCancelsRatherThanStacks() { // The park never ran, so the track's FX still hold their captured state — // exactly what the restore would write. Replaying both would unload every // plugin only to reload it. FxParkQueue q; q.park("{A}"); q.restore("{A}", ops("{FX}", false)); CHECK(q.empty()); } static void testParkOnAnUndrainedRestoreCancelsRatherThanStacks() { // The mirror case: the restore never ran, so the FX are still parked offline, // which is where the new park wants them. FxParkQueue q; q.restore("{A}", ops("{FX}", false)); q.park("{A}"); CHECK(q.empty()); } static void testRepeatedParkStaysOneIntent() { FxParkQueue q; q.park("{A}"); q.park("{A}"); q.park("{A}"); CHECK(q.pending().size() == 1); CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park); } static void testLaterRestoreReplacesTheEarlierOnesOps() { FxParkQueue q; q.restore("{A}", ops("{OLD}", false)); q.restore("{A}", ops("{NEW}", true)); CHECK(q.pending().size() == 1); const FxParkIntent* held = intentFor(q, "{A}"); CHECK(held && !held->park); CHECK(held && held->restoreOps.size() == 1); CHECK(held && held->restoreOps.front().fxGuid == "{NEW}"); CHECK(held && held->restoreOps.front().offline); } static void testOneTracksCancelLeavesEveryOtherTrackAlone() { FxParkQueue q; q.park("{A}"); q.park("{B}"); q.park("{C}"); q.restore("{B}", ops("{FX}", false)); // cancels B only CHECK(q.pending().size() == 2); CHECK(intentFor(q, "{A}") != nullptr); CHECK(intentFor(q, "{B}") == nullptr); CHECK(intentFor(q, "{C}") != nullptr); // Order survives the middle erase: the drain applies in enqueue order. CHECK(q.pending()[0].guid == "{A}"); CHECK(q.pending()[1].guid == "{C}"); } static void testCancelledTrackCanBeQueuedAgain() { // Two rapid switches then a third: the third is the one that must land. FxParkQueue q; q.park("{A}"); q.restore("{A}", ops("{FX}", false)); q.park("{A}"); CHECK(q.pending().size() == 1); CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park); } static void testClearDropsEverythingPending() { FxParkQueue q; q.park("{A}"); q.restore("{B}", ops("{FX}", true)); q.clear(); CHECK(q.empty()); CHECK(q.pending().empty()); } // -- snapshot lifecycle ------------------------------------------------------ static void testParkHandsBackTheOpsOfTheRestoreItCancelled() { // The cancelled restore is the LAST record of the pre-park FX state: the // track's chain still reads the parked values (the restore never ran), and // the cancel means no drain will ever put them back. A park that drops these // snapshots the park's own offline zeros as if they were the user's state. FxParkQueue q; q.restore("{A}", ops("{FX}", false)); const std::vector cancelled = q.park("{A}"); CHECK(cancelled.size() == 1); CHECK(cancelled.size() == 1 && cancelled.front().fxGuid == "{FX}"); CHECK(cancelled.size() == 1 && !cancelled.front().offline); CHECK(q.empty()); // annihilated: the chain already holds what the park wants } static void testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf() { std::vector cancelled = ops("{ONE}", true); cancelled.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, "{TWO}", 1, false}); const PreParkFx fx = preParkFxFromCancelledRestore(cancelled); CHECK(fx.keying == FxKeying::Identity); CHECK(fx.states.size() == 2); CHECK(fx.states.size() == 2 && fx.states[0].fxGuid == "{ONE}" && fx.states[0].offline == 1); CHECK(fx.states.size() == 2 && fx.states[1].fxGuid == "{TWO}" && fx.states[1].offline == 0); } static void testNothingCancelledLeavesTheFxHalfToTheCaller() { const PreParkFx fx = preParkFxFromCancelledRestore({}); CHECK(fx.states.empty()); // caller reads the live chain instead CHECK(fx.keying == FxKeying::Identity); } static void testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities() { // A snapshot lifted from a pre-identity view_state is slot-keyed and carries // no fxGuid. Re-labelling it Identity would make resolveFxRestore drop every // entry as unidentified instead of writing it by slot. std::vector cancelled = { FxOfflineOp{"{TRACK}", FxKeying::Slot, "", 0, true}, FxOfflineOp{"{TRACK}", FxKeying::Slot, "", 1, false}, }; const PreParkFx fx = preParkFxFromCancelledRestore(cancelled); CHECK(fx.keying == FxKeying::Slot); CHECK(fx.states.size() == 2); CHECK(fx.states.size() == 2 && fx.states[0].offline == 1 && fx.states[1].offline == 0); } // -- the makeRestorePlan <-> preParkFxFromCancelledRestore round trip --------- // // The cancel path's whole premise is that a planned restore's ops are a LOSSLESS // carrier of the snapshot's FX half. makeRestorePlan is their only producer, so // the real claim is that the pair composes to the identity on (fxOffline, // fxKeying). Asserting it against hand-built ops would let a change to // makeRestorePlan's field mapping or op ordering pass with every test green. static void testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim() { TrackSnapshot snap; snap.fxKeying = FxKeying::Identity; snap.fxOffline = {FxOfflineState{"{ONE}", 1}, FxOfflineState{"{TWO}", 0}, FxOfflineState{"{THREE}", 1}}; const TrackPlan plan = makeRestorePlan("{TRACK}", snap); const PreParkFx rebuilt = preParkFxFromCancelledRestore(plan.fxOffline); CHECK(rebuilt.keying == FxKeying::Identity); // Three DISTINCT entries, compared as a sequence: a dropped fxGuid, a flipped // offline, or a reordering each fail here. CHECK(rebuilt.states == snap.fxOffline); } static void testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim() { TrackSnapshot snap; snap.fxKeying = FxKeying::Slot; snap.fxOffline = {FxOfflineState{"", 0}, FxOfflineState{"", 1}, FxOfflineState{"", 1}}; const TrackPlan plan = makeRestorePlan("{TRACK}", snap); // Slot keying addresses by POSITION, so the identity holds only while the op // at index i carries slot i. CHECK(plan.fxOffline.size() == 3); CHECK(plan.fxOffline.size() == 3 && plan.fxOffline[0].slot == 0 && plan.fxOffline[1].slot == 1 && plan.fxOffline[2].slot == 2); const PreParkFx rebuilt = preParkFxFromCancelledRestore(plan.fxOffline); CHECK(rebuilt.keying == FxKeying::Slot); CHECK(rebuilt.states == snap.fxOffline); } // -- may this chain be snapshotted? ------------------------------------------- // // Rationale: the snapshot-source invariant (view_fx_park.h, this directory's // CLAUDE.md). // The live values the fold reads, keyed by Flag — so an assertion names the flag // it varies rather than a position in makeParkPlan's op order. struct FlagValues { int showInTcp = 0, showInMixer = 0, mainSend = 0, fxEnable = 0; int operator()(Flag f) const { switch (f) { case Flag::ShowInTcp: return showInTcp; case Flag::ShowInMixer: return showInMixer; case Flag::MainSend: return mainSend; case Flag::FxEnable: return fxEnable; } return -1; } }; static void testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked() { // The four zeros are what a parked track's driven flags actually read; if // makeParkPlan ever writes something else, this is the test that says so. const TrackPlan park = makeParkPlan("{A}", /*fxCount=*/0); CHECK(park.flags.size() == 4); CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0})); } static void testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain() { const TrackPlan park = makeParkPlan("{A}", /*fxCount=*/0); // Each case varies exactly one flag, so together they also prove all four are // in the plan: a missing op makes its case read parked and fail here. CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{1, 0, 0, 0})); CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 1, 0, 0})); CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 1, 0})); CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 1})); } static void testAnEmptyPlanProvesNothing() { CHECK(!parkFlagsAlreadyApplied(std::vector{}, FlagValues{0, 0, 0, 0})); } static void testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse() { // Disjunction pinned; rationale at chainReadsParked (view_fx_park.h). CHECK(!chainReadsParked(/*parkFlagsRead=*/false, /*anyFxOffline=*/false)); CHECK(chainReadsParked(/*parkFlagsRead=*/true, /*anyFxOffline=*/false)); CHECK(chainReadsParked(/*parkFlagsRead=*/false, /*anyFxOffline=*/true)); CHECK(chainReadsParked(/*parkFlagsRead=*/true, /*anyFxOffline=*/true)); } static void testACleanChainIsSnapshottedThenParked() { CHECK(decidePark(/*haveSnapshot=*/false, /*chainReadsParked=*/false) == ParkAction::SnapshotThenPark); } static void testAHeldSnapshotIsNeverOverwrittenWhateverTheChainReads() { // The held snapshot IS the pre-park truth, so the chain is not consulted — // which is what lets the shell skip the live flag reads on this path. CHECK(decidePark(/*haveSnapshot=*/true, /*chainReadsParked=*/false) == ParkAction::ParkOnly); CHECK(decidePark(/*haveSnapshot=*/true, /*chainReadsParked=*/true) == ParkAction::ParkOnly); } static void testAParkedChainWithNoSnapshotIsRefusedRatherThanResnapshotted() { // The defect this whole section exists for: the truth is gone, so the only // non-destructive act is to leave the track alone. Snapshotting here commits // park state as the user's state and no later restore can undo it. CHECK(decidePark(/*haveSnapshot=*/false, /*chainReadsParked=*/true) == ParkAction::Refuse); } static void testRefusalNamesEveryRefusedTrackAndSaysNothingWhenNoneWere() { CHECK(describeRefusedParks({}).empty()); const std::string one = describeRefusedParks({"Bass"}); CHECK(one.find("1 track ") != std::string::npos); CHECK(one.find("Bass") != std::string::npos); const std::string two = describeRefusedParks({"Bass", "Drum bus"}); CHECK(two.find("2 tracks ") != std::string::npos); CHECK(two.find("Bass") != std::string::npos); CHECK(two.find("Drum bus") != std::string::npos); } static void testRefusalRecoveryNamesThePerFxHalfAndAssertsNoCause() { const std::string msg = describeRefusedParks({"Bass"}); // I_FXEN is the chain bypass: a user who restores only the four flags leaves // every individually offlined FX offline and walks straight back into a // refusal, so the recovery has to spell the per-FX step out. CHECK(msg.find("every FX in its chain online") != std::string::npos); // And it must not name a cause: the pair has several routes, and on a track // the user themselves keeps hidden/bypassed there was no lost state at all. CHECK(msg.find("undo") == std::string::npos); CHECK(msg.find("lost") == std::string::npos); } // -- the refusal memo's print-suppression gate -------------------------------- // // reportRefusedParks' own memo (owner + last-reported names, both REAPER-side // statics) can't be driven from here, but the pure gate behind it can: prints // (i.e. updates the memo) on any change, stays silent on an exact repeat, and // treats a fresh empty set as a change too, so a later real refusal is never // mistaken for a repeat of one that already healed. static void testUnchangedOwnerAndSetStaysSilent() { CHECK(!shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {"Bass"})); } static void testADifferentNamedSetReports() { CHECK(shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {"Bass", "Drum bus"})); } static void testADifferentOwnerReportsEvenWithTheSameNames() { // Two alternating project tabs must not suppress each other's first refusal. CHECK(shouldReport(/*sameOwnerAsLast=*/false, {"Bass"}, {"Bass"})); } static void testAFreshEmptySetReportsAndResetsTheMemo() { CHECK(shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {})); // Already empty, same owner: nothing changed, stays silent. CHECK(!shouldReport(/*sameOwnerAsLast=*/true, {}, {})); } // -- re-entrancy ------------------------------------------------------------- static void testTakeDetachesEverythingAndLeavesTheQueueEmpty() { FxParkQueue q; q.park("{A}"); q.restore("{B}", ops("{FX}", true)); const std::vector taken = q.take(); CHECK(taken.size() == 2); CHECK(taken.size() == 2 && taken[0].guid == "{A}" && taken[0].park); CHECK(taken.size() == 2 && taken[1].guid == "{B}" && !taken[1].park); CHECK(q.empty()); } static void testIntentsArrivingDuringADrainSurviveIt() { // [verify — DAW] applying an intent loads/unloads plugins, which is ASSUMED to // pump the message loop, so a switch can re-enter and enqueue mid-drain. Those // intents belong to the NEXT drain — the one in progress must neither see them // nor discard them. FxParkQueue q; q.park("{A}"); const std::vector draining = q.take(); q.restore("{B}", ops("{FX}", false)); // arrives while {A} is being applied CHECK(draining.size() == 1); CHECK(draining.size() == 1 && draining.front().guid == "{A}"); CHECK(q.pending().size() == 1); CHECK(intentFor(q, "{B}") != nullptr); } static void testAReEntrantParkCancelsOnlyWhatIsStillPending() { // {A}'s restore was already taken for the in-flight drain, so a park arriving // mid-drain has nothing to cancel — it must queue as a fresh park rather than // silently annihilate against an intent that has already been applied. FxParkQueue q; q.restore("{A}", ops("{FX}", false)); q.take(); const std::vector cancelled = q.park("{A}"); CHECK(cancelled.empty()); CHECK(q.pending().size() == 1); CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park); } int main() { testParkEnqueuesOneIntentCarryingNoOps(); testParkReportsNothingCancelledWhenNoIntentWasPending(); testParkOnItsOwnPendingParkReportsNothingCancelled(); testRestoreOnAnUndrainedParkCancelsRatherThanStacks(); testParkOnAnUndrainedRestoreCancelsRatherThanStacks(); testRepeatedParkStaysOneIntent(); testLaterRestoreReplacesTheEarlierOnesOps(); testOneTracksCancelLeavesEveryOtherTrackAlone(); testCancelledTrackCanBeQueuedAgain(); testClearDropsEverythingPending(); testParkHandsBackTheOpsOfTheRestoreItCancelled(); testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf(); testNothingCancelledLeavesTheFxHalfToTheCaller(); testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities(); testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim(); testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim(); testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked(); testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain(); testAnEmptyPlanProvesNothing(); testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse(); testACleanChainIsSnapshottedThenParked(); testAHeldSnapshotIsNeverOverwrittenWhateverTheChainReads(); testAParkedChainWithNoSnapshotIsRefusedRatherThanResnapshotted(); testRefusalNamesEveryRefusedTrackAndSaysNothingWhenNoneWere(); testRefusalRecoveryNamesThePerFxHalfAndAssertsNoCause(); testUnchangedOwnerAndSetStaysSilent(); testADifferentNamedSetReports(); testADifferentOwnerReportsEvenWithTheSameNames(); testAFreshEmptySetReportsAndResetsTheMemo(); testTakeDetachesEverythingAndLeavesTheQueueEmpty(); testIntentsArrivingDuringADrainSurviveIt(); testAReEntrantParkCancelsOnlyWhatIsStillPending(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }