package ring import ( "context" "testing" "time" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestMain(m *testing.M) { // make sure we don't leak goroutines after tests in this package have // finished. This is especially important as AdaptiveChan uses a different // goroutine to coordinate work goleak.VerifyTestMain(m) } func TestAdaptiveChan(t *testing.T) { t.Parallel() t.Run("edge case - close send and controller after creation", func(t *testing.T) { t.Parallel() send, recv, ctrl := AdaptiveChan[int]() cleanupAC(t, send, recv, ctrl) }) t.Run("basic operation", func(t *testing.T) { t.Parallel() var stats, expectedStats AdaptiveChanStats send, recv, ctrl := AdaptiveChan[int]() cleanupAC(t, send, recv, ctrl) sendNonBlock(t, send, ints(10)...) err := ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) removeAllocStats(&stats.RingStats) expectedStats.Len = 10 expectedStats.Enqueued = 10 require.Equal(t, expectedStats, stats) recvNonBlock(t, recv, ints(10)...) err = ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) removeAllocStats(&stats.RingStats) expectedStats.Len = 0 expectedStats.Dequeued = 10 expectedStats.StatsRead = 1 require.Equal(t, expectedStats, stats) }) t.Run("using commands to control the ring", func(t *testing.T) { t.Parallel() send, recv, ctrl := AdaptiveChan[int]() cleanupAC(t, send, recv, ctrl) var stats, expectedStats AdaptiveChanStats expectedStats.Min = 10 expectedStats.Max = 20 err := ctrl.Min(ctxFromTest(t), expectedStats.Min) require.NoError(t, err) err = ctrl.Max(ctxFromTest(t), expectedStats.Max) require.NoError(t, err) sendNonBlock(t, send, 1) err = ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) require.Equal(t, expectedStats.Min, stats.Cap, "failed to allocate Min") removeAllocStats(&stats.RingStats) expectedStats.Len = 1 expectedStats.Enqueued = 1 require.Equal(t, expectedStats, stats) err = ctrl.Grow(ctxFromTest(t), (expectedStats.Max+expectedStats.Min)/2-1) require.NoError(t, err) err = ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) require.Equal(t, (expectedStats.Max+expectedStats.Min)/2, stats.Cap, "failed to Grow") err = ctrl.Shrink(ctxFromTest(t), expectedStats.Min) require.NoError(t, err) err = ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) require.Equal(t, expectedStats.Min+1, stats.Cap, "failed to Shrink") err = ctrl.Clear(ctxFromTest(t)) require.NoError(t, err) err = ctrl.WriteStats(ctxFromTest(t), &stats) require.NoError(t, err) require.Equal(t, expectedStats.Min, stats.Cap, "failed to Clear") }) t.Run("use of send and recv channels with a closed controller", func(t *testing.T) { t.Parallel() send, recv, ctrl := AdaptiveChan[int]() ctrl.Close() assertCtrlWriteErr(t, ctrl, ctxFromTest(t), ErrAdaptiveChanControllerClosed) sendNonBlock(t, send, ints(10)...) recvNonBlock(t, recv, ints(10)...) close(send) shouldBeClosed(t, recv) }) } func TestSendOrErr(t *testing.T) { t.Parallel() const val = 44203 var c chan int err := sendOrErr(ctxFromTest(t), c, val) require.Error(t, err) require.ErrorIs(t, err, ErrAdaptiveChanControllerClosed) c = make(chan int, 1) err = sendOrErr(ctxFromTest(t), c, val) require.NoError(t, err) canceledCtx, cancel := context.WithCancel(context.Background()) cancel() err = sendOrErr(canceledCtx, c, val) require.Error(t, err) require.ErrorIs(t, err, context.Canceled) select { case v, ok := <-c: require.True(t, ok) require.Equal(t, val, v) default: t.Fatalf("value not sent to channel") } } func TestRecvOrErr(t *testing.T) { t.Parallel() const ( val = 44203 witness = -1 ) var c chan int canceledCtx, cancel := context.WithCancel(context.Background()) cancel() got := witness err := recvOrErr(canceledCtx, c, &got) require.Error(t, err) require.ErrorIs(t, err, context.Canceled) require.Equal(t, witness, got) c = make(chan int, 1) c <- val err = recvOrErr(ctxFromTest(t), c, &got) require.NoError(t, err) require.Equal(t, val, got) close(c) got = witness err = recvOrErr(ctxFromTest(t), c, &got) require.ErrorIs(t, err, ErrAdaptiveChanClosed) require.Equal(t, witness, got) } // cleanupAC closes the send channel and the controller, and perform a series of // rutinary assertions. func cleanupAC[T any](t *testing.T, send chan<- T, recv <-chan T, ctrl *AdaptiveChanController) { t.Cleanup(func() { close(send) shouldBeClosed(t, recv) var stats AdaptiveChanStats err := ctrl.WriteStats(ctxFromTest(t), &stats) require.Error(t, err) require.ErrorIs(t, err, ErrAdaptiveChanClosed) require.Equal(t, AdaptiveChanStats{}, stats) canceledCtx, cancel := context.WithCancel(context.Background()) cancel() assertCtrlWriteErr(t, ctrl, canceledCtx, context.Canceled) ctrl.Close() err = ctrl.WriteStats(ctxFromTest(t), &stats) require.Error(t, err) require.ErrorIs(t, err, ErrAdaptiveChanControllerClosed) require.Equal(t, AdaptiveChanStats{}, stats) assertCtrlWriteErr(t, ctrl, ctxFromTest(t), ErrAdaptiveChanControllerClosed) }) } func assertCtrlWriteErr(t *testing.T, ctrl *AdaptiveChanController, ctx context.Context, expectedErr error) { t.Helper() err := ctrl.Min(ctx, 1) require.Error(t, err) require.ErrorIs(t, err, expectedErr) err = ctrl.Max(ctx, 1) require.Error(t, err) require.ErrorIs(t, err, expectedErr) err = ctrl.Grow(ctx, 1) require.Error(t, err) require.ErrorIs(t, err, expectedErr) err = ctrl.Shrink(ctx, 1) require.Error(t, err) require.ErrorIs(t, err, expectedErr) err = ctrl.Clear(ctx) require.Error(t, err) require.ErrorIs(t, err, expectedErr) } func shouldBeClosed[T any](t *testing.T, recv <-chan T) { t.Helper() select { case v, ok := <-recv: require.False(t, ok, "unexpected value %q received", v) case <-ctxFromTest(t).Done(): t.Fatalf("context canceled where recv chan should be closed") } } func sendNonBlock[T any](t *testing.T, send chan<- T, s ...T) { t.Helper() var canceled bool for i, v := range s { require.NotPanics(t, func() { select { case send <- v: case <-ctxFromTest(t).Done(): canceled = true } }) require.False(t, canceled, "context canceled while sending item %d/%d", i+1, len(s)) } } func recvNonBlock[T any](t *testing.T, recv <-chan T, s ...T) { t.Helper() var canceled bool for i := range s { select { case s[i] = <-recv: case <-ctxFromTest(t).Done(): canceled = true } require.False(t, canceled, "context canceled while receiving item %d/%d", i+1, len(s)) } } func ctxFromTest(t *testing.T) context.Context { return ctxFromTestWithDefault(t, time.Second) } func ctxFromTestWithDefault(t *testing.T, d time.Duration) context.Context { require.Greater(t, d, 0*time.Second) deadline, ok := t.Deadline() if !ok { deadline = time.Now().Add(d) } ctx, cancel := context.WithDeadline(context.Background(), deadline) t.Cleanup(cancel) return ctx }