// 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, not visible // in the incoming mode — covering both a parked leaf and a derived-invisible parent). #include "../src/core/view/solo_cache.h" #include 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 soloed = soloedTracks({{"{A}", 0}, {"{B}", 0}, {"{C}", 0}}); CHECK(soloed.empty()); } static void testSoloedTracksKeepsRawValueNotABoolean() { const std::map 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 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* 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* 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* 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 testRestorePlanReplaysEveryLiveVisibleEntryVerbatim() { const std::vector ops = planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {"{A}", "{B}"}); CHECK(ops.size() == 2); CHECK(ops[0] == (SoloOp{"{A}", 1})); CHECK(ops[1] == (SoloOp{"{B}", 2})); } static void testRestorePlanSkipsAGuidThatNoLongerExists() { const std::vector ops = planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {"{HERE}"}); CHECK(ops.size() == 1); CHECK(ops[0].guid == "{HERE}"); } static void testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode() { // 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 ops = planSoloRestore({{"{PARKED}", 1}, {"{VISIBLE}", 2}}, {"{PARKED}", "{VISIBLE}"}, {"{VISIBLE}"}); CHECK(ops.size() == 1); CHECK(ops[0] == (SoloOp{"{VISIBLE}", 2})); } static void testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode() { // The parent is never parked (parents are never parked — see planToggle), but a // derived-invisible parent is hidden all the same: replaying its cached solo // would silence the mix onto a track the user cannot see or unsolo. `visibleGuids` // (not the park plan) is the drop set precisely so this case is caught too. const std::vector ops = planSoloRestore({{"{HIDDEN_PARENT}", 1}}, {"{HIDDEN_PARENT}"}, {}); CHECK(ops.empty()); } static void testRestorePlanOfAnEmptyCacheWritesNothing() { CHECK(planSoloRestore({}, {"{A}"}, {}).empty()); } int main() { testUnsoloedProjectYieldsNothingToCache(); testSoloedTracksKeepsRawValueNotABoolean(); testSoloedTracksDropsEmptyGuids(); testStoreThenQueryReturnsTheStoredSet(); testStoringAnEmptySetRemovesTheModeEntry(); testStoreReplacesRatherThanMerges(); testStoreRejectsAnEmptyModeId(); testClearConsumesOnlyTheNamedMode(); testReconcileDropsDeadGuidsAcrossEveryMode(); testReconcileWithEveryGuidLiveRemovesNothing(); testRestorePlanReplaysEveryLiveVisibleEntryVerbatim(); testRestorePlanSkipsAGuidThatNoLongerExists(); testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode(); testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode(); testRestorePlanOfAnEmptyCacheWritesNothing(); if (g_fail == 0) std::printf("solo_cache: all tests passed\n"); return g_fail == 0 ? 0 : 1; }