fix: layer GUID-primary project identity so reopened/new projects reload the bank
classifyProjectTransition now checks the stored GUID first, then the pointer, fixing the w10 regression where a recycled ReaProject* address stopped the bank reloading. poll()'s fork re-GUID gate is bound to !sameProjectObject. Full transition matrix pinned in tests.
This commit is contained in:
@@ -178,20 +178,47 @@ static void testRelocationPlanEmptyInputsNoOp() {
|
||||
CHECK(!deriveRelocationPlan("", "").needed);
|
||||
}
|
||||
|
||||
// --- Project-identity transition (W10 forked-project fix) -------------------
|
||||
// --- Project-identity transition (W12 combined identity fix) ----------------
|
||||
//
|
||||
// Signature: classifyProjectTransition(sameProjectObject, lastGuid, lastPath,
|
||||
// currentGuid, currentPath).
|
||||
// The first arg — did the SAME ReaProject* stay active across the two ticks —
|
||||
// is the primary signal; poll() computes it as `proj == lastProject_`.
|
||||
// Identity is layered GUID-PRIMARY: the stored GUID (identity of record) leads;
|
||||
// the pointer only disambiguates the same-GUID case. poll() computes
|
||||
// sameProjectObject as `proj == lastProject_`. This matrix covers every branch —
|
||||
// the classifier has regressed twice, so every case is pinned.
|
||||
|
||||
static void testTransitionRecycledPointerReopenDifferentProjectLoads() {
|
||||
// THE W12 REGRESSION. REAPER recycled the previous project's ReaProject* address
|
||||
// for a DIFFERENT reopened saved project, so sameProjectObject == true, but the
|
||||
// reopened project carries its OWN (different, non-empty) stored GUID. The old
|
||||
// pointer-primary classifier decided on path alone and returned NoOp (same path)
|
||||
// or SaveAsRelocate (new path) — the bank never reloaded. GUID-first makes this
|
||||
// a Load regardless of path.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidB", "/b/b.rpp")
|
||||
== ProjectTransition::Load);
|
||||
// Same recycled-address regression, but the reopened project happens to sit at
|
||||
// the SAME path as the one we left (old code returned NoOp here). Still a Load.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidB", "/a/a.rpp")
|
||||
== ProjectTransition::Load);
|
||||
}
|
||||
|
||||
static void testTransitionOpenNewUnsavedFromSavedLoads() {
|
||||
// Open a new/unsaved project from a saved one, recycled onto the same address
|
||||
// (sameProjectObject == true): currentGuid empty, lastGuid non-empty -> the
|
||||
// record identity differs -> Load (so the bank clears to the new empty project).
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "", "")
|
||||
== ProjectTransition::Load);
|
||||
}
|
||||
|
||||
static void testTransitionForkTabSwitchLoadsNeverRelocates() {
|
||||
// THE W10 BUG. proj2 and proj3 are forked siblings (Save-As copied the .rpp
|
||||
// incl. our GUID), so BOTH carry the same non-empty GUID on disk but sit at
|
||||
// different paths. Tab-switching between them is a DIFFERENT project object
|
||||
// (sameProjectObject == false). The old GUID-only classifier read this as a
|
||||
// Save-As and clobbered a bank; with the pointer back it is a Load, so neither
|
||||
// bank is ever relocated. This is the regression made provable outside the DAW.
|
||||
// THE W10 CASE — must stay fixed. proj2 and proj3 are forked siblings (Save-As
|
||||
// copied the .rpp incl. our GUID), so BOTH carry the same non-empty GUID on disk
|
||||
// but sit at different paths. Tab-switching between them is a DIFFERENT project
|
||||
// object (sameProjectObject == false). Same GUID -> step 1 falls through; step 2
|
||||
// (!sameProjectObject) -> Load, so neither bank is ever relocated.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"guidShared", "/proj2/p.rpp",
|
||||
"guidShared", "/proj3/p.rpp")
|
||||
@@ -204,26 +231,24 @@ static void testTransitionForkTabSwitchLoadsNeverRelocates() {
|
||||
}
|
||||
|
||||
static void testTransitionGenuineSaveAsRelocates() {
|
||||
// The SAME project object (pointer unchanged) saved to a new .rpp location ->
|
||||
// the one case that legitimately relocates the bank. Same GUID, new path.
|
||||
// The SAME project object (pointer unchanged) AND same GUID saved to a new .rpp
|
||||
// location -> the one case that legitimately relocates the bank (step 3).
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidA", "/b/b.rpp")
|
||||
== ProjectTransition::SaveAsRelocate);
|
||||
}
|
||||
|
||||
static void testTransitionPointerReuseDifferentGuidLoads() {
|
||||
// Pointer *reuse* for a genuinely different project: at THIS tick the object
|
||||
// differs (sameProjectObject == false) and its GUID differs too -> Load. Never
|
||||
// a relocate. This is the case the M4 GUID was originally added to handle;
|
||||
// the pointer check subsumes it (different object) and the GUID corroborates.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"guidA", "/a/a.rpp", "guidB", "/b/b.rpp")
|
||||
== ProjectTransition::Load);
|
||||
static void testTransitionReopenSameProjectRecycledSameAddrIsNoOp() {
|
||||
// Reopen the SAME project, recycled onto the same address: same object, same
|
||||
// (non-empty) GUID, same path -> nothing changed -> NoOp (step 4).
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidA", "/a/a.rpp")
|
||||
== ProjectTransition::NoOp);
|
||||
}
|
||||
|
||||
static void testTransitionTabSwitchDistinctProjectsLoads() {
|
||||
// Ordinary tab-switch between two distinct (non-forked) saved projects:
|
||||
// different object, different GUID -> Load, regardless of paths.
|
||||
static void testTransitionTwoDistinctSavedProjectsDistinctPointersLoad() {
|
||||
// Ordinary tab-switch between two distinct (non-forked) saved projects: distinct
|
||||
// pointers, different GUIDs -> step 1 (GUID differs) -> Load, regardless of paths.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"guidA", "/a/a.rpp", "guidB", "/b/b.rpp")
|
||||
== ProjectTransition::Load);
|
||||
@@ -232,39 +257,40 @@ static void testTransitionTabSwitchDistinctProjectsLoads() {
|
||||
== ProjectTransition::Load);
|
||||
}
|
||||
|
||||
static void testTransitionSaveInPlaceIsNoOp() {
|
||||
// Same object, same path (idle tick, or a Save that did not move the .rpp) ->
|
||||
// nothing to do.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidA", "/a/a.rpp")
|
||||
== ProjectTransition::NoOp);
|
||||
// Empty GUID (unsaved project sitting idle), same (empty) path -> NoOp too.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true, "", "", "", "")
|
||||
== ProjectTransition::NoOp);
|
||||
static void testTransitionTwoUnsavedProjectsSwitchLoads() {
|
||||
// Switch between two unsaved projects: both GUIDs empty (step 1 falls through:
|
||||
// equal), distinct objects -> step 2 (!sameProjectObject) -> Load. Installs the
|
||||
// right in-memory (empty) state for whichever unsaved project is now active.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"", "", "", "")
|
||||
== ProjectTransition::Load);
|
||||
// Distinct unsaved objects may even report distinct (untitled) paths -> Load.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"", "/untitled1", "", "/untitled2")
|
||||
== ProjectTransition::Load);
|
||||
}
|
||||
|
||||
static void testTransitionFirstSaveSameObjectRelocatesButPlanNoOps() {
|
||||
// Same object, empty GUID, path appears (first save of an untitled project).
|
||||
// The classifier says SaveAsRelocate, but the empty-GUID safety is preserved
|
||||
// at execution: the old project dir is empty, so deriveRelocationPlan makes
|
||||
// `needed` false and NOTHING is physically relocated; poll() just mints a GUID.
|
||||
static void testTransitionFirstSaveOfUnsavedRelocatesButPlanNoOps() {
|
||||
// First save of an unsaved project: same object, both GUIDs empty (step 1 & 2
|
||||
// fall through), path appears (step 3) -> SaveAsRelocate. The empty-GUID safety
|
||||
// is preserved at execution: the old project dir is empty, so deriveRelocation-
|
||||
// Plan makes `needed` false and NOTHING is physically relocated; poll() mints a
|
||||
// GUID.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"", "", "", "/a/a.rpp")
|
||||
== ProjectTransition::SaveAsRelocate);
|
||||
// Prove the safety end-to-end: the relocation plan for an empty old dir no-ops.
|
||||
CHECK(!deriveRelocationPlan(/*oldProjectDir=*/"", "/a").needed);
|
||||
CHECK(deriveRelocationPlan(/*oldProjectDir=*/"", "/a").needed == false);
|
||||
}
|
||||
|
||||
static void testTransitionSameObjectSharedGuidStillLoadsWhenObjectDiffers() {
|
||||
// Divergence guard's precondition, from the pure side: even if a forked sibling
|
||||
// still shares our GUID, as long as the OBJECT differs the verdict is Load
|
||||
// (never Save-As). The actual re-GUID that makes the siblings diverge is a
|
||||
// REAPER-facing action in poll() (SetProjExtState + MarkProjectDirty) and is
|
||||
// covered by the DAW procedure; here we lock the pure verdict that gates it.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/false,
|
||||
"guidShared", "/proj2/p.rpp",
|
||||
"guidShared", "/proj3/p.rpp")
|
||||
== ProjectTransition::Load);
|
||||
static void testTransitionInPlaceSaveIsNoOp() {
|
||||
// In-place save (or an idle tick): same object, same GUID, same path -> NoOp.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true,
|
||||
"guidA", "/a/a.rpp", "guidA", "/a/a.rpp")
|
||||
== ProjectTransition::NoOp);
|
||||
// Idle unsaved project (same object, both empty GUID, same empty path) -> NoOp.
|
||||
CHECK(classifyProjectTransition(/*sameProjectObject=*/true, "", "", "", "")
|
||||
== ProjectTransition::NoOp);
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -283,13 +309,15 @@ int main() {
|
||||
testRelocationPlanForSaveAs();
|
||||
testRelocationPlanNotNeededForSaveInPlace();
|
||||
testRelocationPlanEmptyInputsNoOp();
|
||||
testTransitionRecycledPointerReopenDifferentProjectLoads();
|
||||
testTransitionOpenNewUnsavedFromSavedLoads();
|
||||
testTransitionForkTabSwitchLoadsNeverRelocates();
|
||||
testTransitionGenuineSaveAsRelocates();
|
||||
testTransitionPointerReuseDifferentGuidLoads();
|
||||
testTransitionTabSwitchDistinctProjectsLoads();
|
||||
testTransitionSaveInPlaceIsNoOp();
|
||||
testTransitionFirstSaveSameObjectRelocatesButPlanNoOps();
|
||||
testTransitionSameObjectSharedGuidStillLoadsWhenObjectDiffers();
|
||||
testTransitionReopenSameProjectRecycledSameAddrIsNoOp();
|
||||
testTransitionTwoDistinctSavedProjectsDistinctPointersLoad();
|
||||
testTransitionTwoUnsavedProjectsSwitchLoads();
|
||||
testTransitionFirstSaveOfUnsavedRelocatesButPlanNoOps();
|
||||
testTransitionInPlaceSaveIsNoOp();
|
||||
|
||||
if (g_fail == 0) std::printf("capture_paths: all tests passed\n");
|
||||
else std::printf("capture_paths: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user