package ringbuffer import ( "sync" "testing" "github.com/stretchr/testify/require" ) func TestNew(t *testing.T) { tests := []struct { name string capacity int expected int }{ { name: "valid capacity", capacity: 10, expected: 10, }, { name: "zero capacity should default to 1", capacity: 0, expected: 1, }, { name: "negative capacity should default to 1", capacity: -5, expected: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { rb := New[int](tt.capacity) require.NotNil(t, rb) require.Equal(t, tt.expected, rb.Capacity()) require.Equal(t, 0, rb.Len()) }) } } func TestRingBuffer_Push(t *testing.T) { t.Run("push to empty buffer", func(t *testing.T) { rb := New[string](5) rb.Push(100, "first") require.Equal(t, 1, rb.Len()) val, ok := rb.Get(100) require.True(t, ok) require.Equal(t, "first", val) }) t.Run("push multiple items", func(t *testing.T) { rb := New[string](5) rb.Push(100, "first") rb.Push(200, "second") rb.Push(300, "third") require.Equal(t, 3, rb.Len()) val, ok := rb.Get(100) require.True(t, ok) require.Equal(t, "first", val) val, ok = rb.Get(200) require.True(t, ok) require.Equal(t, "second", val) val, ok = rb.Get(300) require.True(t, ok) require.Equal(t, "third", val) }) t.Run("push to full buffer removes oldest", func(t *testing.T) { rb := New[string](3) rb.Push(100, "first") rb.Push(200, "second") rb.Push(300, "third") require.Equal(t, 3, rb.Len()) // This should remove "first" rb.Push(400, "fourth") require.Equal(t, 3, rb.Len()) _, ok := rb.Get(100) require.False(t, ok) val, ok := rb.Get(400) require.True(t, ok) require.Equal(t, "fourth", val) }) t.Run("push with same timestamp updates value", func(t *testing.T) { rb := New[string](5) rb.Push(100, "first") rb.Push(100, "updated") require.Equal(t, 1, rb.Len()) val, ok := rb.Get(100) require.True(t, ok) require.Equal(t, "updated", val) }) } func TestRingBuffer_Get(t *testing.T) { rb := New[int](5) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) tests := []struct { name string timestamp int64 wantValue int wantOk bool }{ { name: "get existing item", timestamp: 100, wantValue: 10, wantOk: true, }, { name: "get middle item", timestamp: 200, wantValue: 20, wantOk: true, }, { name: "get last item", timestamp: 300, wantValue: 30, wantOk: true, }, { name: "get non-existent item", timestamp: 400, wantValue: 0, wantOk: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { val, ok := rb.Get(tt.timestamp) require.Equal(t, tt.wantOk, ok) require.Equal(t, tt.wantValue, val) }) } } func TestRingBuffer_CleanupBefore(t *testing.T) { t.Run("cleanup oldest items", func(t *testing.T) { rb := New[string](10) for i := range int64(10) { rb.Push(i*100, "value") } require.Equal(t, 10, rb.Len()) // Remove items with timestamp < 500 removed := rb.CleanupBefore(500) require.Equal(t, 5, removed) require.Equal(t, 5, rb.Len()) // Verify the correct items remain _, ok := rb.Get(400) require.False(t, ok) _, ok = rb.Get(500) require.True(t, ok) }) t.Run("cleanup all items", func(t *testing.T) { rb := New[string](5) rb.Push(100, "first") rb.Push(200, "second") rb.Push(300, "third") removed := rb.CleanupBefore(1000) require.Equal(t, 3, removed) require.Equal(t, 0, rb.Len()) }) t.Run("cleanup with no matching items", func(t *testing.T) { rb := New[string](5) rb.Push(100, "first") rb.Push(200, "second") removed := rb.CleanupBefore(50) require.Equal(t, 0, removed) require.Equal(t, 2, rb.Len()) }) t.Run("cleanup empty buffer", func(t *testing.T) { rb := New[string](5) removed := rb.CleanupBefore(100) require.Equal(t, 0, removed) require.Equal(t, 0, rb.Len()) }) } func TestRingBuffer_Range(t *testing.T) { t.Run("range over all items", func(t *testing.T) { rb := New[int](5) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) var ( timestamps []int64 values []int ) rb.Range(func(timestamp int64, value int) bool { timestamps = append(timestamps, timestamp) values = append(values, value) return true }) require.Equal(t, []int64{100, 200, 300}, timestamps) require.Equal(t, []int{10, 20, 30}, values) }) t.Run("range with early termination", func(t *testing.T) { rb := New[int](5) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) var count int rb.Range(func(timestamp int64, value int) bool { count++ return count < 2 // Stop after 2 items }) require.Equal(t, 2, count) }) t.Run("range over empty buffer", func(t *testing.T) { rb := New[int](5) called := false rb.Range(func(timestamp int64, value int) bool { called = true return true }) require.False(t, called) }) t.Run("range after wraparound", func(t *testing.T) { rb := New[int](3) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) rb.Push(400, 40) // This causes wraparound var timestamps []int64 rb.Range(func(timestamp int64, value int) bool { timestamps = append(timestamps, timestamp) return true }) require.Equal(t, []int64{200, 300, 400}, timestamps) }) } func TestRingBuffer_Len(t *testing.T) { rb := New[string](5) require.Equal(t, 0, rb.Len()) rb.Push(100, "first") require.Equal(t, 1, rb.Len()) rb.Push(200, "second") require.Equal(t, 2, rb.Len()) rb.CleanupBefore(150) require.Equal(t, 1, rb.Len()) rb.Clear() require.Equal(t, 0, rb.Len()) } func TestRingBuffer_Clear(t *testing.T) { rb := New[string](5) rb.Push(100, "first") rb.Push(200, "second") rb.Push(300, "third") require.Equal(t, 3, rb.Len()) rb.Clear() require.Equal(t, 0, rb.Len()) _, ok := rb.Get(100) require.False(t, ok) } func TestRingBuffer_GetAll(t *testing.T) { t.Run("get all from populated buffer", func(t *testing.T) { rb := New[int](5) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) items := rb.GetAll() require.Len(t, items, 3) require.Equal(t, int64(100), items[0].Timestamp) require.Equal(t, 10, items[0].Value) require.Equal(t, int64(200), items[1].Timestamp) require.Equal(t, 20, items[1].Value) require.Equal(t, int64(300), items[2].Timestamp) require.Equal(t, 30, items[2].Value) }) t.Run("get all from empty buffer", func(t *testing.T) { rb := New[int](5) items := rb.GetAll() require.Nil(t, items) }) t.Run("get all after wraparound", func(t *testing.T) { rb := New[int](3) rb.Push(100, 10) rb.Push(200, 20) rb.Push(300, 30) rb.Push(400, 40) items := rb.GetAll() require.Len(t, items, 3) require.Equal(t, int64(200), items[0].Timestamp) require.Equal(t, int64(300), items[1].Timestamp) require.Equal(t, int64(400), items[2].Timestamp) }) } func TestRingBuffer_Concurrent(t *testing.T) { t.Run("concurrent push", func(t *testing.T) { rb := New[int](100) var wg sync.WaitGroup // Spawn 10 goroutines, each pushing 10 items for i := range 10 { wg.Add(1) go func(base int) { defer wg.Done() for j := range 10 { timestamp := int64(base*10 + j) rb.Push(timestamp, base*10+j) } }(i) } wg.Wait() // We should have 100 items require.Equal(t, 100, rb.Len()) }) t.Run("concurrent push and get", func(t *testing.T) { rb := New[int](50) var wg sync.WaitGroup // Push items wg.Go(func() { for i := range 50 { rb.Push(int64(i), i*10) } }) // Read items wg.Go(func() { for i := range 50 { rb.Get(int64(i)) } }) wg.Wait() }) t.Run("concurrent cleanup and range", func(t *testing.T) { rb := New[int](100) // Populate buffer for i := range 100 { rb.Push(int64(i), i) } var wg sync.WaitGroup // Cleanup old items wg.Go(func() { for i := range 10 { rb.CleanupBefore(int64(i * 10)) } }) // Range over items wg.Go(func() { for range 10 { rb.Range(func(timestamp int64, value int) bool { return true }) } }) wg.Wait() }) } func TestRingBuffer_ComplexScenario(t *testing.T) { t.Run("sliding window simulation", func(t *testing.T) { // Simulate a 10-second sliding window with 1-second slots rb := New[int](10) // Add data for timestamps 0-9 for i := range int64(10) { rb.Push(i, int(i*100)) } require.Equal(t, 10, rb.Len()) // Advance time to 15, cleanup old data (< 5) rb.CleanupBefore(5) require.Equal(t, 5, rb.Len()) // Add more data for i := int64(10); i < 15; i++ { rb.Push(i, int(i*100)) } require.Equal(t, 10, rb.Len()) // Verify only recent data exists _, ok := rb.Get(4) require.False(t, ok) val, ok := rb.Get(5) require.True(t, ok) require.Equal(t, 500, val) val, ok = rb.Get(14) require.True(t, ok) require.Equal(t, 1400, val) }) t.Run("metrics aggregation pattern", func(t *testing.T) { type Metric struct { Count int64 Sum int64 } rb := New[*Metric](60) // 60-second window // Simulate recording metrics for i := range int64(60) { rb.Push(i, &Metric{Count: 1, Sum: i}) } // Calculate aggregated metrics var totalCount, totalSum int64 rb.Range(func(timestamp int64, value *Metric) bool { totalCount += value.Count totalSum += value.Sum return true }) require.Equal(t, int64(60), totalCount) require.Equal(t, int64(1770), totalSum) // Sum of 0 to 59 }) } func BenchmarkRingBuffer_Push(b *testing.B) { rb := New[int](1000) for i := 0; b.Loop(); i++ { rb.Push(int64(i), i) } } func BenchmarkRingBuffer_Get(b *testing.B) { rb := New[int](1000) for i := range 1000 { rb.Push(int64(i), i) } for i := 0; b.Loop(); i++ { rb.Get(int64(i % 1000)) } } func BenchmarkRingBuffer_CleanupBefore(b *testing.B) { for b.Loop() { b.StopTimer() rb := New[int](1000) for j := range 1000 { rb.Push(int64(j), j) } b.StartTimer() rb.CleanupBefore(500) } } func BenchmarkRingBuffer_Range(b *testing.B) { rb := New[int](1000) for i := range 1000 { rb.Push(int64(i), i) } for b.Loop() { rb.Range(func(timestamp int64, value int) bool { return true }) } }