Files
reasampler/tests/test_guid_diff.cpp
T
daniel 02b6350dc9 fix(bank_panel): drive new-content baseline re-arm from persist's load signal
The detector re-armed its GuidBaseline on a pointer compare (proj != lastProject),
a weaker signal than persist's GUID-primary identity. On a load onto a recycled
ReaProject* the baseline never reset, so the just-loaded project's pre-existing
tracks diffed against the previous project and were mass-tagged into the active
mode — opening a Design-saved project mis-tagged its Arrange tracks. Re-arm now
rides persist's authoritative load signal via bankPanelNotifyProjectLoaded().
2026-07-23 19:26:06 -04:00

195 lines
8.4 KiB
C++

// 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 <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)
static bool has(const std::vector<std::string>& 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<std::string> prev{"{A}", "{B}"};
std::set<std::string> 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<std::string> shrunk{"{A}"};
CHECK(newGuids(prev, shrunk).empty());
// Determinism: ascending set order.
std::set<std::string> p2;
std::set<std::string> 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<std::string> prev{"{A}"};
std::set<std::string> 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;
}