Files
kcp-go/ringbuffer_test.go
xtaci 6d3de74039 fix(ringbuffer): fix off-by-one in Discard() causing index out of range panic
RingBuffer.Discard() used `end <= cap` to decide whether the discard
range is contiguous. When head + n equals exactly len(elements), this
incorrectly set head to len(elements) instead of wrapping it to 0.

A subsequent Peek() or Push() would then access elements[len(elements)],
triggering a runtime panic: "index out of range [64] with length 64".

The crash path in production:
  KCP.Input -> parse_una -> snd_buf.Discard(count) -> shrink_buf ->
  snd_buf.Peek() -> panic

Fix: change the boundary condition from `end <= cap` to `end < cap`,
so the exact-boundary case falls through to the wrap-around branch
which correctly computes head = end - cap = 0.

Add TestRingBufferDiscardBoundary to cover this edge case.
2026-02-10 14:29:29 +08:00

302 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package kcp
import (
"math/rand"
"testing"
)
func TestRingSize(t *testing.T) {
r := NewRingBuffer[int](1)
if r.Len() != 0 {
t.Errorf("Expected length 0, got %d", r.Len())
}
// re-zero
for i := range 64 {
r.Push(i)
r.Pop()
if r.Len() != 0 {
t.Errorf("Expected length 0 after pushing and popping, got %d", r.Len())
}
}
left := 1024 * 1024
for i := 0; i < left; i++ {
r.Push(i)
}
for {
use := rand.Int() % (left + 1)
left -= use
for range use {
if _, ok := r.Pop(); !ok {
t.Errorf("Expected to pop value, but got none")
}
}
if r.Len() != left {
t.Errorf("Expected length %d after popping, got %d", left, r.Len())
}
t.Log("use", use, "left", left, "head", r.head, "tail", r.tail, "len", r.Len(), "maxlen", r.MaxLen())
if left == 0 {
break
}
}
if r.Len() != 0 {
t.Errorf("Expected length 0 after pushing and popping, got %d", r.Len())
}
}
func TestRingSize2(t *testing.T) {
// emulate a condition where head = 32, tail=31, and Len() = 63
r := NewRingBuffer[int](64)
for i := range 63 {
r.Push(i)
}
for range 32 {
if _, ok := r.Pop(); !ok {
t.Errorf("Expected to pop value, but got none")
}
}
for i := range 32 {
r.Push(i + 63)
if r.Len() != i+(63-32+1) {
t.Errorf("Expected length %d after popping 31 elements, got %d", i+(63-32+1), r.Len())
}
}
// check head, tail and Len()
if r.head != 32 {
t.Errorf("Expected head to be 32, got %d", r.head)
}
if r.tail != 31 {
t.Errorf("Expected tail to be 31, got %d", r.tail)
}
if r.Len() != 63 {
t.Errorf("Expected length to be 63, got %d", r.Len())
}
t.Log("head", r.head, "tail", r.tail, "len", r.Len(), "maxlen", r.MaxLen())
// now we push one element to trigger grow
r.Push(95)
if r.MaxLen() != 127 {
t.Errorf("Expected capacity to be 128 after grow, got %d", r.MaxLen())
}
if r.Len() != 64 {
t.Errorf("Expected length to be 64 after pushing one element, got %d", r.Len())
}
if r.head != 0 {
t.Errorf("Expected head to be 0 after grow, got %d", r.head)
}
if r.tail != 64 {
t.Errorf("Expected tail to be 64 after grow, got %d", r.tail)
}
}
func TestRingBuffer(t *testing.T) {
r := NewRingBuffer[int](1)
if r.Len() != 0 {
t.Errorf("Expected length 0, got %d", r.Len())
}
for i := range 64 {
r.Push(i)
if r.Len() != i+1 {
t.Errorf("Expected length %d, got %d", i+1, r.Len())
}
}
for i := range 32 {
val, ok := r.Pop()
if !ok || val != i {
t.Errorf("Expected to pop %d, got %d (ok: %v)", i, val, ok)
}
}
if r.Len() != 32 {
t.Errorf("Expected length 32 after popping 32 elements, got %d", r.Len())
}
// Push more elements to test the ring's behavior
size := r.Len()
for i := range 32 {
r.Push(i)
if r.Len() != i+1+size {
t.Errorf("Expected length %d, got %d", i+1+size, r.Len())
}
}
if r.Len() != 64 {
t.Errorf("Expected length 64 after pushing 32 more elements, got %d", r.Len())
}
// ringbuffer should be [32 ... 63, 0 ... 31]
evicted := 0
round := 0
expectedHead := []int{62, 28}
for !r.IsEmpty() {
evicted += r.Discard(30)
if round < len(expectedHead) {
if v, ok := r.Peek(); !ok || *v != expectedHead[round] {
t.Errorf("Invalid discard state: unexpected %d", *v)
}
} else if _, ok := r.Peek(); ok {
t.Errorf("Unexpected non-nil head element")
}
if r.Len()+evicted != 64 {
t.Errorf("Unexpected ringbuffer length after discard op")
}
round++
}
}
func TestRingBufferGrow(t *testing.T) {
initialSize := 4
r := NewRingBuffer[int](initialSize)
expectedCapacities := []int{8, 16, 32, 64, 128}
// Push enough elements to trigger multiple grows
pushCount := 100
for i := range pushCount {
r.Push(i)
// Check if capacity is among expected
capacity := r.MaxLen()
valid := false
for _, ec := range expectedCapacities {
if capacity == ec-1 {
valid = true
break
}
}
if !valid && capacity <= RINGBUFFER_EXP {
t.Errorf("unexpected capacity during growth: %d", capacity)
}
}
// Push to 1024
for i := pushCount; i < 1024; i++ {
r.Push(i)
}
// Make sure the growth is 10% increase after 4096
r.Push(1)
if r.MaxLen() != 1126 {
t.Errorf("expected capacity to be 1126, got %d", r.MaxLen())
}
// Check values are preserved in correct order
for i := range pushCount {
v, ok := r.Pop()
if !ok {
t.Fatalf("expected to pop value at index %d", i)
return
}
if v != i {
t.Errorf("expected value %d, got %d", i, v)
}
}
}
func TestRingForEach(t *testing.T) {
r := NewRingBuffer[int](10)
for i := range 10 {
r.Push(i)
}
sum := 0
i := 0
r.ForEach(func(v *int) bool {
if *v != i {
t.Errorf("Expected index %d, got %d", i, *v)
}
i++
sum += *v
return true
})
if sum != 45 { // 0 + 1 + ... + 9 = 45
t.Errorf("Expected sum to be 45, got %d", sum)
}
count := 0
r.ForEach(func(v *int) bool {
count++
return true
})
if count != 10 {
t.Errorf("Expected count to be 10, got %d", count)
}
}
// TestRingBufferDiscardBoundary tests discarding elements that land exactly
// at the end of the backing array. This previously caused head to equal
// len(elements), leading to an index-out-of-range panic in Peek().
func TestRingBufferDiscardBoundary(t *testing.T) {
// Create a ring buffer with backing array length 64 (MaxLen = 63).
r := NewRingBuffer[int](64)
// Fill to capacity (63 elements), head=0, tail=63.
for i := range 63 {
r.Push(i)
}
// Pop 1 element so head=1, push 1 so tail wraps to 0. Still 63 elements.
r.Pop()
r.Push(63)
// Now head=1, tail=0, len=63.
// Discard exactly 63 elements. head + 63 = 64 = len(elements).
// This must wrap head to 0, not leave it at 64.
r.Discard(63)
if r.Len() != 0 {
t.Fatalf("expected empty ring, got Len()=%d", r.Len())
}
// Push a new element and Peek — this panicked before the fix.
r.Push(99)
if v, ok := r.Peek(); !ok || *v != 99 {
t.Fatalf("expected Peek()=99, got %v (ok=%v)", v, ok)
}
}
func TestRingForEachReverse(t *testing.T) {
r := NewRingBuffer[int](10)
for i := range 10 {
r.Push(i)
}
sum := 0
i := 9
r.ForEachReverse(func(v *int) bool {
if *v != i {
t.Errorf("Expected index %d, got %d", i, *v)
}
i--
sum += *v
return true
})
if sum != 45 { // 0 + 1 + ... + 9 = 45
t.Errorf("Expected sum to be 45, got %d", sum)
}
count := 0
r.ForEachReverse(func(v *int) bool {
count++
return true
})
if count != 10 {
t.Errorf("Expected count to be 10, got %d", count)
}
}