1
0
buffer/pool_test.go
2026-02-28 14:12:34 +01:00

909 lines
18 KiB
Go

package buffer_test
import (
"bytes"
"code.squareroundforest.org/arpio/buffer"
"errors"
"io"
"math/rand"
"testing"
)
type fakePool struct {
allocSize int
alloc, free int
errAfter []int
zeroAfter []int
varyingSize []int
rand *rand.Rand
}
type foreverPool struct {
allocSize int
items [][]byte
}
type syncedForeverPool[T any] struct {
create func() T
items chan []T
}
func (p fakePool) allocCondition(c *[]int) bool {
if len(*c) == 0 {
return false
}
if p.alloc < (*c)[0] {
return false
}
*c = (*c)[1:]
return true
}
func (p *fakePool) ensureRand() {
if p.rand != nil {
return
}
p.rand = rand.New(rand.NewSource(9))
}
func (p *fakePool) Get() ([]byte, error) {
defer func() {
p.alloc++
}()
if p.allocCondition(&p.errAfter) {
return nil, errTest
}
if p.allocCondition(&p.zeroAfter) {
return nil, nil
}
n := p.allocSize
if len(p.varyingSize) > 1 {
p.ensureRand()
n = p.varyingSize[0] + p.rand.Intn(p.varyingSize[1])
}
if len(p.varyingSize) == 1 {
p.ensureRand()
n = 1 + p.rand.Intn(p.varyingSize[0])
}
return make([]byte, n), nil
}
func (p *fakePool) Put([]byte) {
p.free++
}
func (p *foreverPool) Get() ([]byte, error) {
if len(p.items) == 0 {
return make([]byte, p.allocSize), nil
}
var i []byte
i, p.items = p.items[0], p.items[1:]
return i, nil
}
func (p *foreverPool) Put(i []byte) {
p.items = append(p.items, i)
}
func newSyncedForeverPool[T any](c func() T) *syncedForeverPool[T] {
items := make(chan []T, 1)
items <- nil
return &syncedForeverPool[T]{
create: c,
items: items,
}
}
func (p *syncedForeverPool[T]) Get() (T, error) {
items := <-p.items
defer func() {
p.items <- items
}()
if len(items) == 0 {
return p.create(), nil
}
var i T
i, items = items[0], items[1:]
return i, nil
}
func (p *syncedForeverPool[T]) Put(i T) {
items := <-p.items
defer func() {
p.items <- items
}()
items = append(items, i)
}
func TestPoolUsage(t *testing.T) {
for _, cr := range []createReader{buffer.BufferedReader, testContent} {
t.Run("allocate", func(t *testing.T) {
t.Run("read", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b := make([]byte, 256)
n, err := r.Read(b)
if err != nil {
t.Fatal(err)
}
if n != len(b) {
t.Fatal("invalid read length")
}
if !bytes.Equal(b, generate(len(b))) {
t.Fatal("invalid content")
}
if p.alloc != 1 {
t.Fatal("invalid allocation count")
}
})
t.Run("read bytes", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 9}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<12)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("unexpected delimiter")
}
if len(b) != 0 {
t.Fatal("invalid content")
}
if p.alloc != 8 {
t.Fatal("invalid allocation count")
}
})
t.Run("read utf8", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
runes, n, err := r.ReadUTF8(1 << 12)
if err != nil {
t.Fatal(err)
}
if n != 1<<12 {
t.Fatal("unexpected delimiter")
}
if len(runes) != 1<<12 {
t.Fatal("invalid content")
}
if p.alloc != 3 {
t.Fatal("invalid allocation count")
}
})
t.Run("peek", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(3 * 1 << 12)
if err != nil {
t.Fatal(err)
}
if len(b) != 3*1<<12 {
t.Fatal("invalid content")
}
if p.alloc != 3 {
t.Fatal("invalid allocation count")
}
})
t.Run("buffered", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b := r.Buffered()
if len(b) != 0 {
t.Fatal("invalid content")
}
if p.alloc != 0 {
t.Fatal("invalid allocation count")
}
})
t.Run("write to", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
var b bytes.Buffer
n, err := r.WriteTo(&b)
if err != nil {
t.Fatal(err)
}
if n != 1<<15 {
t.Fatal("invalid write length")
}
if !bytes.Equal(b.Bytes(), generate(1<<15)) {
t.Fatal("invalid content")
}
if p.alloc != 1 {
t.Fatal("invalid allocation count")
}
})
})
t.Run("free", func(t *testing.T) {
t.Run("read", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b := make([]byte, 1<<9)
for {
_, err := r.Read(b)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
}
if p.alloc != 1 {
t.Fatal("invalid allocation count")
}
if p.free != 1 {
t.Fatal("invalid free count")
}
})
t.Run("read bytes", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
_, _, err := r.ReadBytes([]byte("123"), 1<<15+3)
if err != nil {
t.Fatal(err)
}
_, _, err = r.ReadBytes([]byte("123"), 1<<15+3)
if !errors.Is(err, io.EOF) {
t.Fatal(err)
}
if p.alloc != 9 {
t.Fatal("invalid allocation count", p.alloc)
}
if p.free != 9 {
t.Fatal("invalid free count", p.free)
}
})
t.Run("read utf8", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
for {
runes, n, err := r.ReadUTF8(1 << 12)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
if n != 1<<12 {
t.Fatal("unexpected delimiter")
}
if len(runes) != 1<<12 {
t.Fatal("invalid content")
}
}
if p.alloc != p.free {
t.Fatal("invalid allocation count", p.alloc, p.free)
}
})
t.Run("peek", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(3 * 1 << 12)
if err != nil {
t.Fatal(err)
}
if len(b) != 3*1<<12 {
t.Fatal("invalid content")
}
if p.alloc != 3 {
t.Fatal("invalid allocation count")
}
if p.free != 0 {
t.Fatal("invalid allocation count")
}
b = make([]byte, 1<<9)
for {
_, err := r.Read(b)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
}
if p.alloc != 3 {
t.Fatal("invalid allocation count")
}
if p.free != 3 {
t.Fatal("invalid allocation count")
}
})
t.Run("buffered", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
b := r.Buffered()
if len(b) != 0 {
t.Fatal("invalid content")
}
if p.alloc != 0 || p.free != 0 {
t.Fatal("invalid allocation count")
}
})
t.Run("write to", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1 << 12}
o := buffer.Options{Pool: p}
r := cr(g, o)
var b bytes.Buffer
n, err := r.WriteTo(&b)
if err != nil {
t.Fatal(err)
}
if n != 1<<15 {
t.Fatal("invalid write length")
}
if !bytes.Equal(b.Bytes(), generate(1<<15)) {
t.Fatal("invalid content")
}
if p.alloc != 1 {
t.Fatal("invalid allocation count")
}
if p.free != 1 {
t.Fatal("invalid free count")
}
})
})
t.Run("null segment", func(t *testing.T) {
t.Run("read", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 12,
zeroAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b := make([]byte, 256)
_, err := r.Read(b)
if !errors.Is(err, buffer.ErrZeroAllocation) {
t.Fatal("failed to fail", err)
}
})
t.Run("read bytes", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 9,
zeroAfter: []int{1},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
_, _, err := r.ReadBytes([]byte("123"), 1<<12)
if err != nil {
t.Fatal(err)
}
_, _, err = r.ReadBytes([]byte("123"), 1<<12)
if !errors.Is(err, buffer.ErrZeroAllocation) {
t.Fatal("failed to fail", err)
}
})
t.Run("read utf8", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 12,
zeroAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
_, _, err := r.ReadUTF8(1 << 12)
if !errors.Is(err, buffer.ErrZeroAllocation) {
t.Fatal("failed to fail", err)
}
})
t.Run("peek", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 12,
zeroAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
_, err := r.Peek(3 * 1 << 12)
if !errors.Is(err, buffer.ErrZeroAllocation) {
t.Fatal("failed to fail", err)
}
})
t.Run("write to", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 12,
zeroAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
var b bytes.Buffer
_, err := r.WriteTo(&b)
if !errors.Is(err, buffer.ErrZeroAllocation) {
t.Fatal("failed to fail", err)
}
})
})
t.Run("varying segment sizes", func(t *testing.T) {
t.Run("read bytes", func(t *testing.T) {
t.Run("find", func(t *testing.T) {
g := &gen{
max: 1 << 15,
customContentAfter: []int{1 << 11},
customContent: map[int][]byte{1 << 11: []byte("123")},
}
p := &fakePool{varyingSize: []int{8, 256}}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<15)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("failed to find delimiter")
}
if !bytes.Equal(b, append(generate(1<<11), []byte("123")...)) {
t.Fatal("invalid content")
}
})
t.Run("find not", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{varyingSize: []int{8, 256}}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<15)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("invalid delimiter")
}
if !bytes.Equal(b, generate(1<<15)) {
t.Fatal("invalid content")
}
})
})
t.Run("peek", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{varyingSize: []int{8, 256}}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(1 << 11)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, generate(1<<11)) {
t.Fatal("invalid content")
}
})
})
t.Run("one byte segments", func(t *testing.T) {
t.Run("read", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, generate(1<<15)) {
t.Fatal("invalid content")
}
})
t.Run("read bytes", func(t *testing.T) {
t.Run("find", func(t *testing.T) {
g := &gen{
max: 1 << 15,
customContentAfter: []int{1 << 11},
customContent: map[int][]byte{1 << 11: []byte("123")},
}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<15)
if err != nil {
t.Fatal(err)
}
if !ok {
t.Fatal("failed to find delimiter")
}
if !bytes.Equal(b, append(generate(1<<11), []byte("123")...)) {
t.Fatal("invalid content")
}
})
t.Run("find not", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<15)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("invalid delimiter found")
}
if len(b) != 0 {
t.Fatal("invalid content", len(b))
}
})
})
t.Run("read utf8", func(t *testing.T) {
g := &gen{
max: 1 << 15,
rng: utf8W2Range,
}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
runes, n, err := r.ReadUTF8(1 << 14)
if err != nil {
t.Fatal(err)
}
if n != 1<<15 {
t.Fatal("invalid read length")
}
if string(runes) != string(generateFrom(utf8W2Range, 1<<15)) {
t.Fatal("invalid content")
}
})
t.Run("peek", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(1 << 14)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, generate(1<<14)) {
t.Fatal("invalid content")
}
})
t.Run("write to", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{allocSize: 1}
o := buffer.Options{Pool: p}
r := cr(g, o)
var b bytes.Buffer
n, err := r.WriteTo(&b)
if err != nil {
t.Fatal(err)
}
if n != 1<<15 {
t.Fatal("invalid write length")
}
if !bytes.Equal(b.Bytes(), generate(1<<15)) {
t.Fatal("invalid content")
}
})
})
t.Run("pool error on allocate", func(t *testing.T) {
t.Run("read", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := io.ReadAll(r)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail")
}
if len(b) != 0 {
t.Fatal("invalid content")
}
})
t.Run("read bytes", func(t *testing.T) {
t.Run("immediate", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<12)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail with the right error", err)
}
if ok {
t.Fatal("invalid delimiter found")
}
if len(b) != 0 {
t.Fatal("invalid content")
}
})
t.Run("on grow", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{1},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, ok, err := r.ReadBytes([]byte("123"), 1<<15)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("invalid delimiter found")
}
if !bytes.Equal(b, generate(1<<11)) {
t.Fatal("invalid content")
}
b, ok, err = r.ReadBytes([]byte("123"), 1<<15)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail with the right error", err)
}
if ok {
t.Fatal("invalid delimiter found")
}
if len(b) != 0 {
t.Fatal("invalid content")
}
})
})
t.Run("read utf8", func(t *testing.T) {
g := &gen{
max: 1 << 15,
rng: utf8W2Range,
}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
runes, n, err := r.ReadUTF8(1 << 14)
if !errors.Is(err, errTest) {
t.Fatal(err)
}
if n != 0 {
t.Fatal("invalid read length")
}
if len(runes) != 0 {
t.Fatal("invalid content")
}
})
t.Run("peek", func(t *testing.T) {
t.Run("immediate", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(1 << 12)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail with the right error", err)
}
if len(b) != 0 {
t.Fatal("invalid content")
}
})
t.Run("on grow", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{1},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
b, err := r.Peek(1 << 15)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, generate(1<<11)) {
t.Fatal("invalid content")
}
b, ok, err := r.ReadBytes([]byte("123"), 1<<11)
if err != nil {
t.Fatal(err)
}
if ok {
t.Fatal("invalid delimiter found")
}
if !bytes.Equal(b, generate(1<<11)) {
t.Fatal("invalid content")
}
b, err = r.Peek(1 << 15)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail with the right error")
}
if len(b) != 0 {
t.Fatal("invalid content")
}
})
})
t.Run("write to", func(t *testing.T) {
g := &gen{max: 1 << 15}
p := &fakePool{
allocSize: 1 << 11,
errAfter: []int{0},
}
o := buffer.Options{Pool: p}
r := cr(g, o)
var b bytes.Buffer
n, err := r.WriteTo(&b)
if !errors.Is(err, errTest) {
t.Fatal("failed to fail with the right error")
}
if n != 0 {
t.Fatal("invalid write length")
}
if b.Len() != 0 {
t.Fatal("invalid content")
}
})
})
}
}
func TestDefaultPool(t *testing.T) {
g := &gen{max: 1 << 18}
r := buffer.BufferedReader(g, buffer.Options{})
b, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(b, generate(1<<18)) {
t.Fatal("output does not match", len(b))
}
}