commit a1c161ec0688e1daeb6ca764267f2c2f7098c33c
parent fd641654d80bffe4a48f2cafe9cc554340113732
Author: Michael Camilleri <[email protected]>
Date: Sun, 17 May 2026 06:05:42 +0900
Fix test helper for debounce-coalesce tests
ManualDebounceSleep.releaseAll() was a one-shot gate: it resumed only the
continuations registered at the instant it was called, then cleared the array.
Any sleep() that parked afterward waited on a continuation nothing would ever
resume again. The debounce-coalesce tests spawn the live (non-cancelled)
debounce task in the final loop iteration with no suspension point before
releaseAll(), and Task creation only enqueues — the task registers its
continuation when it later starts running, not synchronously.
releaseAll() is now sticky: it sets a released flag so a sleep() reaching
the gate afterward resumes immediately instead of parking. The release no
longer depends on every debounce task having been scheduled first, so
both suites are deterministic regardless of task-scheduling order.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
1 file changed, 14 insertions(+), 0 deletions(-)
diff --git a/Tests/Support/TestHelpers.swift b/Tests/Support/TestHelpers.swift
@@ -40,15 +40,28 @@ func makeTestStore(
/// wall-clock timing — Task.sleep on a contended simulator can wake before
/// the next actor hop lands, racing the cancellation that should have
/// suppressed the prior debounce task.
+///
+/// `releaseAll()` is sticky: it opens the gate permanently, so a debounce
+/// task that only reaches its `sleep()` *after* the test called
+/// `releaseAll()` resumes immediately instead of parking forever. Without
+/// this, the live (non-cancelled) debounce task — spawned with no
+/// suspension point before `releaseAll()` — could miss the one-shot
+/// release and strand the flush, failing the test with `count → 0`.
final class ManualDebounceSleep: @unchecked Sendable {
private let lock = NSLock()
private var continuations: [CheckedContinuation<Void, Never>] = []
+ private var released = false
var sleepFn: @Sendable (Duration) async throws -> Void {
{ @Sendable [weak self] _ in
await withCheckedContinuation { cont in
guard let self else { cont.resume(); return }
self.lock.lock()
+ if self.released {
+ self.lock.unlock()
+ cont.resume()
+ return
+ }
self.continuations.append(cont)
self.lock.unlock()
}
@@ -57,6 +70,7 @@ final class ManualDebounceSleep: @unchecked Sendable {
func releaseAll() {
lock.lock()
+ released = true
let toRelease = continuations
continuations.removeAll()
lock.unlock()