grafana/pkg/util/ring/ring_test.go

574 lines
16 KiB
Go
Raw Normal View History

package ring
import (
"slices"
"testing"
"github.com/stretchr/testify/require"
)
func ints(n int) []int {
ret := make([]int, n)
for i := range ret {
ret[i] = i + 1
}
return ret
}
func TestRing(t *testing.T) {
t.Parallel()
const (
dLen = 10
dHalfLen = dLen / 2
)
data := ints(dLen)
lData := slices.Clone(data[:dHalfLen])
rData := slices.Clone(data[dHalfLen:])
require.NotPanics(t, func() {
new(Ring[int]).WriteStats(nil)
}, "WriteStats should be panic free")
t.Run("basic enqueue and dequeue - no min, no max", func(t *testing.T) {
t.Parallel()
q, expected := new(Ring[int]), new(Ring[int])
enq(t, q, data...)
expected.len = dLen
expected.stats.Enqueued = dLen
expected.buf = data
ringEq(t, expected, q)
deq(t, q, lData...)
expected.back = dHalfLen
expected.len = dHalfLen
expected.stats.Dequeued = dHalfLen
expected.buf = append(make([]int, dHalfLen), rData...)
ringEq(t, expected, q)
enq(t, q, data...)
expected.back = 0
expected.len = dLen + dHalfLen
expected.stats.Enqueued += dLen
expected.buf = append(rData, data...)
ringEq(t, expected, q)
deqAll(t, q, append(rData, data...)...)
expected.len = 0
expected.stats.Dequeued += dLen + dHalfLen
expected.buf = []int{}
ringEq(t, expected, q)
enq(t, q, data...)
expected.len = dLen
expected.stats.Enqueued += dLen
expected.buf = data
ringEq(t, expected, q)
clearRing(t, q)
expected.len = 0
expected.stats.Dequeued += dLen
expected.buf = []int{}
ringEq(t, expected, q)
})
t.Run("enqueue, dequeue, grow and shrink - no min, yes max", func(t *testing.T) {
t.Parallel()
q, expected := new(Ring[int]), new(Ring[int])
q.Max = dLen
// basic wrap and overwrite
enq(t, q, lData...)
enq(t, q, data...)
enq(t, q, data...)
expected.back = dHalfLen
expected.buf = append(rData, lData...)
expected.len = dLen
expected.stats.Enqueued = 2*dLen + dHalfLen
expected.stats.Dropped = dLen + dHalfLen
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
// can't allocate past max and cannot shrink because we're at capacity
q.Grow(3 * dLen)
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
q.Shrink(2 * dLen)
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
// remove some items and play with extra space
deq(t, q, lData...)
expected.back = 0
expected.buf = rData
expected.len -= dHalfLen
expected.stats.Dequeued = dHalfLen
require.Equal(t, dLen, q.Cap())
ringEq(t, expected, q)
q.Shrink(1)
ringEq(t, expected, q)
require.Equal(t, dHalfLen+1, q.Cap())
q.Grow(2)
ringEq(t, expected, q)
require.Equal(t, dHalfLen+2, q.Cap())
q.Grow(dLen)
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
})
t.Run("enqueue, dequeue, grow and shrink - yes min, no max", func(t *testing.T) {
t.Parallel()
q, expected := new(Ring[int]), new(Ring[int])
q.Min = dHalfLen
// enqueueing one item should allocate Min
enq(t, q, 1)
expected.buf = []int{1}
expected.len = 1
expected.stats.Enqueued = 1
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// clearing should not migrate now
clearRing(t, q)
expected.buf = []int{}
expected.len = 0
expected.stats.Dequeued = 1
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// enqueue some data
enq(t, q, data...)
expected.buf = data
expected.len = dLen
expected.stats.Enqueued += dLen
ringEq(t, expected, q)
require.GreaterOrEqual(t, q.Cap(), dLen)
// now clearing should migrate and move to a slice of Min length
clearRing(t, q)
expected.buf = []int{}
expected.len = 0
expected.stats.Dequeued += dLen
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// we shouldn't be able to shrink past Min, but it shouldn't allocate a
// greater slice either because it's purpose is to reduce allocated
// memory if possible
q.Min = dLen
q.Shrink(dHalfLen)
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// dequeueing shouldn't allocate either, just in case
require.Zero(t, q.Dequeue())
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// enqueueing one item allocates again to Min, which is now greater than
// before
enq(t, q, 1)
expected.buf = []int{1}
expected.len = 1
expected.stats.Enqueued += 1
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
// we reduce Min again, then we should be able to shrink as well
q.Min = dHalfLen
q.Shrink(dHalfLen)
ringEq(t, expected, q)
require.Equal(t, dHalfLen+1, q.Cap())
q.Shrink(1)
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
q.Shrink(0)
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// enqueue a lot and then dequeue all, we should still see Min cap
enq(t, q, data...)
expected.buf = append(expected.buf, data...)
expected.len += dLen
expected.stats.Enqueued += dLen
ringEq(t, expected, q)
require.GreaterOrEqual(t, q.Cap(), dLen+1)
deqAll(t, q, expected.buf...)
expected.buf = []int{}
expected.len = 0
expected.stats.Dequeued += dLen + 1
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
})
t.Run("enqueue, dequeue, grow and shrink - yes min, yes max", func(t *testing.T) {
t.Parallel()
q, expected := new(Ring[int]), new(Ring[int])
q.Min, q.Max = dHalfLen, dLen
// single enqueueing should allocate for Min
enq(t, q, 1)
expected.buf = []int{1}
expected.len = 1
expected.stats.Enqueued = 1
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
// enqueue a lot until we overwrite the first item
enq(t, q, data...)
expected.back = 1
expected.buf = append(data[dLen-1:], data[:dLen-1]...)
expected.len = dLen
expected.stats.Enqueued += dLen
expected.stats.Dropped = 1
ringEq(t, expected, q)
require.Equal(t, dLen, q.Cap())
// clearing should bring us back to Min alloc
clearRing(t, q)
expected.back = 0
expected.buf = expected.buf[:0]
expected.len = 0
expected.stats.Dequeued += dLen
ringEq(t, expected, q)
require.Equal(t, dHalfLen, q.Cap())
})
t.Run("growing and shrinking invariants - no min, no max", func(t *testing.T) {
t.Parallel()
q, expected := new(Ring[int]), new(Ring[int])
// dummy grow and shrink
q.Grow(0)
require.Equal(t, 0, q.Cap())
ringEq(t, expected, q)
q.Shrink(0)
require.Equal(t, 0, q.Cap())
ringEq(t, expected, q)
// add 3*dLen and leave 2*dLen
q.Grow(3 * dLen)
expected.buf = []int{}
require.Equal(t, 3*dLen, q.Cap())
ringEq(t, expected, q)
q.Shrink(2 * dLen)
require.Equal(t, 2*dLen, q.Cap())
ringEq(t, expected, q)
// add dLen items and play with cap
enq(t, q, data...)
expected.buf = data
expected.len = dLen
expected.stats.Enqueued = dLen
require.Equal(t, 2*dLen, q.Cap())
ringEq(t, expected, q)
q.Grow(2 * dLen)
require.GreaterOrEqual(t, q.Cap(), 3*dLen)
ringEq(t, expected, q)
q.Shrink(0)
require.Equal(t, dLen, q.Cap())
ringEq(t, expected, q)
// remove all items and shrink to zero
deqAll(t, q, data...)
expected.buf = []int{}
expected.len = 0
expected.stats.Dequeued = dLen
require.Equal(t, dLen, q.Cap())
ringEq(t, expected, q)
q.Shrink(0)
expected.buf = nil
require.Equal(t, 0, q.Cap())
ringEq(t, expected, q)
})
}
// enq enqueues the given items into the given Ring.
func enq[T any](t *testing.T, q *Ring[T], s ...T) {
t.Helper()
initLen := q.Len()
initCap := q.Cap()
for _, v := range s {
require.NotPanics(t, func() {
q.Enqueue(v)
})
}
expectedLen := initLen + len(s)
if q.Max > 0 {
expectedMax := max(initCap, q.Max)
expectedLen = min(expectedLen, expectedMax)
}
require.Equal(t, expectedLen, q.Len())
}
// deq dequeues len(expected) items from the given Ring and compares them to
// expected. Ring should have at least len(expected) items.
func deq[T any](t *testing.T, q *Ring[T], expected ...T) {
t.Helper()
if q.Cap() == 0 {
require.Nil(t, q.buf) // internal state
require.Equal(t, 0, q.back) // internal state
return
}
oldLen := q.Len()
require.True(t, oldLen >= len(expected))
got := make([]T, len(expected))
for i := range got {
var val T
require.NotPanics(t, func() {
prePeekLen := q.Len()
val = q.Peek()
require.Equal(t, prePeekLen, q.Len())
got[i] = q.Dequeue()
})
require.Equal(t, val, got[i])
}
require.Equal(t, expected, got)
require.Equal(t, oldLen-len(expected), q.Len())
}
// clearRing calls Clear on the given Ring and performs a set of assertions that
// should be satisfied afterwards.
func clearRing[T any](t *testing.T, q *Ring[T]) {
t.Helper()
var expectedBuf []T
if clearShouldMigrate(q.Cap(), q.Min, q.Max) {
expectedBuf = make([]T, q.Min)
} else {
expectedBuf = make([]T, q.Cap())
}
require.NotPanics(t, func() {
q.Clear()
})
require.Equal(t, expectedBuf, q.buf) // internal state
require.Equal(t, 0, q.Len())
require.Equal(t, 0, q.back) // internal state
// dequeueing should yield zero values
var zero T
for i := 0; i < 10; i++ {
var val1, val2 T
require.NotPanics(t, func() {
val1 = q.Peek()
val1 = q.Dequeue()
})
require.Equal(t, zero, val1)
require.Equal(t, zero, val2)
}
}
// deqAll depletes the given Ring and compares the dequeued items to those
// provided.
func deqAll[T any](t *testing.T, q *Ring[T], expected ...T) {
t.Helper()
deq[T](t, q, expected...)
zeroS := make([]T, q.Cap())
require.Equal(t, zeroS, q.buf) // internal state
require.Equal(t, 0, q.Len())
// dequeueing further should yield zero values when empty
var zero T
for i := 0; i < 10; i++ {
var val1, val2 T
require.NotPanics(t, func() {
val1 = q.Peek()
val2 = q.Dequeue()
})
require.Equal(t, zero, val1)
require.Equal(t, zero, val2)
}
clearRing(t, q)
}
// ringEq tests that the given Rings are the same in many aspects. The following
// are the things that are not checked:
// - The values of Min and Max, since the code does not programmatically
// channge them
// - Allocation numbers (Cap, Grown, Shrunk, Allocs)
// - The free capacity to the right of `got`
func ringEq[T any](t *testing.T, expected, got *Ring[T]) {
t.Helper()
var expStats, gotStats RingStats
require.NotPanics(t, func() {
expected.WriteStats(&expStats)
got.WriteStats(&gotStats)
})
// capacity and allocations are to be tested separately
removeAllocStats(&expStats)
removeAllocStats(&gotStats)
require.Equal(t, expStats, gotStats, "expStats == gotStats")
// internal state
require.Equal(t, expected.back, got.back, "expected.back == got.back")
// only check for used capacity
require.Equal(t, expected.buf, got.buf[:min(got.back+got.len, len(got.buf))],
"expected.buf == got.buf[:min(got.back+got.len, len(got.s))]")
}
func removeAllocStats(s *RingStats) {
s.Cap = 0
s.Grown = 0
s.Shrunk = 0
s.Allocs = 0
}
func TestMinMaxValidity(t *testing.T) {
t.Parallel()
testCases := []struct {
Min, Max int
minIsValid, maxIsValid bool
}{
{Min: 0, Max: 0, minIsValid: false, maxIsValid: false},
{Min: 0, Max: 1, minIsValid: false, maxIsValid: true},
{Min: 0, Max: 2, minIsValid: false, maxIsValid: true},
{Min: 1, Max: 0, minIsValid: true, maxIsValid: false},
{Min: 1, Max: 1, minIsValid: true, maxIsValid: true},
{Min: 1, Max: 2, minIsValid: true, maxIsValid: true},
{Min: 2, Max: 0, minIsValid: true, maxIsValid: false},
{Min: 2, Max: 1, minIsValid: false, maxIsValid: false},
{Min: 2, Max: 2, minIsValid: true, maxIsValid: true},
}
for i, tc := range testCases {
gotMinIsValid := minIsValid(tc.Min, tc.Max)
require.Equal(t, tc.minIsValid, gotMinIsValid,
"test index %d; test data: %#v", i, tc)
gotMaxIsValid := maxIsValid(tc.Min, tc.Max)
require.Equal(t, tc.maxIsValid, gotMaxIsValid,
"test index %d; test data: %#v", i, tc)
}
}
func TestClearShouldMigrate(t *testing.T) {
t.Parallel()
testCases := []struct {
// we don't need to include Max in the test, we just disable it by
// passing zero because Max is only needed to establish the validity of
// Min. The validity of Min wrt Max is already covered in the test for
// minIsValid, and once Min is valid Max has no impact on the outcome of
// clearShouldMigrate.
CurCap, Min int
expected bool
}{
{CurCap: 0, Min: 0, expected: false},
{CurCap: 0, Min: 9, expected: false},
{CurCap: 0, Min: 10, expected: false},
{CurCap: 0, Min: 11, expected: false},
{CurCap: 10, Min: 0, expected: false},
{CurCap: 10, Min: 9, expected: true},
{CurCap: 10, Min: 10, expected: false},
{CurCap: 10, Min: 11, expected: false},
}
for i, tc := range testCases {
got := clearShouldMigrate(tc.CurCap, tc.Min, 0)
require.Equal(t, tc.expected, got,
"test index %d; test data: %#v", i, tc)
}
}
func TestFixAllocSize(t *testing.T) {
t.Parallel()
testCases := []struct {
CurLen, Min, Max, NewCap, expected int
}{
// we don't need to add test cases for odd configurations of Min and Max
// not being valid for different reasons because that is already covered
// in the unit tests for minIsValid and maxIsValid. It suffices to
// provide a zero for Min or Max to disable their respective behaviour
{CurLen: 0, Min: 0, Max: 0, NewCap: 0, expected: 0},
{CurLen: 0, Min: 0, Max: 0, NewCap: 5, expected: 5},
{CurLen: 0, Min: 0, Max: 10, NewCap: 0, expected: 0},
{CurLen: 0, Min: 0, Max: 10, NewCap: 9, expected: 9},
{CurLen: 0, Min: 0, Max: 10, NewCap: 10, expected: 10},
{CurLen: 0, Min: 0, Max: 10, NewCap: 11, expected: 10},
{CurLen: 0, Min: 10, Max: 0, NewCap: 0, expected: 10},
{CurLen: 0, Min: 10, Max: 0, NewCap: 5, expected: 10},
{CurLen: 0, Min: 10, Max: 0, NewCap: 9, expected: 10},
{CurLen: 0, Min: 10, Max: 0, NewCap: 10, expected: 10},
{CurLen: 0, Min: 10, Max: 0, NewCap: 11, expected: 11},
{CurLen: 0, Min: 10, Max: 10, NewCap: 0, expected: 10},
{CurLen: 0, Min: 10, Max: 10, NewCap: 5, expected: 10},
{CurLen: 0, Min: 10, Max: 10, NewCap: 9, expected: 10},
{CurLen: 0, Min: 10, Max: 10, NewCap: 10, expected: 10},
{CurLen: 0, Min: 10, Max: 10, NewCap: 11, expected: 10},
{CurLen: 0, Min: 10, Max: 20, NewCap: 0, expected: 10},
{CurLen: 0, Min: 10, Max: 20, NewCap: 5, expected: 10},
{CurLen: 0, Min: 10, Max: 20, NewCap: 9, expected: 10},
{CurLen: 0, Min: 10, Max: 20, NewCap: 10, expected: 10},
{CurLen: 0, Min: 10, Max: 20, NewCap: 19, expected: 19},
{CurLen: 0, Min: 10, Max: 20, NewCap: 20, expected: 20},
{CurLen: 0, Min: 10, Max: 20, NewCap: 21, expected: 20},
{CurLen: 5, Min: 0, Max: 0, NewCap: 0, expected: 5},
{CurLen: 5, Min: 0, Max: 0, NewCap: 5, expected: 5},
{CurLen: 5, Min: 0, Max: 0, NewCap: 10, expected: 10},
{CurLen: 5, Min: 0, Max: 10, NewCap: 0, expected: 5},
{CurLen: 5, Min: 0, Max: 10, NewCap: 5, expected: 5},
{CurLen: 5, Min: 0, Max: 10, NewCap: 9, expected: 9},
{CurLen: 5, Min: 0, Max: 10, NewCap: 10, expected: 10},
{CurLen: 5, Min: 0, Max: 10, NewCap: 11, expected: 10},
{CurLen: 5, Min: 10, Max: 0, NewCap: 0, expected: 10},
{CurLen: 5, Min: 10, Max: 0, NewCap: 5, expected: 10},
{CurLen: 5, Min: 10, Max: 0, NewCap: 9, expected: 10},
{CurLen: 5, Min: 10, Max: 0, NewCap: 10, expected: 10},
{CurLen: 5, Min: 10, Max: 0, NewCap: 11, expected: 11},
{CurLen: 5, Min: 10, Max: 10, NewCap: 0, expected: 10},
{CurLen: 5, Min: 10, Max: 10, NewCap: 5, expected: 10},
{CurLen: 5, Min: 10, Max: 10, NewCap: 9, expected: 10},
{CurLen: 5, Min: 10, Max: 10, NewCap: 10, expected: 10},
{CurLen: 5, Min: 10, Max: 10, NewCap: 11, expected: 10},
{CurLen: 5, Min: 10, Max: 20, NewCap: 0, expected: 10},
{CurLen: 5, Min: 10, Max: 20, NewCap: 5, expected: 10},
{CurLen: 5, Min: 10, Max: 20, NewCap: 9, expected: 10},
{CurLen: 5, Min: 10, Max: 20, NewCap: 10, expected: 10},
{CurLen: 5, Min: 10, Max: 20, NewCap: 19, expected: 19},
{CurLen: 5, Min: 10, Max: 20, NewCap: 20, expected: 20},
{CurLen: 5, Min: 10, Max: 20, NewCap: 21, expected: 20},
}
for i, tc := range testCases {
got := fixAllocSize(tc.CurLen, tc.Min, tc.Max, tc.NewCap)
require.Equal(t, tc.expected, got,
"test index %d; test data %#v", i, tc)
}
}