mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
654 lines
14 KiB
Go
654 lines
14 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package ringbuffer
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
rb := New[int]()
|
|
if rb.Len() != 0 {
|
|
t.Errorf("new buffer should be empty, got len=%d", rb.Len())
|
|
}
|
|
if rb.Cap() != 0 {
|
|
t.Errorf("new buffer should have zero capacity (lazy allocation), got cap=%d", rb.Cap())
|
|
}
|
|
if !rb.IsEmpty() {
|
|
t.Error("new buffer should be empty")
|
|
}
|
|
|
|
// After first push, buffer should be allocated
|
|
rb.Push(1)
|
|
if rb.Cap() < 16 {
|
|
t.Errorf("after first push, buffer should be allocated, got cap=%d", rb.Cap())
|
|
}
|
|
}
|
|
|
|
func TestNewWithSize(t *testing.T) {
|
|
size := 32
|
|
rb := NewWithSize[string](size)
|
|
if rb.Cap() != 0 {
|
|
t.Errorf("new buffer should have zero capacity (lazy allocation), got cap=%d", rb.Cap())
|
|
}
|
|
|
|
// After first push, buffer should be allocated
|
|
rb.Push("test")
|
|
if rb.Cap() < 16 {
|
|
t.Errorf("after first push, buffer should be allocated, got cap=%d", rb.Cap())
|
|
}
|
|
}
|
|
|
|
func TestPushPop(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Push some values
|
|
for i := 0; i < 5; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
if rb.Len() != 5 {
|
|
t.Errorf("expected len=5, got %d", rb.Len())
|
|
}
|
|
|
|
// Pop values in FIFO order
|
|
for i := 0; i < 5; i++ {
|
|
val, ok := rb.Pop()
|
|
if !ok {
|
|
t.Fatalf("Pop() failed at iteration %d", i)
|
|
}
|
|
if val != i {
|
|
t.Errorf("expected %d, got %d", i, val)
|
|
}
|
|
}
|
|
|
|
if rb.Len() != 0 {
|
|
t.Errorf("buffer should be empty, got len=%d", rb.Len())
|
|
}
|
|
}
|
|
|
|
func TestPopEmpty(t *testing.T) {
|
|
rb := New[int]()
|
|
val, ok := rb.Pop()
|
|
if ok {
|
|
t.Error("Pop() on empty buffer should return false")
|
|
}
|
|
if val != 0 {
|
|
t.Errorf("Pop() on empty buffer should return zero value, got %d", val)
|
|
}
|
|
}
|
|
|
|
func TestPeek(t *testing.T) {
|
|
rb := New[string]()
|
|
|
|
// Peek empty buffer
|
|
_, ok := rb.Peek()
|
|
if ok {
|
|
t.Error("Peek() on empty buffer should return false")
|
|
}
|
|
|
|
rb.Push("first")
|
|
rb.Push("second")
|
|
|
|
val, ok := rb.Peek()
|
|
if !ok {
|
|
t.Fatal("Peek() should return true")
|
|
}
|
|
if val != "first" {
|
|
t.Errorf("expected 'first', got '%s'", val)
|
|
}
|
|
|
|
// Peek shouldn't remove the element
|
|
if rb.Len() != 2 {
|
|
t.Errorf("Peek() shouldn't change length, got %d", rb.Len())
|
|
}
|
|
|
|
// Verify Pop still gets the same element
|
|
val, _ = rb.Pop()
|
|
if val != "first" {
|
|
t.Errorf("expected 'first', got '%s'", val)
|
|
}
|
|
}
|
|
|
|
func TestGrowth(t *testing.T) {
|
|
rb := NewWithSize[int](4)
|
|
initialCap := rb.Cap()
|
|
|
|
// Fill the buffer
|
|
for i := 0; i < initialCap; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
if !rb.IsFull() {
|
|
t.Error("buffer should be full")
|
|
}
|
|
|
|
// Push one more to trigger growth
|
|
rb.Push(999)
|
|
|
|
if rb.Cap() <= initialCap {
|
|
t.Errorf("buffer should have grown, cap=%d, initialCap=%d", rb.Cap(), initialCap)
|
|
}
|
|
|
|
if rb.Len() != initialCap+1 {
|
|
t.Errorf("expected len=%d, got %d", initialCap+1, rb.Len())
|
|
}
|
|
|
|
// Verify all elements are still there in order
|
|
for i := 0; i < initialCap; i++ {
|
|
val, ok := rb.Pop()
|
|
if !ok || val != i {
|
|
t.Errorf("expected %d, got %d (ok=%v)", i, val, ok)
|
|
}
|
|
}
|
|
val, ok := rb.Pop()
|
|
if !ok || val != 999 {
|
|
t.Errorf("expected 999, got %d (ok=%v)", val, ok)
|
|
}
|
|
}
|
|
|
|
func TestGrowthWithWraparound(t *testing.T) {
|
|
rb := NewWithSize[int](4)
|
|
|
|
// Create wraparound condition
|
|
rb.Push(1)
|
|
rb.Push(2)
|
|
rb.Push(3)
|
|
rb.Pop() // Remove 1
|
|
rb.Pop() // Remove 2
|
|
rb.Push(4)
|
|
rb.Push(5)
|
|
rb.Push(6) // Buffer is now [6, _, _, 3, 4, 5] with wrap
|
|
|
|
// Now it's full and wrapped, trigger growth
|
|
rb.Push(7)
|
|
|
|
// Verify order is preserved
|
|
expected := []int{3, 4, 5, 6, 7}
|
|
for i, exp := range expected {
|
|
val, ok := rb.Pop()
|
|
if !ok || val != exp {
|
|
t.Errorf("iteration %d: expected %d, got %d (ok=%v)", i, exp, val, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClear(t *testing.T) {
|
|
rb := New[int]()
|
|
for i := 0; i < 10; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
rb.Clear()
|
|
|
|
if !rb.IsEmpty() {
|
|
t.Error("buffer should be empty after Clear()")
|
|
}
|
|
if rb.Len() != 0 {
|
|
t.Errorf("len should be 0, got %d", rb.Len())
|
|
}
|
|
|
|
// Should be able to use buffer after clear
|
|
rb.Push(42)
|
|
val, ok := rb.Pop()
|
|
if !ok || val != 42 {
|
|
t.Errorf("expected 42, got %d (ok=%v)", val, ok)
|
|
}
|
|
}
|
|
|
|
func TestCompaction(t *testing.T) {
|
|
rb := NewWithSize[int](16)
|
|
|
|
// Grow the buffer significantly
|
|
for i := 0; i < 100; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
largeCap := rb.Cap()
|
|
if largeCap <= 16 {
|
|
t.Fatalf("buffer should have grown, cap=%d", largeCap)
|
|
}
|
|
|
|
// Empty most of the buffer
|
|
for i := 0; i < 99; i++ {
|
|
rb.Pop()
|
|
}
|
|
|
|
// Trigger many operations at low capacity to simulate sustained low usage
|
|
for i := 0; i < 300; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
|
|
// Buffer should have compacted
|
|
finalCap := rb.Cap()
|
|
if finalCap >= largeCap {
|
|
t.Logf("Warning: buffer didn't compact as expected. largeCap=%d, finalCap=%d", largeCap, finalCap)
|
|
// Don't fail, as compaction thresholds are heuristic
|
|
}
|
|
}
|
|
|
|
func TestWatermarkTracking(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Push some elements
|
|
for i := 0; i < 20; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
stats := rb.Stats()
|
|
if stats.PeakSize < 20 {
|
|
t.Errorf("PeakSize should be at least 20, got %d", stats.PeakSize)
|
|
}
|
|
|
|
// Pop all
|
|
for i := 0; i < 20; i++ {
|
|
rb.Pop()
|
|
}
|
|
|
|
stats = rb.Stats()
|
|
// Peak should have been tracked
|
|
if stats.PeakSize < 0 {
|
|
t.Errorf("PeakSize should be non-negative, got %d", stats.PeakSize)
|
|
}
|
|
}
|
|
|
|
func TestMaxInWindowTracking(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Simulate a workload that oscillates between high and low usage
|
|
// The max-in-window should track the peak size
|
|
|
|
// Phase 1: Grow to 50 elements
|
|
for i := 0; i < 50; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
stats := rb.Stats()
|
|
if stats.PeakSize < 50 {
|
|
t.Errorf("After pushing 50, PeakSize should be at least 50, got %d", stats.PeakSize)
|
|
}
|
|
|
|
// Phase 2: Maintain around 30 elements for a while
|
|
for i := 0; i < 20; i++ {
|
|
rb.Pop()
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
|
|
stats = rb.Stats()
|
|
// Peak should track the maximum seen
|
|
if stats.PeakSize < 30 {
|
|
t.Logf("After sustained usage at 30, PeakSize=%d (expected >= 30)", stats.PeakSize)
|
|
}
|
|
|
|
// Phase 3: Drop to near empty and wait for window reset
|
|
for rb.Len() > 2 {
|
|
rb.Pop()
|
|
}
|
|
for i := 0; i < 300; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
|
|
stats = rb.Stats()
|
|
// Peak should have reset to current low value after window expires
|
|
if stats.PeakSize > 10 {
|
|
t.Logf("After sustained low usage and window reset, PeakSize=%d", stats.PeakSize)
|
|
}
|
|
|
|
// IdleTicks should have accumulated
|
|
if stats.IdleTicks == 0 {
|
|
t.Error("IdleTicks should have accumulated during sustained low usage")
|
|
}
|
|
}
|
|
|
|
func TestStats(t *testing.T) {
|
|
rb := New[int]()
|
|
rb.Push(1)
|
|
rb.Push(2)
|
|
|
|
stats := rb.Stats()
|
|
if stats.Len != 2 {
|
|
t.Errorf("Stats.Len should be 2, got %d", stats.Len)
|
|
}
|
|
if stats.Cap != rb.Cap() {
|
|
t.Errorf("Stats.Cap mismatch: %d vs %d", stats.Cap, rb.Cap())
|
|
}
|
|
|
|
str := stats.String()
|
|
if str == "" {
|
|
t.Error("Stats.String() should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestGenericTypes(t *testing.T) {
|
|
// Test with struct type
|
|
type testStruct struct {
|
|
id int
|
|
name string
|
|
}
|
|
|
|
rb := New[testStruct]()
|
|
rb.Push(testStruct{1, "one"})
|
|
rb.Push(testStruct{2, "two"})
|
|
|
|
val, ok := rb.Pop()
|
|
if !ok || val.id != 1 || val.name != "one" {
|
|
t.Errorf("expected {1, 'one'}, got {%d, '%s'} (ok=%v)", val.id, val.name, ok)
|
|
}
|
|
|
|
// Test with pointer type
|
|
rbPtr := New[*testStruct]()
|
|
s1 := &testStruct{10, "ten"}
|
|
rbPtr.Push(s1)
|
|
|
|
val2, ok := rbPtr.Pop()
|
|
if !ok || val2 != s1 {
|
|
t.Error("pointer value mismatch")
|
|
}
|
|
}
|
|
|
|
func TestLargeBuffer(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Add many elements
|
|
n := 10000
|
|
for i := 0; i < n; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
if rb.Len() != n {
|
|
t.Errorf("expected len=%d, got %d", n, rb.Len())
|
|
}
|
|
|
|
// Remove them all
|
|
for i := 0; i < n; i++ {
|
|
val, ok := rb.Pop()
|
|
if !ok {
|
|
t.Fatalf("Pop() failed at iteration %d", i)
|
|
}
|
|
if val != i {
|
|
t.Errorf("expected %d, got %d", i, val)
|
|
}
|
|
}
|
|
|
|
if !rb.IsEmpty() {
|
|
t.Error("buffer should be empty")
|
|
}
|
|
}
|
|
|
|
func TestAlternatingPushPop(t *testing.T) {
|
|
rb := NewWithSize[int](8)
|
|
|
|
// Simulate a queue with alternating push/pop
|
|
// Push 2, pop 1 each iteration - buffer grows by 1 each time
|
|
for i := 0; i < 100; i++ {
|
|
rb.Push(i)
|
|
rb.Push(i + 100)
|
|
_, ok := rb.Pop()
|
|
if !ok {
|
|
t.Fatalf("Pop() failed at iteration %d", i)
|
|
}
|
|
}
|
|
|
|
// Should have one element per iteration remaining (we pushed 200, popped 100)
|
|
if rb.Len() != 100 {
|
|
t.Errorf("expected len=100, got %d", rb.Len())
|
|
}
|
|
|
|
// Verify buffer grew to accommodate the data
|
|
if rb.Cap() < 100 {
|
|
t.Errorf("buffer should have grown to at least 100, got %d", rb.Cap())
|
|
}
|
|
|
|
// Drain and verify FIFO order
|
|
// Pattern pushed: 0, 100, 1, 101, 2, 102, ... 99, 199
|
|
// We popped first 100 items: 0, 1, 2, ... 49, 100, 101, ... 149
|
|
// Remaining: 50, 150, 51, 151, 52, 152, ... 99, 199
|
|
for i := 0; i < 100; i++ {
|
|
v, ok := rb.Pop()
|
|
if !ok {
|
|
t.Fatalf("Pop() failed when draining at position %d", i)
|
|
}
|
|
// Interleaved pattern: even positions get 50+i/2, odd get 150+i/2
|
|
var expected int
|
|
if i%2 == 0 {
|
|
expected = 50 + i/2
|
|
} else {
|
|
expected = 150 + i/2
|
|
}
|
|
if v != expected {
|
|
t.Errorf("draining position %d: expected %d, got %d", i, expected, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkPush(b *testing.B) {
|
|
rb := New[int]()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Push(i)
|
|
}
|
|
}
|
|
|
|
func BenchmarkPop(b *testing.B) {
|
|
rb := New[int]()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Push(i)
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Pop()
|
|
}
|
|
}
|
|
|
|
func BenchmarkPushPop(b *testing.B) {
|
|
rb := New[int]()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
}
|
|
|
|
func BenchmarkPushPopWithGrowth(b *testing.B) {
|
|
rb := NewWithSize[int](4)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
rb.Push(i)
|
|
if i%2 == 0 {
|
|
rb.Pop()
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNilBufferBehavior(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Buffer should start as nil
|
|
if rb.Cap() != 0 {
|
|
t.Errorf("new buffer should have nil buf (cap=0), got cap=%d", rb.Cap())
|
|
}
|
|
|
|
// Should handle operations on nil buffer
|
|
if !rb.IsEmpty() {
|
|
t.Error("nil buffer should report as empty")
|
|
}
|
|
if rb.Len() != 0 {
|
|
t.Errorf("nil buffer should have len=0, got %d", rb.Len())
|
|
}
|
|
|
|
_, ok := rb.Pop()
|
|
if ok {
|
|
t.Error("Pop on nil buffer should return false")
|
|
}
|
|
|
|
_, ok = rb.Peek()
|
|
if ok {
|
|
t.Error("Peek on nil buffer should return false")
|
|
}
|
|
|
|
// Push should allocate buffer
|
|
rb.Push(42)
|
|
if rb.Cap() == 0 {
|
|
t.Error("buffer should be allocated after first Push")
|
|
}
|
|
if rb.Len() != 1 {
|
|
t.Errorf("expected len=1 after push, got %d", rb.Len())
|
|
}
|
|
|
|
// Pop back to empty
|
|
val, ok := rb.Pop()
|
|
if !ok || val != 42 {
|
|
t.Errorf("Pop should return 42, got %d (ok=%v)", val, ok)
|
|
}
|
|
|
|
// After being idle while empty, buffer should be freed
|
|
for i := 0; i < 250; i++ {
|
|
rb.Pop() // Trigger idle ticks
|
|
}
|
|
|
|
if rb.Cap() != 0 {
|
|
t.Logf("Note: buffer not yet freed after idle period, cap=%d", rb.Cap())
|
|
// This is fine - compaction happens in considerCompaction which is only
|
|
// called from Pop when there's something to pop
|
|
}
|
|
|
|
// Clear should free the buffer
|
|
rb.Push(1)
|
|
rb.Push(2)
|
|
rb.Clear()
|
|
if rb.Cap() != 0 {
|
|
t.Errorf("Clear should free buffer, got cap=%d", rb.Cap())
|
|
}
|
|
}
|
|
|
|
func TestBufferDeallocationWhenIdle(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Push and then pop to create a buffer
|
|
for i := 0; i < 50; i++ {
|
|
rb.Push(i)
|
|
}
|
|
initialCap := rb.Cap()
|
|
if initialCap == 0 {
|
|
t.Fatal("buffer should be allocated after pushes")
|
|
}
|
|
|
|
for i := 0; i < 50; i++ {
|
|
rb.Pop()
|
|
}
|
|
|
|
// Buffer should still exist but be empty
|
|
if rb.Len() != 0 {
|
|
t.Errorf("buffer should be empty, got len=%d", rb.Len())
|
|
}
|
|
if rb.Cap() == 0 {
|
|
t.Error("buffer should not be immediately freed")
|
|
}
|
|
|
|
// Sustain empty state for idle threshold operations
|
|
// Call Pop repeatedly on empty buffer to accumulate idle ticks
|
|
for i := 0; i < 250; i++ {
|
|
rb.Pop() // Pop on empty buffer still updates watermarks
|
|
}
|
|
|
|
// Now buffer should be deallocated
|
|
if rb.Cap() != 0 {
|
|
t.Errorf("buffer should be freed after sustained idle empty state, got cap=%d", rb.Cap())
|
|
}
|
|
|
|
// Should still work after deallocation
|
|
rb.Push(999)
|
|
if rb.Len() != 1 {
|
|
t.Errorf("expected len=1 after push, got %d", rb.Len())
|
|
}
|
|
val, ok := rb.Pop()
|
|
if !ok || val != 999 {
|
|
t.Errorf("expected 999, got %d (ok=%v)", val, ok)
|
|
}
|
|
}
|
|
|
|
func TestBurstyWorkload(t *testing.T) {
|
|
rb := New[int]()
|
|
|
|
// Simulate bursty workload: mostly idle at 10 items, bursts to 1000
|
|
// Max-in-window should size for the bursts, not the average
|
|
|
|
// Initial burst
|
|
for i := 0; i < 1000; i++ {
|
|
rb.Push(i)
|
|
}
|
|
stats := rb.Stats()
|
|
if stats.PeakSize < 1000 {
|
|
t.Errorf("Peak should capture initial burst of 1000, got %d", stats.PeakSize)
|
|
}
|
|
|
|
burstCap := rb.Cap()
|
|
t.Logf("After burst, capacity=%d", burstCap)
|
|
|
|
// Work through burst back to idle
|
|
for i := 0; i < 990; i++ {
|
|
rb.Pop()
|
|
}
|
|
|
|
// Stay at 10 items for a while (less than window size)
|
|
for i := 0; i < 100; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
|
|
// Peak should still remember the burst within the window
|
|
stats = rb.Stats()
|
|
if stats.PeakSize < 10 {
|
|
t.Errorf("Peak should track current size in window, got %d", stats.PeakSize)
|
|
}
|
|
|
|
// Do another small burst before window resets
|
|
for i := 0; i < 50; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
stats = rb.Stats()
|
|
if stats.PeakSize < 50 {
|
|
t.Errorf("Peak should capture 50-item burst, got %d", stats.PeakSize)
|
|
}
|
|
|
|
// Drain back down
|
|
for rb.Len() > 10 {
|
|
rb.Pop()
|
|
}
|
|
|
|
// Key test: After 256 operations, window resets
|
|
// Do low-level operations to trigger window reset
|
|
for i := 0; i < 260; i++ {
|
|
rb.Push(i)
|
|
rb.Pop()
|
|
}
|
|
|
|
// Peak should have reset to reflect only recent operations
|
|
stats = rb.Stats()
|
|
if stats.PeakSize > 20 {
|
|
t.Logf("After window reset (260 ops), peak=%d (should be ~10)", stats.PeakSize)
|
|
}
|
|
|
|
// Another burst - demonstrates max-in-window captures peaks
|
|
for i := 0; i < 500; i++ {
|
|
rb.Push(i)
|
|
}
|
|
|
|
stats = rb.Stats()
|
|
if stats.PeakSize < 500 {
|
|
t.Errorf("Peak should capture 500-item burst, got %d", stats.PeakSize)
|
|
}
|
|
|
|
// Buffer should have grown to accommodate
|
|
if rb.Cap() < 512 {
|
|
t.Errorf("Buffer should have grown for burst, got cap=%d", rb.Cap())
|
|
}
|
|
}
|