From 7f98a8b7ca7e915f84e0905128ba97cd9bc9cf2d Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Mon, 25 Aug 2025 15:25:50 +0800 Subject: [PATCH] utils: use synctest to make the timer tests fully deterministic (#5306) * utils: use synctest to make the timer tests fully deterministic * Go 1.24 workaround --- internal/utils/timer_test.go | 178 ++++++++++++++++------------------- 1 file changed, 83 insertions(+), 95 deletions(-) diff --git a/internal/utils/timer_test.go b/internal/utils/timer_test.go index 4d0742cc3..1a17cab15 100644 --- a/internal/utils/timer_test.go +++ b/internal/utils/timer_test.go @@ -4,97 +4,83 @@ import ( "testing" "time" + "github.com/quic-go/quic-go/internal/synctest" + "github.com/stretchr/testify/require" ) -const testDuration = 10 * time.Millisecond +func TestTimerResets(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + timer := NewTimer() -func TestTimerCreateAndReset(t *testing.T) { - timer := NewTimer() - select { - case <-timer.Chan(): - t.Fatal("timer should not have fired") - default: - } - - deadline := time.Now().Add(testDuration) - timer.Reset(deadline) - require.Equal(t, deadline, timer.Deadline()) - - select { - case <-timer.Chan(): - case <-time.After(2 * testDuration): - t.Fatal("timer should have fired") - } - - timer.SetRead() - timer.Reset(time.Now().Add(testDuration)) - - select { - case <-timer.Chan(): - case <-time.After(2 * testDuration): - t.Fatal("timer should have fired") - } -} - -func TestTimerMultipleResets(t *testing.T) { - timer := NewTimer() - for i := 0; i < 10; i++ { - timer.Reset(time.Now().Add(testDuration)) - if i%2 == 0 { - select { - case <-timer.Chan(): - case <-time.After(2 * testDuration): - t.Fatal("timer should have fired") - } - timer.SetRead() - } else { - time.Sleep(testDuration * 2) + select { + case <-timer.Chan(): + t.Fatal("timer should not have fired") + default: } - } - select { - case <-timer.Chan(): - case <-time.After(2 * testDuration): - t.Fatal("timer should have fired") - } + start := time.Now() + + // timer fires immediately for a deadline in the past + timer.Reset(time.Now().Add(-time.Second)) + select { + case <-timer.Chan(): + require.Zero(t, time.Since(start)) + timer.SetRead() + case <-time.After(time.Hour): // this can be replaced with a default once we drop support for Go 1.24 + t.Fatal("timer should have fired") + } + + // timer reset without getting read + for range 10 { + time.Sleep(time.Second) + timer.Reset(time.Now().Add(time.Hour)) + } + select { + case <-timer.Chan(): + require.Equal(t, time.Since(start), time.Hour+10*time.Second) + timer.SetRead() + case <-time.After(2 * time.Hour): + t.Fatal("timer should have fired") + } + + const d = 10 * time.Minute + for i := range 10 { + start := time.Now() + timer.Reset(time.Now().Add(d)) + if i%2 == 0 { + select { + case <-timer.Chan(): + require.Equal(t, time.Since(start), d) + case <-time.After(2 * d): + t.Fatal("timer should have fired") + } + timer.SetRead() + } else { + time.Sleep(2 * d) + } + } + + select { + case <-timer.Chan(): + default: + t.Fatal("timer should have fired") + } + }) } -func TestTimerResetWithoutExpiration(t *testing.T) { - timer := NewTimer() - for i := 0; i < 10; i++ { - timer.Reset(time.Now().Add(time.Hour)) - } - timer.Reset(time.Now().Add(testDuration)) +func TestTimerClearDeadline(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + timer := NewTimer() + timer.Reset(time.Time{}) - select { - case <-timer.Chan(): - case <-time.After(2 * testDuration): - t.Fatal("timer should have fired") - } -} - -func TestTimerPastDeadline(t *testing.T) { - timer := NewTimer() - timer.Reset(time.Now().Add(-time.Second)) - - select { - case <-timer.Chan(): - case <-time.After(testDuration): - t.Fatal("timer should have fired immediately") - } -} - -func TestTimerZeroDeadline(t *testing.T) { - timer := NewTimer() - timer.Reset(time.Time{}) - - // we don't expect the timer to be set for a zero deadline - select { - case <-timer.Chan(): - t.Fatal("timer should not have fired") - case <-time.After(testDuration): - } + // we don't expect the timer to be set for a zero deadline + select { + case <-timer.Chan(): + t.Fatal("timer should not have fired") + case <-time.After(time.Hour): + } + }) } func TestTimerSameDeadline(t *testing.T) { @@ -105,7 +91,7 @@ func TestTimerSameDeadline(t *testing.T) { select { case <-timer.Chan(): - case <-time.After(testDuration): + default: t.Fatal("timer should have fired") } @@ -114,7 +100,7 @@ func TestTimerSameDeadline(t *testing.T) { select { case <-timer.Chan(): - case <-time.After(testDuration): + default: t.Fatal("timer should have fired") } }) @@ -126,26 +112,28 @@ func TestTimerSameDeadline(t *testing.T) { select { case <-timer.Chan(): - case <-time.After(testDuration): + default: t.Fatal("timer should have fired") } select { case <-timer.Chan(): t.Fatal("timer should not have fired again") - case <-time.After(testDuration): + default: } }) } -func TestTimerStopping(t *testing.T) { - timer := NewTimer() - timer.Reset(time.Now().Add(testDuration)) - timer.Stop() +func TestTimerStop(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + timer := NewTimer() + timer.Reset(time.Now().Add(time.Second)) + timer.Stop() - select { - case <-timer.Chan(): - t.Fatal("timer should not have fired") - case <-time.After(2 * testDuration): - } + select { + case <-timer.Chan(): + t.Fatal("timer should not have fired") + case <-time.After(time.Hour): + } + }) }