// Standalone tests for the Design View park surface's pure decisions — no // REAPER, no test framework. // // 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" // makeParkPlan — the park flags' 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) // -- 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, {}, {})); } // -- the apply's one undo point ---------------------------------------------- // // 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 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 testAReapplyThatWroteSomethingLeavesAPoint() { // A tag/untag reapply that actually parked a track: real project state moved. CHECK(applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/true)); } 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() { testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked(); testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain(); testAnEmptyPlanProvesNothing(); testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse(); testACleanChainIsSnapshottedThenParked(); testAHeldSnapshotIsNeverOverwrittenWhateverTheChainReads(); testAParkedChainWithNoSnapshotIsRefusedRatherThanResnapshotted(); testRefusalNamesEveryRefusedTrackAndSaysNothingWhenNoneWere(); testRefusalRecoveryNamesThePerFxHalfAndAssertsNoCause(); testUnchangedOwnerAndSetStaysSilent(); testADifferentNamedSetReports(); testADifferentOwnerReportsEvenWithTheSameNames(); testAFreshEmptySetReportsAndResetsTheMemo(); testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite(); testAReapplyThatWroteSomethingLeavesAPoint(); testAReapplyThatWroteNothingLeavesNoPointAtAll(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }