// Standalone tests for reasampler::newGuids + GuidBaseline — no REAPER, no test // framework. Mirror of test_view_mode_model: iterate the hard logic outside the DAW. // // Covers (D2 Wave-2 new-content detection): // 1. newGuids: current \ previous, empty-GUID filtering, determinism. // 2. GuidBaseline first-poll guard: the first observe() after open reports NOTHING // new (pre-existing content stays Arrange) and establishes the baseline. // 3. Incremental detection: only GUIDs added since the prior observe() are returned. // 4. Deletion drops from the baseline so a reused GUID is re-detected. // 5. reset() (project switch) re-arms the first-poll guard: the next observe() // re-baselines and reports nothing new — never diffs across projects. #include "../src/guid_diff.h" #include #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) static bool has(const std::vector& v, const std::string& g) { for (const auto& e : v) if (e == g) return true; return false; } // -- 1. newGuids set difference ---------------------------------------------- static void testNewGuidsDifference() { std::set prev{"{A}", "{B}"}; std::set cur{"{A}", "{B}", "{C}", "{D}"}; auto added = newGuids(prev, cur); CHECK(added.size() == 2); CHECK(has(added, "{C}")); CHECK(has(added, "{D}")); CHECK(!has(added, "{A}")); // pre-existing, not new CHECK(!has(added, "{B}")); // No change ⇒ nothing new. CHECK(newGuids(cur, cur).empty()); // A removed GUID is not "new" (it is absent from current). std::set shrunk{"{A}"}; CHECK(newGuids(prev, shrunk).empty()); // Determinism: ascending set order. std::set p2; std::set c2{"{Z}", "{A}", "{M}"}; auto ordered = newGuids(p2, c2); CHECK(ordered.size() == 3); CHECK(ordered[0] == "{A}" && ordered[1] == "{M}" && ordered[2] == "{Z}"); } static void testNewGuidsIgnoresEmpty() { std::set prev{"{A}"}; std::set cur{"", "{A}", "{B}"}; // empty ⇒ a GUID-read failure auto added = newGuids(prev, cur); CHECK(added.size() == 1); CHECK(has(added, "{B}")); CHECK(!has(added, "")); // never tag an empty GUID } // -- 2. First-poll guard ----------------------------------------------------- static void testBaselineFirstPollReportsNothing() { GuidBaseline b; CHECK(!b.primed()); // First observe after open: pre-existing content must NOT be tagged. auto first = b.observe({"{A}", "{B}", "{C}"}); CHECK(first.empty()); // nothing new at open CHECK(b.primed()); } // -- 3. Incremental detection ------------------------------------------------ static void testBaselineIncremental() { GuidBaseline b; b.observe({"{A}", "{B}"}); // baseline auto t1 = b.observe({"{A}", "{B}", "{C}"}); CHECK(t1.size() == 1 && has(t1, "{C}")); // only the newly-added GUID // Next tick with a further addition — earlier-added {C} is now baseline. auto t2 = b.observe({"{A}", "{B}", "{C}", "{D}"}); CHECK(t2.size() == 1 && has(t2, "{D}")); CHECK(!has(t2, "{C}")); // A steady state reports nothing new. CHECK(b.observe({"{A}", "{B}", "{C}", "{D}"}).empty()); } // -- 4. Deletion drops from baseline; reused GUID re-detected ---------------- static void testBaselineDeletionReDetect() { GuidBaseline b; b.observe({"{A}", "{B}"}); // Delete {B}: not "new", and drops out of the baseline. CHECK(b.observe({"{A}"}).empty()); // {B} reappears (REAPER reused the GUID or the user re-added) ⇒ detected again. auto again = b.observe({"{A}", "{B}"}); CHECK(again.size() == 1 && has(again, "{B}")); } // -- 5. reset() re-arms the first-poll guard (project switch) ----------------- static void testResetReBaselines() { GuidBaseline b; b.observe({"{A}"}); // project 1 baseline b.observe({"{A}", "{B}"}); // {B} detected in project 1 b.reset(); CHECK(!b.primed()); // Switching to project 2: its pre-existing content must NOT be mass-tagged even // though those GUIDs were never seen before reset. auto afterSwitch = b.observe({"{X}", "{Y}", "{Z}"}); CHECK(afterSwitch.empty()); // re-baselined, nothing new CHECK(b.primed()); // Content created in project 2 after the switch IS detected. auto p2new = b.observe({"{X}", "{Y}", "{Z}", "{W}"}); CHECK(p2new.size() == 1 && has(p2new, "{W}")); } // -- 6. Reload-mis-tag regression: a project LOAD must re-baseline before the first // post-load observe, so the newly-loaded project's PRE-EXISTING content is never // reported as new. This locks the exact failure behind the reload-mis-tag bug: // the detector used to re-arm on a `proj != lastProject` pointer compare, which a // recycled ReaProject* address defeats; the previous project's stale baseline then // reported the whole just-loaded project as new content and it got mass-tagged into // the active mode. The fix routes the re-arm through persist's authoritative load // signal (bankPanelNotifyProjectLoaded -> reset()), modeled here as: on a load, // reset() runs BEFORE the first observe of the new project's set. // // The seam under test is GuidBaseline; the shell wiring (main.cpp notify -> // bank_panel reset()) is DAW-verified, but the load-then-observe DECISION lives // here and is what the bug got wrong. static void testReloadReBaselinesBeforeFirstObserve() { // Project A is open and settled: its content is the baseline, steady state reports // nothing new. This is the "extension already running against project A" precondition // the bug needs (a NON-empty stale baseline to mis-diff the next project against). GuidBaseline b; b.observe({"{A1}", "{A2}"}); // A baseline (first-poll guard) CHECK(b.observe({"{A1}", "{A2}"}).empty()); // steady: nothing new CHECK(b.primed()); // Daniel opens project B (saved in Design). B's pre-existing tracks are an ENTIRELY // different GUID set from A. persist raises its load signal; the fix calls reset() // (via bankPanelNotifyProjectLoaded) BEFORE the first post-load observe. b.reset(); auto afterLoad = b.observe({"{B1}", "{B2}", "{B3}"}); // The load must tag NOTHING: B's pre-existing content is the baseline, not "new". // Untagged/Arrange leaves stay Arrange; nothing is mass-tagged into Design. CHECK(afterLoad.empty()); // And genuine post-load creation in B is still detected (the fix must not deafen the // detector — only suppress the pre-existing set at the load boundary). auto createdInB = b.observe({"{B1}", "{B2}", "{B3}", "{B4}"}); CHECK(createdInB.size() == 1 && has(createdInB, "{B4}")); } // -- 6b. Negative control: WITHOUT the load re-baseline (the old pointer-miss path where // reset() never fired), the just-loaded project's pre-existing content IS reported // as new — i.e. it would be mass-tagged. This proves the assertion in test 6 is // load-bearing (the reset() is what prevents the mis-tag), not self-affirming. static void testMissingReBaselineWouldMisTag() { GuidBaseline b; b.observe({"{A1}", "{A2}"}); // A baseline b.observe({"{A1}", "{A2}"}); // settled against A // Simulate the BUG: no reset() on the load (the pointer compare missed a recycled // ReaProject*). The next observe diffs B's set against A's stale baseline. auto misdetected = b.observe({"{B1}", "{B2}", "{B3}"}); // Every one of B's pre-existing tracks looks "new" — exactly the mass-tag that // parked the Arrange tracks into Design on open. This is the failure the fix removes. CHECK(misdetected.size() == 3); CHECK(has(misdetected, "{B1}") && has(misdetected, "{B2}") && has(misdetected, "{B3}")); } int main() { testNewGuidsDifference(); testNewGuidsIgnoresEmpty(); testBaselineFirstPollReportsNothing(); testBaselineIncremental(); testBaselineDeletionReDetect(); testResetReBaselines(); testReloadReBaselinesBeforeFirstObserve(); testMissingReBaselineWouldMisTag(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }