Collapse a Design View mode switch to ONE undo point: FX writes run inline in applyMode's block, deferred park queue deleted
This commit is contained in:
+30
-313
@@ -1,16 +1,15 @@
|
||||
// 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.
|
||||
// Standalone tests for the Design View park surface's pure decisions — 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; a
|
||||
// pre-park snapshot is never taken from a chain a park has already touched; and
|
||||
// one drain closes as at most ONE undo point, in the FX domain only.
|
||||
// The properties under test: a pre-park snapshot is never taken from a chain a
|
||||
// park has already touched, the refusal that follows names its tracks and is
|
||||
// reported once per changed set, and one mode apply leaves at most ONE undo
|
||||
// point — none at all when a reapply found everything already where it wanted it.
|
||||
|
||||
#include "../src/shell/view/view_fx_park.h"
|
||||
#include "../src/shell/view/view.h" // applyMintsUndoPoint — the apply's undo-point fold
|
||||
|
||||
#include "core/view/view_mode_model.h" // makeRestorePlan — the ops' only producer
|
||||
#include "core/view/view_mode_model.h" // makeParkPlan — the park flags' only producer
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
@@ -22,219 +21,6 @@ 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<FxOfflineOp> 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<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);
|
||||
}
|
||||
|
||||
// -- 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
|
||||
@@ -360,99 +146,33 @@ static void testAFreshEmptySetReportsAndResetsTheMemo() {
|
||||
CHECK(!shouldReport(/*sameOwnerAsLast=*/true, {}, {}));
|
||||
}
|
||||
|
||||
// -- 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() {
|
||||
// [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<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);
|
||||
}
|
||||
|
||||
// -- the drain's undo point --------------------------------------------------
|
||||
// -- the apply's one undo point ----------------------------------------------
|
||||
//
|
||||
// These pin fxParkUndoClose's own fold only. The real wroteAnyFx verdict —
|
||||
// setOfflineIfChanged -> applyPark/applyRestore -> FxParkUndoBlock::noteWrite
|
||||
// -- runs against live REAPER calls and cannot be pinned here; docs/VERIFICATION.md
|
||||
// items 51 and 53 are the sole cover for that plumbing being wired correctly.
|
||||
// These pin applyMintsUndoPoint's fold only. The `wroteAnything` verdict it
|
||||
// consumes — writeIfChanged / parkTrack / restoreTrack, all running against live
|
||||
// REAPER calls — cannot be pinned here; docs/VERIFICATION.md §"Mode switching"
|
||||
// is the sole cover for that plumbing being wired correctly.
|
||||
|
||||
static void testADrainThatWroteFxClosesItsBlockAsOneNamedFxPoint() {
|
||||
const FxParkUndoClose close = fxParkUndoClose(true);
|
||||
|
||||
// 2 is UNDO_STATE_FX (reaper_plugin.h:1542), pinned as a literal here and
|
||||
// static_asserted against the macro in view_fx_park.cpp. The drain writes
|
||||
// per-FX offline and nothing else, so any wider mask would make it marshal
|
||||
// track config or items it never touched.
|
||||
CHECK(close.mask == 2);
|
||||
CHECK(close.label != nullptr && std::string(close.label) ==
|
||||
"ReaSampler: Design View FX state");
|
||||
static void testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite() {
|
||||
// An explicitly fired action stays undoable even when the plan found nothing
|
||||
// to write — the discard form is for reapplies only.
|
||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/false));
|
||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/true));
|
||||
}
|
||||
|
||||
static void testADrainThatWroteNothingClosesItsBlockAsADiscard() {
|
||||
// The project-load reapply: a park is planned for every inactive leaf, and a
|
||||
// project saved parked already holds every one of those FX offline. Nothing is
|
||||
// written, so the block must leave no undo point behind at all.
|
||||
const FxParkUndoClose close = fxParkUndoClose(false);
|
||||
static void testAReapplyThatWroteSomethingLeavesAPoint() {
|
||||
// A tag/untag reapply that actually parked a track: real project state moved.
|
||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/true));
|
||||
}
|
||||
|
||||
CHECK(close.mask == 0);
|
||||
CHECK(close.label != nullptr && std::string(close.label).empty());
|
||||
static void testAReapplyThatWroteNothingLeavesNoPointAtAll() {
|
||||
// The project-load reapply over a project saved fully parked: every flag and
|
||||
// every FX already sit where the plan wants them. Opening a project must not
|
||||
// cost the user an undo step.
|
||||
CHECK(!applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/false));
|
||||
}
|
||||
|
||||
int main() {
|
||||
testParkEnqueuesOneIntentCarryingNoOps();
|
||||
testParkReportsNothingCancelledWhenNoIntentWasPending();
|
||||
testParkOnItsOwnPendingParkReportsNothingCancelled();
|
||||
testRestoreOnAnUndrainedParkCancelsRatherThanStacks();
|
||||
testParkOnAnUndrainedRestoreCancelsRatherThanStacks();
|
||||
testRepeatedParkStaysOneIntent();
|
||||
testLaterRestoreReplacesTheEarlierOnesOps();
|
||||
testOneTracksCancelLeavesEveryOtherTrackAlone();
|
||||
testCancelledTrackCanBeQueuedAgain();
|
||||
testClearDropsEverythingPending();
|
||||
testParkHandsBackTheOpsOfTheRestoreItCancelled();
|
||||
testCancelledRestoreOpsBecomeTheFreshSnapshotsFxHalf();
|
||||
testNothingCancelledLeavesTheFxHalfToTheCaller();
|
||||
testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities();
|
||||
testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim();
|
||||
testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim();
|
||||
testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked();
|
||||
testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain();
|
||||
testAnEmptyPlanProvesNothing();
|
||||
@@ -468,12 +188,9 @@ int main() {
|
||||
testADifferentOwnerReportsEvenWithTheSameNames();
|
||||
testAFreshEmptySetReportsAndResetsTheMemo();
|
||||
|
||||
testTakeDetachesEverythingAndLeavesTheQueueEmpty();
|
||||
testIntentsArrivingDuringADrainSurviveIt();
|
||||
testAReEntrantParkCancelsOnlyWhatIsStillPending();
|
||||
|
||||
testADrainThatWroteFxClosesItsBlockAsOneNamedFxPoint();
|
||||
testADrainThatWroteNothingClosesItsBlockAsADiscard();
|
||||
testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite();
|
||||
testAReapplyThatWroteSomethingLeavesAPoint();
|
||||
testAReapplyThatWroteNothingLeavesNoPointAtAll();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user