From cc33c651cd16a97afdd03dcc1414cee7c00a05c2 Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Wed, 25 Feb 2026 13:25:18 +0100 Subject: [PATCH] fix(graph): stabilize debouncer tests with synctest fake clock Replace real time.Sleep waits with testing/synctest fake clock to eliminate CI flakiness caused by timer races on loaded machines. Co-Authored-By: Claude Opus 4.6 --- graph/debounce_test.go | 68 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/graph/debounce_test.go b/graph/debounce_test.go index 89b8187..3fefafd 100644 --- a/graph/debounce_test.go +++ b/graph/debounce_test.go @@ -3,6 +3,7 @@ package graph import ( "sync/atomic" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/assert" @@ -10,48 +11,59 @@ import ( ) func TestDebouncer_Coalesces(t *testing.T) { - d := NewDebouncer(50 * time.Millisecond) - var calls atomic.Int32 + synctest.Test(t, func(t *testing.T) { + d := NewDebouncer(50 * time.Millisecond) + var calls atomic.Int32 - // Fire 10 rapid calls for the same key — only the last should execute. - for range 10 { - d.Debounce("key1", func() { - calls.Add(1) - }) - } + // Fire 10 rapid calls for the same key — only the last should execute. + for range 10 { + d.Debounce("key1", func() { + calls.Add(1) + }) + } - // Wait for the debounce delay plus some margin. - time.Sleep(150 * time.Millisecond) + // Advance fake clock past the debounce delay and let goroutines settle. + 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) { - d := NewDebouncer(50 * time.Millisecond) - var calls atomic.Int32 + synctest.Test(t, func(t *testing.T) { + d := NewDebouncer(50 * time.Millisecond) + var calls atomic.Int32 - d.Debounce("key-a", func() { calls.Add(1) }) - d.Debounce("key-b", func() { calls.Add(1) }) - d.Debounce("key-c", func() { calls.Add(1) }) + d.Debounce("key-a", func() { calls.Add(1) }) + d.Debounce("key-b", 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) { - d := NewDebouncer(100 * time.Millisecond) - var value atomic.Int32 + synctest.Test(t, func(t *testing.T) { + d := NewDebouncer(100 * time.Millisecond) + var value atomic.Int32 - // First call sets value to 1. - d.Debounce("key", func() { value.Store(1) }) + // First call sets value to 1. + d.Debounce("key", func() { value.Store(1) }) - // Wait 60ms (less than the 100ms delay), then replace with value 2. - time.Sleep(60 * time.Millisecond) - d.Debounce("key", func() { value.Store(2) }) + // Advance 60ms (less than the 100ms delay) — first timer hasn't fired. + time.Sleep(60 * time.Millisecond) - // At 60ms the first timer hasn't fired yet. Wait for the second timer. - time.Sleep(150 * time.Millisecond) + // Replace with value 2 — resets the timer to fire at 60+100 = 160ms. + 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") + }) }