diff --git a/ringbuffer/ringbuffer.go b/ringbuffer/ringbuffer.go index e3095594..96ee1bc0 100644 --- a/ringbuffer/ringbuffer.go +++ b/ringbuffer/ringbuffer.go @@ -2,7 +2,6 @@ package ringbuffer import ( "sync" - "sync/atomic" ) type buffer[T any] struct { @@ -30,29 +29,25 @@ func New[T any](size int64) *RingBuffer[T] { func (rb *RingBuffer[T]) Push(item T) { rb.mu.Lock() - rb.content.tail = (rb.content.tail + 1) % rb.content.mod - if rb.content.tail == rb.content.head { - size := rb.content.mod * 2 - newBuff := make([]T, size) - for i := int64(0); i < rb.content.mod; i++ { - idx := (rb.content.tail + i) % rb.content.mod - newBuff[i] = rb.content.items[idx] - } - content := &buffer[T]{ - items: newBuff, - head: 0, - tail: rb.content.mod, - mod: size, - } - rb.content = content + nextTail := (rb.content.tail + 1) % rb.content.mod + if rb.len == rb.content.mod { + // Buffer is full, advance head to overwrite oldest element + rb.content.head = (rb.content.head + 1) % rb.content.mod + } else { + // Buffer not full, increment length + rb.len++ } - atomic.AddInt64(&rb.len, 1) + + rb.content.tail = nextTail rb.content.items[rb.content.tail] = item rb.mu.Unlock() } func (rb *RingBuffer[T]) Len() int64 { - return atomic.LoadInt64(&rb.len) + rb.mu.Lock() + length := rb.len + rb.mu.Unlock() + return length } func (rb *RingBuffer[T]) Pop() (T, bool) { @@ -66,7 +61,7 @@ func (rb *RingBuffer[T]) Pop() (T, bool) { item := rb.content.items[rb.content.head] var t T rb.content.items[rb.content.head] = t - atomic.AddInt64(&rb.len, -1) + rb.len-- rb.mu.Unlock() return item, true } @@ -82,7 +77,7 @@ func (rb *RingBuffer[T]) PopN(n int64) ([]T, bool) { if n >= rb.len { n = rb.len } - atomic.AddInt64(&rb.len, -n) + rb.len -= n items := make([]T, n) for i := int64(0); i < n; i++ { diff --git a/ringbuffer/ringbuffer_test.go b/ringbuffer/ringbuffer_test.go index 3e0ea552..758c3647 100644 --- a/ringbuffer/ringbuffer_test.go +++ b/ringbuffer/ringbuffer_test.go @@ -24,18 +24,28 @@ func TestPushPop(t *testing.T) { } func TestPushPopN(t *testing.T) { - rb := New[Item](1024) + size := int64(1024) + rb := New[Item](size) n := 5000 + + // Since we're pushing more items than buffer size, + // only the last 'size' items should be present for i := 0; i < n; i++ { rb.Push(Item{i}) } - items, ok := rb.PopN(int64(n)) + + items, ok := rb.PopN(size) if !ok { - t.Fatal("expected to pop many items") + t.Fatal("expected to pop items") } - for i := 0; i < n; i++ { - if items[i].i != i { - t.Fatal("invalid item popped") + + // Verify we got the most recent items (n-size to n) + startIdx := n - int(size) + for i := 0; i < len(items); i++ { + expected := startIdx + i + if items[i].i != expected { + t.Fatalf("invalid item at position %d: got %d, want %d", + i, items[i].i, expected) } } } @@ -93,3 +103,153 @@ func TestPopThreadSafety(t *testing.T) { } }) } + +func TestPushPopReal(t *testing.T) { + rb := New[int](10) + + // Push 10 values + for i := int64(0); i < 10; i++ { + rb.Push(int(i)) + if rb.Len() != i+1 { + t.Fatalf("expected length %d after push, got %d", i+1, rb.Len()) + } + } + + // Pop 10 values + for i := int64(0); i < 10; i++ { + val, ok := rb.Pop() + if !ok { + t.Fatalf("expected Pop to succeed on iteration %d", i) + } + if val != int(i) { + t.Errorf("expected value %d, got %d", i, val) + } + } + + // Verify buffer is empty + if rb.Len() != 0 { + t.Errorf("expected empty buffer, got length %d", rb.Len()) + } +} + +func TestPopEdge(t *testing.T) { + rb := New[int](1) + + // Test pop from empty buffer + item, ok := rb.Pop() + if ok { + t.Fatalf("expected Pop to return false on empty buffer, got true") + } + if item != 0 { + t.Errorf("expected zero value for item, got %d", item) + } +} + +func TestPopNEdge(t *testing.T) { + rb := New[int](1) + + // Test PopN on empty buffer + items, ok := rb.PopN(1) + if ok { + t.Fatalf("expected PopN to return false on empty buffer, got true") + } + if items != nil { + t.Errorf("expected nil slice, got %v", items) + } + + // Test PopN with n <= 0 + items, ok = rb.PopN(0) + if ok || items != nil { + t.Errorf("expected PopN(0) to return (nil, false) on empty buffer, got (%v, %v)", items, ok) + } + + // Test PopN with n > buffer size + rb.Push(1) + items, ok = rb.PopN(2) + if !ok || len(items) != 1 || items[0] != 1 { + t.Errorf("expected PopN(2) to return slice [1], got %v, %v", items, ok) + } +} + +func TestCircularWrap(t *testing.T) { + rb := New[int](3) + + // Fill buffer + rb.Push(1) + rb.Push(2) + rb.Push(3) + + // Pop one and push one to force wrap + val, _ := rb.Pop() + if val != 1 { + t.Errorf("expected 1, got %d", val) + } + rb.Push(4) + + // Verify correct order after wrap + expected := []int{2, 3, 4} + for i, exp := range expected { + val, ok := rb.Pop() + if !ok || val != exp { + t.Errorf("item %d: expected %d, got %d", i, exp, val) + } + } +} + +func TestConcurrentAccess(t *testing.T) { + rb := New[int](5) + done := make(chan bool) + + // Concurrent push + go func() { + for i := 0; i < 100; i++ { + rb.Push(i) + } + done <- true + }() + + // Concurrent pop + go func() { + for i := 0; i < 100; i++ { + rb.Pop() + } + done <- true + }() + + // Wait for both goroutines + <-done + <-done +} + +func BenchmarkPush(b *testing.B) { + rb := New[int](100) + for i := 0; i < b.N; i++ { + rb.Push(i) + } +} + +func BenchmarkPop(b *testing.B) { + rb := New[int](100) + for i := 0; i < 100; i++ { + rb.Push(i) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + rb.Pop() + rb.Push(i) + } +} + +func BenchmarkPopN(b *testing.B) { + rb := New[int](100) + for i := 0; i < 100; i++ { + rb.Push(i) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + rb.PopN(10) + for j := 0; j < 10; j++ { + rb.Push(j) + } + } +}