Ψ-W1-T2: disjoint per-mode solo surfaces and a playback-gated mode switch

Solo is cached, cleared and replayed per mode on a real switch only; the switch is refused visibly while the transport runs. The footer segment now routes through the activate actions, so a panel switch finally persists.
This commit is contained in:
2026-08-01 19:43:18 -04:00
parent 8bf6841f7b
commit 9c234c2e6b
23 changed files with 811 additions and 49 deletions
+176
View File
@@ -0,0 +1,176 @@
// Standalone tests for reasampler::view::solo_cache — no REAPER, no test framework.
//
// Covers: the soloed-subset filter (zeros and empty GUIDs dropped, raw values kept),
// the cache's store/query/clear lifecycle including the empty-set-removes rule, GUID
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, parked
// track).
#include "../src/core/view/solo_cache.h"
#include <cstdio>
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// --- soloedTracks: the filter ------------------------------------------------
static void testUnsoloedProjectYieldsNothingToCache() {
const std::map<std::string, int> soloed =
soloedTracks({{"{A}", 0}, {"{B}", 0}, {"{C}", 0}});
CHECK(soloed.empty());
}
static void testSoloedTracksKeepsRawValueNotABoolean() {
const std::map<std::string, int> soloed =
soloedTracks({{"{A}", 1}, {"{B}", 0}, {"{C}", 2}, {"{D}", 6}});
CHECK(soloed.size() == 3);
CHECK(soloed.at("{A}") == 1); // plain solo
CHECK(soloed.at("{C}") == 2); // solo-in-place survives
CHECK(soloed.at("{D}") == 6); // safe solo-in-place survives
CHECK(soloed.count("{B}") == 0);
}
static void testSoloedTracksDropsEmptyGuids() {
const std::map<std::string, int> soloed = soloedTracks({{"", 1}, {"{A}", 1}});
CHECK(soloed.size() == 1);
CHECK(soloed.count("{A}") == 1);
}
// --- SoloCache: store / query / clear ----------------------------------------
static void testStoreThenQueryReturnsTheStoredSet() {
SoloCache cache;
CHECK(cache.store("arrange", {{"{A}", 1}, {"{B}", 2}}));
const std::map<std::string, int>* got = cache.query("arrange");
CHECK(got != nullptr);
CHECK(got->size() == 2);
CHECK(got->at("{A}") == 1);
CHECK(got->at("{B}") == 2);
CHECK(cache.query("design") == nullptr);
}
static void testStoringAnEmptySetRemovesTheModeEntry() {
SoloCache cache;
cache.store("arrange", {{"{A}", 1}});
CHECK(cache.query("arrange") != nullptr);
// Unsoloing everything and switching away must leave no record behind — an
// empty record would not survive the serialize round trip.
CHECK(cache.store("arrange", {}));
CHECK(cache.query("arrange") == nullptr);
CHECK(cache.empty());
}
static void testStoreReplacesRatherThanMerges() {
SoloCache cache;
cache.store("design", {{"{A}", 1}, {"{B}", 1}});
cache.store("design", {{"{C}", 2}});
const std::map<std::string, int>* got = cache.query("design");
CHECK(got != nullptr);
CHECK(got->size() == 1);
CHECK(got->count("{C}") == 1);
}
static void testStoreRejectsAnEmptyModeId() {
SoloCache cache;
CHECK(!cache.store("", {{"{A}", 1}}));
CHECK(cache.empty());
}
static void testClearConsumesOnlyTheNamedMode() {
SoloCache cache;
cache.store("arrange", {{"{A}", 1}});
cache.store("design", {{"{B}", 1}});
CHECK(cache.clear("arrange"));
CHECK(cache.query("arrange") == nullptr);
CHECK(cache.query("design") != nullptr);
CHECK(!cache.clear("arrange")); // already consumed
}
// --- SoloCache::reconcile ----------------------------------------------------
static void testReconcileDropsDeadGuidsAcrossEveryMode() {
SoloCache cache;
cache.store("arrange", {{"{LIVE}", 1}, {"{DEAD}", 2}});
cache.store("design", {{"{DEAD}", 1}});
CHECK(cache.reconcile({"{LIVE}"}) == 2);
const std::map<std::string, int>* arrange = cache.query("arrange");
CHECK(arrange != nullptr);
CHECK(arrange->size() == 1);
CHECK(arrange->count("{LIVE}") == 1);
// The design entry lost its only track, so the mode record goes with it.
CHECK(cache.query("design") == nullptr);
}
static void testReconcileWithEveryGuidLiveRemovesNothing() {
SoloCache cache;
cache.store("arrange", {{"{A}", 1}, {"{B}", 5}});
CHECK(cache.reconcile({"{A}", "{B}", "{UNRELATED}"}) == 0);
CHECK(cache.query("arrange")->size() == 2);
}
// --- planSoloRestore ---------------------------------------------------------
static void testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim() {
const std::vector<SoloOp> ops =
planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {});
CHECK(ops.size() == 2);
CHECK(ops[0] == (SoloOp{"{A}", 1}));
CHECK(ops[1] == (SoloOp{"{B}", 2}));
}
static void testRestorePlanSkipsAGuidThatNoLongerExists() {
const std::vector<SoloOp> ops =
planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {});
CHECK(ops.size() == 1);
CHECK(ops[0].guid == "{HERE}");
}
static void testRestorePlanSkipsATrackParkedInTheIncomingMode() {
// Soloing a parked track would silence the mix while contributing nothing
// audible, and the track is hidden, so the user could not undo it.
const std::vector<SoloOp> ops =
planSoloRestore({{"{PARKED}", 1}, {"{VISIBLE}", 2}}, {"{PARKED}", "{VISIBLE}"},
{"{PARKED}"});
CHECK(ops.size() == 1);
CHECK(ops[0] == (SoloOp{"{VISIBLE}", 2}));
}
static void testRestorePlanOfAnEmptyCacheWritesNothing() {
CHECK(planSoloRestore({}, {"{A}"}, {}).empty());
}
int main() {
testUnsoloedProjectYieldsNothingToCache();
testSoloedTracksKeepsRawValueNotABoolean();
testSoloedTracksDropsEmptyGuids();
testStoreThenQueryReturnsTheStoredSet();
testStoringAnEmptySetRemovesTheModeEntry();
testStoreReplacesRatherThanMerges();
testStoreRejectsAnEmptyModeId();
testClearConsumesOnlyTheNamedMode();
testReconcileDropsDeadGuidsAcrossEveryMode();
testReconcileWithEveryGuidLiveRemovesNothing();
testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim();
testRestorePlanSkipsAGuidThatNoLongerExists();
testRestorePlanSkipsATrackParkedInTheIncomingMode();
testRestorePlanOfAnEmptyCacheWritesNothing();
if (g_fail == 0) std::printf("solo_cache: all tests passed\n");
return g_fail == 0 ? 0 : 1;
}