Merge pull request 'fix(graph): stabilize debouncer tests with synctest fake clock' (#741) from fix-flaky-debouncer-test into main
Release / release (push) Successful in 1m15s
schemas / vulnerabilities (push) Successful in 2m43s
schemas / check-release (push) Successful in 4m22s
schemas / check (push) Successful in 6m22s
pre-commit / pre-commit (push) Successful in 12m20s
schemas / build (push) Successful in 9m21s
schemas / deploy-prod (push) Successful in 1m13s

Reviewed-on: #741
This commit was merged in pull request #741.
This commit is contained in:
2026-02-25 12:49:10 +00:00
+40 -28
View File
@@ -3,6 +3,7 @@ package graph
import ( import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"testing/synctest"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -10,48 +11,59 @@ import (
) )
func TestDebouncer_Coalesces(t *testing.T) { func TestDebouncer_Coalesces(t *testing.T) {
d := NewDebouncer(50 * time.Millisecond) synctest.Test(t, func(t *testing.T) {
var calls atomic.Int32 d := NewDebouncer(50 * time.Millisecond)
var calls atomic.Int32
// Fire 10 rapid calls for the same key — only the last should execute. // Fire 10 rapid calls for the same key — only the last should execute.
for range 10 { for range 10 {
d.Debounce("key1", func() { d.Debounce("key1", func() {
calls.Add(1) calls.Add(1)
}) })
} }
// Wait for the debounce delay plus some margin. // Advance fake clock past the debounce delay and let goroutines settle.
time.Sleep(150 * time.Millisecond) time.Sleep(50 * time.Millisecond)
synctest.Wait()
assert.Equal(t, int32(1), calls.Load(), "rapid calls should coalesce into a single execution") assert.Equal(t, int32(1), calls.Load(), "rapid calls should coalesce into a single execution")
})
} }
func TestDebouncer_DifferentKeys(t *testing.T) { func TestDebouncer_DifferentKeys(t *testing.T) {
d := NewDebouncer(50 * time.Millisecond) synctest.Test(t, func(t *testing.T) {
var calls atomic.Int32 d := NewDebouncer(50 * time.Millisecond)
var calls atomic.Int32
d.Debounce("key-a", func() { calls.Add(1) }) d.Debounce("key-a", func() { calls.Add(1) })
d.Debounce("key-b", func() { calls.Add(1) }) d.Debounce("key-b", func() { calls.Add(1) })
d.Debounce("key-c", func() { calls.Add(1) }) d.Debounce("key-c", func() { calls.Add(1) })
time.Sleep(150 * time.Millisecond) time.Sleep(50 * time.Millisecond)
synctest.Wait()
assert.Equal(t, int32(3), calls.Load(), "different keys should fire independently") assert.Equal(t, int32(3), calls.Load(), "different keys should fire independently")
})
} }
func TestDebouncer_TimerReset(t *testing.T) { func TestDebouncer_TimerReset(t *testing.T) {
d := NewDebouncer(100 * time.Millisecond) synctest.Test(t, func(t *testing.T) {
var value atomic.Int32 d := NewDebouncer(100 * time.Millisecond)
var value atomic.Int32
// First call sets value to 1. // First call sets value to 1.
d.Debounce("key", func() { value.Store(1) }) d.Debounce("key", func() { value.Store(1) })
// Wait 60ms (less than the 100ms delay), then replace with value 2. // Advance 60ms (less than the 100ms delay) — first timer hasn't fired.
time.Sleep(60 * time.Millisecond) time.Sleep(60 * time.Millisecond)
d.Debounce("key", func() { value.Store(2) })
// At 60ms the first timer hasn't fired yet. Wait for the second timer. // Replace with value 2 — resets the timer to fire at 60+100 = 160ms.
time.Sleep(150 * time.Millisecond) d.Debounce("key", func() { value.Store(2) })
require.Equal(t, int32(2), value.Load(), "later call should replace the earlier one") // Advance another 100ms (total 160ms) to fire the reset timer.
time.Sleep(100 * time.Millisecond)
synctest.Wait()
require.Equal(t, int32(2), value.Load(), "later call should replace the earlier one")
})
} }