deterministic_test.go 6.29 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
package clock

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestNowReturnsCurrentTime(t *testing.T) {
	now := time.UnixMilli(23829382)
	clock := NewDeterministicClock(now)
	require.Equal(t, now, clock.Now())
}

func TestAdvanceTime(t *testing.T) {
	start := time.UnixMilli(1000)
	clock := NewDeterministicClock(start)
	clock.AdvanceTime(500 * time.Millisecond)

	require.Equal(t, start.Add(500*time.Millisecond), clock.Now())
}

func TestAfter(t *testing.T) {
	t.Run("ZeroCompletesImmediately", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ch := clock.After(0)
		require.Len(t, ch, 1, "duration should already have been reached")
	})

	t.Run("CompletesWhenTimeAdvances", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ch := clock.After(500 * time.Millisecond)
		require.Len(t, ch, 0, "should not complete immediately")

		clock.AdvanceTime(499 * time.Millisecond)
		require.Len(t, ch, 0, "should not complete before time is due")

		clock.AdvanceTime(1 * time.Millisecond)
		require.Len(t, ch, 1, "should complete when time is reached")
		require.Equal(t, clock.Now(), <-ch)
	})

	t.Run("CompletesWhenTimeAdvancesPastDue", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ch := clock.After(500 * time.Millisecond)
		require.Len(t, ch, 0, "should not complete immediately")

		clock.AdvanceTime(9000 * time.Millisecond)
		require.Len(t, ch, 1, "should complete when time is past")
		require.Equal(t, clock.Now(), <-ch)
	})

	t.Run("RegisterAsPending", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		_ = clock.After(500 * time.Millisecond)

		ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancelFunc()
		require.True(t, clock.WaitForNewPendingTask(ctx), "should have added a new pending task")
	})
}

func TestNewTicker(t *testing.T) {
	t.Run("FiresAfterEachDuration", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately")

		clock.AdvanceTime(4 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire before due")

		clock.AdvanceTime(1 * time.Second)
		require.Len(t, ticker.Ch(), 1, "should fire when due")
		require.Equal(t, clock.Now(), <-ticker.Ch(), "should post current time")

		clock.AdvanceTime(4 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not re-fire before due")

		clock.AdvanceTime(1 * time.Second)
		require.Len(t, ticker.Ch(), 1, "should fire when due")
		require.Equal(t, clock.Now(), <-ticker.Ch(), "should post current time")
	})

	t.Run("SkipsFiringWhenAdvancedMultipleDurations", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately")

		// Advance more than three periods, but should still only fire once
		clock.AdvanceTime(16 * time.Second)
		require.Len(t, ticker.Ch(), 1, "should fire when due")
		require.Equal(t, clock.Now(), <-ticker.Ch(), "should post current time")

		clock.AdvanceTime(1 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire until due again")
	})

	t.Run("StopFiring", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)

		ticker.Stop()

		clock.AdvanceTime(10 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire after stop")
	})

	t.Run("ResetPanicWhenLessNotPositive", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		require.Panics(t, func() {
			ticker.Reset(0)
		}, "reset to 0 should panic")
		require.Panics(t, func() {
			ticker.Reset(-1)
		}, "reset to negative duration should panic")
	})

	t.Run("ResetWithShorterPeriod", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately")

		ticker.Reset(1 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately after reset")

		clock.AdvanceTime(1 * time.Second)
		require.Len(t, ticker.Ch(), 1, "should fire when new duration reached")
		require.Equal(t, clock.Now(), <-ticker.Ch(), "should post current time")
	})

	t.Run("ResetWithLongerPeriod", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately")

		ticker.Reset(7 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire immediately after reset")

		clock.AdvanceTime(5 * time.Second)
		require.Len(t, ticker.Ch(), 0, "should not fire when old duration reached")

		clock.AdvanceTime(2 * time.Second)
		require.Len(t, ticker.Ch(), 1, "should fire when new duration reached")
		require.Equal(t, clock.Now(), <-ticker.Ch(), "should post current time")
	})

	t.Run("RegisterAsPending", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		ticker := clock.NewTicker(5 * time.Second)
		defer ticker.Stop()

		ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancelFunc()
		require.True(t, clock.WaitForNewPendingTask(ctx), "should have added a new pending task")
	})
}

func TestWaitForPending(t *testing.T) {
	t.Run("DoNotBlockWhenAlreadyPending", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		_ = clock.After(5 * time.Minute)
		_ = clock.After(5 * time.Minute)

		ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancelFunc()
		require.True(t, clock.WaitForNewPendingTask(ctx), "should have added a new pending task")
	})

	t.Run("ResetNewPendingFlagAfterWaiting", func(t *testing.T) {
		clock := NewDeterministicClock(time.UnixMilli(1000))
		_ = clock.After(5 * time.Minute)
		_ = clock.After(5 * time.Minute)

		ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancelFunc()
		require.True(t, clock.WaitForNewPendingTask(ctx), "should have added a new pending task")

		ctx, cancelFunc = context.WithTimeout(context.Background(), 250*time.Millisecond)
		defer cancelFunc()
		require.False(t, clock.WaitForNewPendingTask(ctx), "should have reset new pending task flag")
	})
}