package agent import ( "fmt" "strings" "testing" "time" "github.com/benbjohnson/clock" "github.com/stretchr/testify/require" ) func TestAlignedTicker(t *testing.T) { interval := 10 * time.Second jitter := 0 * time.Second offset := 0 * time.Second clk := clock.NewMock() since := clk.Now() until := since.Add(60 * time.Second) ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() expected := []time.Time{ time.Unix(10, 0).UTC(), time.Unix(20, 0).UTC(), time.Unix(30, 0).UTC(), time.Unix(40, 0).UTC(), time.Unix(50, 0).UTC(), time.Unix(60, 0).UTC(), } actual := []time.Time{} clk.Add(10 * time.Second) for !clk.Now().After(until) { select { case tm := <-ticker.Elapsed(): actual = append(actual, tm.UTC()) } clk.Add(10 * time.Second) } require.Equal(t, expected, actual) } func TestAlignedTickerJitter(t *testing.T) { interval := 10 * time.Second jitter := 5 * time.Second offset := 0 * time.Second clk := clock.NewMock() since := clk.Now() until := since.Add(61 * time.Second) ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() last := since for !clk.Now().After(until) { select { case tm := <-ticker.Elapsed(): dur := tm.Sub(last) // 10s interval + 5s jitter + up to 1s late firing. require.True(t, dur <= 16*time.Second, "expected elapsed time to be less than 16 seconds, but was %s", dur) require.True(t, dur >= 5*time.Second, "expected elapsed time to be more than 5 seconds, but was %s", dur) last = last.Add(interval) default: } clk.Add(1 * time.Second) } } func TestAlignedTickerOffset(t *testing.T) { interval := 10 * time.Second jitter := 0 * time.Second offset := 3 * time.Second clk := clock.NewMock() since := clk.Now() until := since.Add(61 * time.Second) ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() expected := []time.Time{ time.Unix(13, 0).UTC(), time.Unix(23, 0).UTC(), time.Unix(33, 0).UTC(), time.Unix(43, 0).UTC(), time.Unix(53, 0).UTC(), } actual := []time.Time{} clk.Add(10*time.Second + offset) for !clk.Now().After(until) { tm := <-ticker.Elapsed() actual = append(actual, tm.UTC()) clk.Add(10 * time.Second) } require.Equal(t, expected, actual) } func TestAlignedTickerMissedTick(t *testing.T) { interval := 10 * time.Second jitter := 0 * time.Second offset := 0 * time.Second clk := clock.NewMock() since := clk.Now() ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() clk.Add(25 * time.Second) tm := <-ticker.Elapsed() require.Equal(t, time.Unix(10, 0).UTC(), tm.UTC()) clk.Add(5 * time.Second) tm = <-ticker.Elapsed() require.Equal(t, time.Unix(30, 0).UTC(), tm.UTC()) } func TestUnalignedTicker(t *testing.T) { interval := 10 * time.Second jitter := 0 * time.Second offset := 0 * time.Second clk := clock.NewMock() clk.Add(1 * time.Second) since := clk.Now() until := since.Add(60 * time.Second) ticker := &UnalignedTicker{ interval: interval, jitter: jitter, offset: offset, } ticker.start(clk) defer ticker.Stop() expected := []time.Time{ time.Unix(1, 0).UTC(), time.Unix(11, 0).UTC(), time.Unix(21, 0).UTC(), time.Unix(31, 0).UTC(), time.Unix(41, 0).UTC(), time.Unix(51, 0).UTC(), time.Unix(61, 0).UTC(), } actual := []time.Time{} for !clk.Now().After(until) { select { case tm := <-ticker.Elapsed(): actual = append(actual, tm.UTC()) default: } clk.Add(10 * time.Second) } require.Equal(t, expected, actual) } func TestRollingTicker(t *testing.T) { interval := 10 * time.Second jitter := 0 * time.Second offset := 0 * time.Second clk := clock.NewMock() clk.Add(1 * time.Second) since := clk.Now() until := since.Add(60 * time.Second) ticker := &UnalignedTicker{ interval: interval, jitter: jitter, offset: offset, } ticker.start(clk) defer ticker.Stop() expected := []time.Time{ time.Unix(1, 0).UTC(), time.Unix(11, 0).UTC(), time.Unix(21, 0).UTC(), time.Unix(31, 0).UTC(), time.Unix(41, 0).UTC(), time.Unix(51, 0).UTC(), time.Unix(61, 0).UTC(), } actual := []time.Time{} for !clk.Now().After(until) { select { case tm := <-ticker.Elapsed(): actual = append(actual, tm.UTC()) default: } clk.Add(10 * time.Second) } require.Equal(t, expected, actual) } // Simulates running the Ticker for an hour and displays stats about the // operation. func TestAlignedTickerDistribution(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } interval := 10 * time.Second jitter := 5 * time.Second offset := 0 * time.Second clk := clock.NewMock() since := clk.Now() ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() dist := simulatedDist(ticker, clk) printDist(dist) require.True(t, 350 < dist.Count) require.True(t, 9 < dist.Mean() && dist.Mean() < 11) } func TestAlignedTickerDistributionWithOffset(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } interval := 10 * time.Second jitter := 5 * time.Second offset := 3 * time.Second clk := clock.NewMock() since := clk.Now() ticker := &AlignedTicker{ interval: interval, jitter: jitter, offset: offset, minInterval: interval / 100, } ticker.start(since, clk) defer ticker.Stop() dist := simulatedDist(ticker, clk) printDist(dist) require.True(t, 350 < dist.Count) require.True(t, 9 < dist.Mean() && dist.Mean() < 11) } // Simulates running the Ticker for an hour and displays stats about the // operation. func TestUnalignedTickerDistribution(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } interval := 10 * time.Second jitter := 5 * time.Second offset := 0 * time.Second clk := clock.NewMock() ticker := &UnalignedTicker{ interval: interval, jitter: jitter, offset: offset, } ticker.start(clk) defer ticker.Stop() dist := simulatedDist(ticker, clk) printDist(dist) require.True(t, 350 < dist.Count) require.True(t, 9 < dist.Mean() && dist.Mean() < 11) } func TestUnalignedTickerDistributionWithOffset(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } interval := 10 * time.Second jitter := 5 * time.Second offset := 3 * time.Second clk := clock.NewMock() ticker := &UnalignedTicker{ interval: interval, jitter: jitter, offset: offset, } ticker.start(clk) defer ticker.Stop() dist := simulatedDist(ticker, clk) printDist(dist) require.True(t, 350 < dist.Count) require.True(t, 9 < dist.Mean() && dist.Mean() < 11) } // Simulates running the Ticker for an hour and displays stats about the // operation. func TestRollingTickerDistribution(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } interval := 10 * time.Second jitter := 5 * time.Second clk := clock.NewMock() ticker := &RollingTicker{ interval: interval, jitter: jitter, } ticker.start(clk) defer ticker.Stop() dist := simulatedDist(ticker, clk) printDist(dist) require.True(t, 275 < dist.Count) require.True(t, 12 < dist.Mean() && 13 > dist.Mean()) } type Distribution struct { Buckets [60]int Count int Waittime float64 } func (d *Distribution) Mean() float64 { return d.Waittime / float64(d.Count) } func printDist(dist Distribution) { for i, count := range dist.Buckets { fmt.Printf("%2d %s\n", i, strings.Repeat("x", count)) } fmt.Printf("Average interval: %f\n", dist.Mean()) fmt.Printf("Count: %d\n", dist.Count) } func simulatedDist(ticker Ticker, clk *clock.Mock) Distribution { since := clk.Now() until := since.Add(1 * time.Hour) var dist Distribution last := clk.Now() for !clk.Now().After(until) { select { case tm := <-ticker.Elapsed(): dist.Buckets[tm.Second()]++ dist.Count++ dist.Waittime += tm.Sub(last).Seconds() last = tm default: clk.Add(1 * time.Second) } } return dist }