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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user