Strip FX writes from the Design View mode switch; split view.cpp into park/lanes/reconcile/tracks and add the record-scoped undo repair pass

This commit is contained in:
2026-08-06 17:19:43 -04:00
parent d455b9a2fa
commit c4038a9e9c
22 changed files with 1172 additions and 1146 deletions
-197
View File
@@ -1,197 +0,0 @@
// 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 <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)
// -- 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}");
CHECK(park.flags.size() == 4);
CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0}));
}
static void testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain() {
const TrackPlan park = makeParkPlan("{A}");
// 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() {
// 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;
}
+5 -5
View File
@@ -816,11 +816,11 @@ using LiveFlags = std::map<std::string, int>;
// Returns nothing; mutates `vm` snapshots/active mode and `live` flags in place,
// exactly mirroring view.cpp's park then restore then setActiveMode ordering.
//
// Guarded, the park mirrors decidePark's three ways: a held snapshot parks
// without recapturing, a chain that already reads parked with no snapshot is
// REFUSED (left exactly as found), and only a clean chain is snapshotted. The one
// `live` flag stands in for the whole park-flag-plus-FX fold the shell reads —
// the model half of the defect is identical either way.
// Guarded, the park recaptures only when the model holds no record — the property
// the shell's parkTrack keeps. Its second guarded branch (a parked-looking chain
// with no record is left alone) outlived the shell rule it mirrored and is kept
// here only as a second unguarded-vs-guarded contrast; the model half of the defect
// is identical either way.
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
const std::string& target, bool guard) {
TogglePlan plan = vm.planToggle(tree, target);
+105
View File
@@ -0,0 +1,105 @@
// Standalone tests for the Design View park surface's pure decisions — no REAPER, no
// test framework.
//
// The properties under test: the four flags a park drives address exactly the four
// REAPER parameters the tool owns (never mute, never solo), 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_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 <cstdio>
#include <set>
#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)
// -- what a park is allowed to address ----------------------------------------
//
// trackFlagParm is the ONE mapping the park's pre-park read, the restore's write and
// the reconcile's repair all share; a mistyped parameter there is silent at compile
// time and drives the wrong track property forever.
static void testEachDrivenFlagAddressesItsOwnReaperParameter() {
const std::set<std::string> parms{trackFlagParm(Flag::ShowInTcp),
trackFlagParm(Flag::ShowInMixer),
trackFlagParm(Flag::MainSend),
trackFlagParm(Flag::FxEnable)};
CHECK(parms.size() == 4); // no two flags collapse onto one parameter
CHECK(parms.count("B_SHOWINTCP") == 1);
CHECK(parms.count("B_SHOWINMIXER") == 1);
CHECK(parms.count("B_MAINSEND") == 1);
CHECK(parms.count("I_FXEN") == 1);
}
static void testNoDrivenFlagCanEverAddressMuteOrSolo() {
// The never-touch-mute invariant, held at the one place a Flag becomes a REAPER
// parameter name.
for (const Flag f : {Flag::ShowInTcp, Flag::ShowInMixer, Flag::MainSend, Flag::FxEnable}) {
const std::string parm = trackFlagParm(f);
CHECK(parm != "B_MUTE");
CHECK(parm != "I_SOLO");
}
}
static void testAParkPlanDrivesEveryDrivenFlagToZero() {
// 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}");
CHECK(park.flags.size() == 4);
std::set<std::string> parms;
for (const TrackFlagOp& op : park.flags) {
CHECK(op.guid == "{A}");
CHECK(op.value == 0);
parms.insert(trackFlagParm(op.flag));
}
CHECK(parms.size() == 4);
}
// -- 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 already
// sits where the plan wants it. Opening a project must not cost the user an undo
// step.
CHECK(!applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/false));
}
int main() {
testEachDrivenFlagAddressesItsOwnReaperParameter();
testNoDrivenFlagCanEverAddressMuteOrSolo();
testAParkPlanDrivesEveryDrivenFlagToZero();
testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite();
testAReapplyThatWroteSomethingLeavesAPoint();
testAReapplyThatWroteNothingLeavesNoPointAtAll();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}
+158
View File
@@ -0,0 +1,158 @@
// Standalone tests for the Design View repair pass's pure decision — no REAPER, no
// test framework.
//
// The two properties the pass exists for: it drives a flag ONLY where a park record
// names the track (so a track the tool never parked can never have its main send
// re-enabled under it), and it fires on applied-output drift rather than on "an undo
// happened". Both AppliedStates are produced by the real fingerprint
// (view::appliedState) over real models, so a change to what the fingerprint carries
// shows up here rather than being papered over by hand-built fixtures.
#include "../src/shell/view/view_reconcile.h"
#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)
namespace {
TrackSnapshot captured(int showInTcp, int showInMixer, int mainSend, int fxEnable) {
TrackSnapshot snap;
snap.showInTcp = showInTcp;
snap.showInMixer = showInMixer;
snap.mainSend = mainSend;
snap.fxEnable = fxEnable;
return snap;
}
bool hasOp(const std::vector<TrackFlagOp>& ops, const std::string& guid, Flag flag, int value) {
for (const TrackFlagOp& op : ops)
if (op.guid == guid && op.flag == flag && op.value == value) return true;
return false;
}
bool namesTrack(const std::vector<TrackFlagOp>& ops, const std::string& guid) {
for (const TrackFlagOp& op : ops)
if (op.guid == guid) return true;
return false;
}
} // namespace
// -- the trigger: applied-output drift, not "an undo happened" -----------------
static void testAnUndoThatLeftOurAppliedOutputAloneRepairsNothing() {
// The rejected trigger is "did an undo occur" — an undo that touched nothing we
// drive still answers yes, and repairing on it rewrites state the user owns.
ViewModeModel model;
model.setActiveMode(kDesignModeId);
model.storeSnapshot("{A}", captured(1, 1, 1, 1));
const view::AppliedState state = view::appliedState(model);
CHECK(planReconcile(state, state).empty());
}
static void testADriftedActiveModeAloneIsNotAReasonToDriveATrack() {
// Drift is the gate, not the instruction: the record still has an owner, so the
// reapply that follows this pass is what drives it.
ViewModeModel applied;
applied.setActiveMode(kDesignModeId);
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
ViewModeModel reloaded; // active mode Arrange — the fingerprint differs
reloaded.storeSnapshot("{A}", captured(1, 1, 1, 1));
CHECK(view::appliedState(applied) != view::appliedState(reloaded));
CHECK(planReconcile(view::appliedState(applied), view::appliedState(reloaded)).empty());
}
// -- the scope rule: a record names the track, or nothing is written -----------
static void testARecordTheReloadedModelLostIsHandedBackItsCapturedValues() {
// The undo case: REAPER rolled view_state back to before the switch, so the model
// no longer records the park — but the flags are still driven on the track, and
// this memo is the only surviving account of what they were.
ViewModeModel applied;
applied.setActiveMode(kDesignModeId);
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
const ViewModeModel reloaded; // Arrange, no records at all
const std::vector<TrackFlagOp> ops =
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
CHECK(ops.size() == 4);
CHECK(hasOp(ops, "{A}", Flag::ShowInTcp, 1));
CHECK(hasOp(ops, "{A}", Flag::ShowInMixer, 1));
CHECK(hasOp(ops, "{A}", Flag::MainSend, 1));
CHECK(hasOp(ops, "{A}", Flag::FxEnable, 1));
}
static void testARepairedFlagReturnsToItsCapturedValueNeverToOn() {
// A track the user had OUT of the master mix and FX-bypassed before we parked it:
// the repair must hand back exactly that, not a hardcoded "on".
ViewModeModel applied;
applied.setActiveMode(kDesignModeId);
applied.storeSnapshot("{A}", captured(1, 0, 0, 0));
const ViewModeModel reloaded;
const std::vector<TrackFlagOp> ops =
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
CHECK(hasOp(ops, "{A}", Flag::MainSend, 0));
CHECK(hasOp(ops, "{A}", Flag::ShowInMixer, 0));
CHECK(hasOp(ops, "{A}", Flag::FxEnable, 0));
CHECK(!hasOp(ops, "{A}", Flag::MainSend, 1));
}
static void testATrackNeitherStateRecordsIsNeverDrivenAtAll() {
// The regression this rule is built against: a track the model merely calls
// active is NOT a track the tool parked. It holds no captured value, so asserting
// B_MAINSEND=1 on it would silently re-enable a send the user disabled by hand.
ViewModeModel applied;
applied.setActiveMode(kDesignModeId);
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
ViewModeModel reloaded;
reloaded.membership().tag("{B}", kArrangeModeId); // active, never parked, no record
const std::vector<TrackFlagOp> ops =
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
CHECK(namesTrack(ops, "{A}"));
CHECK(!namesTrack(ops, "{B}"));
}
static void testASpentFlagHalfIsNotARecordToRepairFrom() {
// A restore already handed the flags back and zeroed them on the record; the FX
// half outlives that. Repairing from the spent half would drive those zeros —
// i.e. re-park the track — so the fingerprint must not carry it.
ViewModeModel applied;
applied.setActiveMode(kDesignModeId);
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
applied.releaseSnapshotFlags("{A}");
const ViewModeModel reloaded;
CHECK(planReconcile(view::appliedState(applied), view::appliedState(reloaded)).empty());
}
int main() {
testAnUndoThatLeftOurAppliedOutputAloneRepairsNothing();
testADriftedActiveModeAloneIsNotAReasonToDriveATrack();
testARecordTheReloadedModelLostIsHandedBackItsCapturedValues();
testARepairedFlagReturnsToItsCapturedValueNeverToOn();
testATrackNeitherStateRecordsIsNeverDrivenAtAll();
testASpentFlagHalfIsNotARecordToRepairFrom();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}