diff --git a/pkg/services/alerting/ticker_test.go b/pkg/services/alerting/ticker_test.go index bb679750a51..0db3c6b777f 100644 --- a/pkg/services/alerting/ticker_test.go +++ b/pkg/services/alerting/ticker_test.go @@ -1,121 +1,119 @@ package alerting -// import ( -// "testing" -// "time" -// -// "github.com/benbjohnson/clock" -// ) -// -// func inspectTick(tick time.Time, last time.Time, offset time.Duration, t *testing.T) { -// if !tick.Equal(last.Add(time.Duration(1) * time.Second)) { -// t.Fatalf("expected a tick 1 second more than prev, %s. got: %s", last, tick) -// } -// } -// -// returns the new last tick seen -// func assertAdvanceUntil(ticker *Ticker, last, desiredLast time.Time, offset, wait time.Duration, t *testing.T) time.Time { -// for { -// select { -// case tick := <-ticker.C: -// inspectTick(tick, last, offset, t) -// last = tick -// case <-time.NewTimer(wait).C: -// if last.Before(desiredLast) { -// t.Fatalf("waited %s for ticker to advance to %s, but only went up to %s", wait, desiredLast, last) -// } -// if last.After(desiredLast) { -// t.Fatalf("timer advanced too far. should only have gone up to %s, but it went up to %s", desiredLast, last) -// } -// return last -// } -// } -// } -// -// func assertNoAdvance(ticker *Ticker, desiredLast time.Time, wait time.Duration, t *testing.T) { -// for { -// select { -// case tick := <-ticker.C: -// t.Fatalf("timer should have stayed at %s, instead it advanced to %s", desiredLast, tick) -// case <-time.NewTimer(wait).C: -// return -// } -// } -// } -// -// func TestTickerRetro1Hour(t *testing.T) { -// offset := time.Duration(10) * time.Second -// last := time.Unix(0, 0) -// mock := clock.NewMock() -// mock.Add(time.Duration(1) * time.Hour) -// desiredLast := mock.Now().Add(-offset) -// ticker := NewTicker(last, offset, mock) -// -// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t) -// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t) -// -// } -// -// func TestAdvanceWithUpdateOffset(t *testing.T) { -// offset := time.Duration(10) * time.Second -// last := time.Unix(0, 0) -// mock := clock.NewMock() -// mock.Add(time.Duration(1) * time.Hour) -// desiredLast := mock.Now().Add(-offset) -// ticker := NewTicker(last, offset, mock) -// -// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t) -// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t) -// -// // lowering offset should see a few more ticks -// offset = time.Duration(5) * time.Second -// ticker.updateOffset(offset) -// desiredLast = mock.Now().Add(-offset) -// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(9)*time.Millisecond, t) -// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t) -// -// // advancing clock should see even more ticks -// mock.Add(time.Duration(1) * time.Hour) -// desiredLast = mock.Now().Add(-offset) -// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(8)*time.Millisecond, t) -// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t) -// -// } -// -// func getCase(lastSeconds, offsetSeconds int) (time.Time, time.Duration) { -// last := time.Unix(int64(lastSeconds), 0) -// offset := time.Duration(offsetSeconds) * time.Second -// return last, offset -// } -// -// func TestTickerNoAdvance(t *testing.T) { -// -// // it's 00:01:00 now. what are some cases where we don't want the ticker to advance? -// mock := clock.NewMock() -// mock.Add(time.Duration(60) * time.Second) -// -// type Case struct { -// last int -// offset int -// } -// -// // note that some cases add up to now, others go into the future -// cases := []Case{ -// {50, 10}, -// {50, 30}, -// {59, 1}, -// {59, 10}, -// {59, 30}, -// {60, 1}, -// {60, 10}, -// {60, 30}, -// {90, 1}, -// {90, 10}, -// {90, 30}, -// } -// for _, c := range cases { -// last, offset := getCase(c.last, c.offset) -// ticker := NewTicker(last, offset, mock) -// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t) -// } -// } +import ( + "context" + "fmt" + "math/rand" + "sync" + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/stretchr/testify/require" +) + +func TestTicker(t *testing.T) { + readChanOrFail := func(t *testing.T, ctx context.Context, c chan time.Time) time.Time { + t.Helper() + select { + case tick := <-c: + return tick + case <-ctx.Done(): + require.Failf(t, fmt.Sprintf("%v", ctx.Err()), "timeout reading the channel") + default: + require.Failf(t, "channel is empty but it should have a tick", "") + } + return time.Time{} + } + t.Run("should not drop ticks", func(t *testing.T) { + clk := clock.NewMock() + intervalSec := rand.Int63n(100) + 10 + interval := time.Duration(intervalSec) * time.Second + last := clk.Now() + ticker := NewTicker(last, 0, clk, intervalSec) + + ticks := rand.Intn(9) + 1 + jitter := rand.Int63n(int64(interval) - 1) + + clk.Add(time.Duration(ticks)*interval + time.Duration(jitter)) + + w := sync.WaitGroup{} + w.Add(1) + regTicks := make([]time.Time, 0, ticks) + go func() { + for timestamp := range ticker.C { + regTicks = append(regTicks, timestamp) + if len(regTicks) == ticks { + w.Done() + } + } + }() + w.Wait() + + require.Len(t, regTicks, ticks) + + t.Run("ticks should monotonically increase", func(t *testing.T) { + for i := 1; i < len(regTicks); i++ { + previous := regTicks[i-1] + current := regTicks[i] + require.Equal(t, interval, current.Sub(previous)) + } + }) + }) + + t.Run("should not put anything to channel until it's time", func(t *testing.T) { + clk := clock.NewMock() + intervalSec := rand.Int63n(9) + 1 + interval := time.Duration(intervalSec) * time.Second + last := clk.Now() + ticker := NewTicker(last, 0, clk, intervalSec) + expectedTick := clk.Now().Add(interval) + for { + require.Empty(t, ticker.C) + clk.Add(time.Duration(rand.Int31n(500)+100) * time.Millisecond) + if clk.Now().After(expectedTick) { + break + } + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(func() { + cancel() + }) + actual := readChanOrFail(t, ctx, ticker.C) + require.Equal(t, expectedTick, actual) + }) + + t.Run("should put the tick in the channel immediately if it is behind", func(t *testing.T) { + clk := clock.NewMock() + intervalSec := rand.Int63n(9) + 1 + interval := time.Duration(intervalSec) * time.Second + last := clk.Now() + ticker := NewTicker(last, 0, clk, intervalSec) + + // We can expect the first tick to be at a consistent interval. Take a snapshot of the clock now, before we advance it. + expectedTick := clk.Now().Add(interval) + + require.Empty(t, ticker.C) + + clk.Add(interval) // advance the clock by the interval to make the ticker tick the first time. + clk.Add(interval) // advance the clock by the interval to make the ticker tick the second time. + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(func() { + cancel() + }) + + // Irregardless of wall time, the first tick should be initial clock + interval. + actual1 := readChanOrFail(t, ctx, ticker.C) + require.Equal(t, expectedTick, actual1) + + var actual2 time.Time + require.Eventually(t, func() bool { + actual2 = readChanOrFail(t, ctx, ticker.C) + return true + }, time.Second, 10*time.Millisecond) + + // Similarly, the second tick should be last tick + interval irregardless of wall time. + require.Equal(t, expectedTick.Add(interval), actual2) + }) +}