One undo-block opener for the whole tree: fourteen sites route through it, and the reclaim span can no longer swallow a user's act

The drain refuses a tick inside a foreign block; applyMode wakes the reclaimer
before it opens the span.
This commit is contained in:
2026-08-07 16:38:11 -04:00
parent b2f6b3f1d0
commit b7b93a7ac3
25 changed files with 561 additions and 313 deletions
+89
View File
@@ -1433,6 +1433,93 @@ static void testTheDirectionHoldsThroughTheDrainsOwnMessagePump() {
CHECK(q.activity(host.clock, host.rolling) == FxReclaimActivity::Idle); // latch released
}
// -- 13. the sleep invariant ------------------------------------------------------------
// !pending() => !span.open(), at EVERY terminator. A span left open with the wakeup down
// has nothing left that could ever close it: the timer gates every tick on pending(), and
// the block then swallows the user's edits until a save, a state load, the next switch or
// unload. The check below is written as that implication and applied to each pure move
// that can put the loop to sleep, rather than falling out of one scenario.
static bool asleepWithTheSpanOpen(const FxReclaimQueue& q, const UndoSpan& span) {
return !q.pending() && span.open();
}
static void testNoTerminatorLeavesTheSpanOpenWithTheLoopAsleep() {
// FIRST, the predicate's own teeth: the violation is expressible, so a CHECK that it
// does not occur is not vacuously true.
{
FxReclaimQueue q;
UndoSpan span;
span.begin(kProjA, "ReaSampler: activate Design view", 9);
q.goIdle(); // the sleep WITHOUT the close — exactly the leak this test forbids
CHECK(asleepWithTheSpanOpen(q, span));
}
// 1. Every verdict a tick can come back with, swept whole. The shell closes on
// spanEndsAfter and sleeps on settled, in that order (fxReclaimTick); the property
// is that the second can never fire where the first did not.
for (int bits = 0; bits < 8; ++bits) {
ReclaimRunResult run;
run.settled = (bits & 1) != 0;
run.aborted = (bits & 2) != 0;
run.transportDeferred = (bits & 4) != 0;
FxReclaimQueue q;
UndoSpan span;
q.invalidate();
CHECK(q.beginTick(kProjA));
span.begin(kProjA, "ReaSampler: activate Design view", 9);
if (spanEndsAfter(run)) span.end();
if (!run.aborted && run.settled) q.goIdle();
CHECK(!asleepWithTheSpanOpen(q, span));
}
// 2. The state-load terminator. `abandon` is the one move that clears the wakeup with
// work still owed, which is what makes the close-FIRST ordering load-bearing rather
// than tidy — reverse the two and the loop is asleep under an open block.
{
FxReclaimQueue q;
UndoSpan span;
q.invalidate();
span.begin(kProjA, "ReaSampler: activate Design view", 9);
span.end(); // fxReclaimAbandonForStateLoad closes before it abandons
q.abandon();
CHECK(!q.pending());
CHECK(!asleepWithTheSpanOpen(q, span));
}
// 3. The project moving out from under the span. No active project sleeps the loop
// (beginTick's nullptr arm), a tab switch does not — but both close, because the
// span is keyed on its OWN project and neither one is it.
for (ReaProject* arrived : {static_cast<ReaProject*>(nullptr), kProjB}) {
FxReclaimQueue q;
UndoSpan span;
q.invalidate();
q.beginTick(kProjA);
span.begin(kProjA, "ReaSampler: activate Design view", 9);
if (span.project() != arrived) span.end();
q.beginTick(arrived);
CHECK(!span.open());
CHECK(!asleepWithTheSpanOpen(q, span));
}
// 4. The terminators with no queue move at all — a project save, an extension unload,
// the next real switch superseding. Each is the idempotent close alone, so the
// wakeup is whatever it already was; the implication has to hold either way.
for (bool woken : {false, true}) {
FxReclaimQueue q;
UndoSpan span;
if (woken) q.invalidate();
span.begin(kProjA, "ReaSampler: activate Design view", 9);
span.end();
CHECK(!asleepWithTheSpanOpen(q, span));
}
}
int main() {
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
@@ -1484,6 +1571,8 @@ int main() {
testARollingTransportHidesOfflineWorkFromTheDirectionButNeverOnline();
testTheDirectionHoldsThroughTheDrainsOwnMessagePump();
testNoTerminatorLeavesTheSpanOpenWithTheLoopAsleep();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}