The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/util/ring/ring_test.go

573 lines
16 KiB

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)
}
}