Files
reasampler/tests/test_view_fx_park.cpp
T

421 lines
17 KiB
C++

// 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 <cstdio>
#include <string>
#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 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? -------------------------------------------
//
// The pre-park snapshot is restore's only source of truth, so one taken from an
// already-parked chain makes every later restore write hidden/out-of-mix/
// FX-disabled back, permanently. The park site cannot infer a clean chain from
// an absent snapshot — discardDeferredFxParks drops intents whose flag writes
// already landed — so the chain itself has to be asked.
// 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<TrackFlagOp>{}, FlagValues{0, 0, 0, 0}));
}
static void testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse() {
// The disjunction, pinned: a snapshot has a flag half and an FX half, and each
// is the only source of truth for its own. An AND here would re-open the
// defect one layer down — flags clean, FX still offline, snapshotted as truth.
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);
}
// -- 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);
}
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();
testTakeDetachesEverythingAndLeavesTheQueueEmpty();
testIntentsArrivingDuringADrainSurviveIt();
testAReEntrantParkCancelsOnlyWhatIsStillPending();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}