Clear the park snapshot where the restore is planned, not where it drains; make the drain re-entrant and reload-aware

This commit is contained in:
2026-08-03 13:35:30 -04:00
parent a4a1c3860f
commit 7169d7f22b
7 changed files with 298 additions and 91 deletions
+125 -5
View File
@@ -1,9 +1,10 @@
// Standalone tests for the deferred FX-park queue's re-entrancy rule — no
// REAPER, no test framework.
// 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 property 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.
// 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;
// and a cancel must not strand the pre-park FX state it was the last record of.
#include "../src/shell/view/view_fx_park.h"
@@ -43,6 +44,17 @@ static void testParkEnqueuesOneIntentCarryingNoOps() {
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
@@ -124,8 +136,109 @@ static void testClearDropsEverythingPending() {
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<FxOfflineOp> 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<FxOfflineOp> 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<FxOfflineOp> 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);
}
// -- re-entrancy -------------------------------------------------------------
static void testTakeDetachesEverythingAndLeavesTheQueueEmpty() {
FxParkQueue q;
q.park("{A}");
q.restore("{B}", ops("{FX}", true));
const std::vector<FxParkIntent> 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() {
// Applying an intent loads/unloads plugins, which pumps 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<FxParkIntent> 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<FxOfflineOp> 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();
@@ -133,6 +246,13 @@ int main() {
testOneTracksCancelLeavesEveryOtherTrackAlone();
testCancelledTrackCanBeQueuedAgain();
testClearDropsEverythingPending();
testParkHandsBackTheOpsOfTheRestoreItCancelled();
testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf();
testNothingCancelledLeavesTheFxHalfToTheCaller();
testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities();
testTakeDetachesEverythingAndLeavesTheQueueEmpty();
testIntentsArrivingDuringADrainSurviveIt();
testAReEntrantParkCancelsOnlyWhatIsStillPending();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;