init repo
This commit is contained in:
commit
3c238b5248
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.cover
|
||||||
25
Makefile
Normal file
25
Makefile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
sources = $(shell find . -name "*.go")
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build: $(sources)
|
||||||
|
go build
|
||||||
|
|
||||||
|
fmt: $(sources)
|
||||||
|
go fmt
|
||||||
|
|
||||||
|
check: $(sources)
|
||||||
|
go test -count 1
|
||||||
|
|
||||||
|
.cover: $(sources)
|
||||||
|
go test -count 1 -coverprofile .cover
|
||||||
|
|
||||||
|
cover: .cover
|
||||||
|
go tool cover -func .cover
|
||||||
|
|
||||||
|
showcover: .cover
|
||||||
|
go tool cover -html .cover
|
||||||
|
|
||||||
|
clean:
|
||||||
|
go clean
|
||||||
|
rm .cover
|
||||||
172
buffered_test.go
Normal file
172
buffered_test.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuffered(t *testing.T) {
|
||||||
|
t.Run("none buffered", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b := r.Buffered()
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buffered after error", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 12,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(18)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all buffered", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(48)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(48)) {
|
||||||
|
t.Fatal("invalid content 1", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if !bytes.Equal(b, generate(48)) {
|
||||||
|
t.Fatal("invalid content 2", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buffered across segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(144)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(144)) {
|
||||||
|
t.Fatal("invalid content 1", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if !bytes.Equal(b, generate(192)) {
|
||||||
|
t.Fatal("invalid content 2", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buffered mid segment", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b := make([]byte, 32)
|
||||||
|
n, err := r.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 32 {
|
||||||
|
t.Fatal("invalid read length", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(32)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if !bytes.Equal(b, generate(64)[32:]) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("buffered mid segment across segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b := make([]byte, 288)
|
||||||
|
n, err := r.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 288 {
|
||||||
|
t.Fatal("invalid read length", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(288)) {
|
||||||
|
t.Fatal("invalid content 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if !bytes.Equal(b, generate(384)[288:]) {
|
||||||
|
t.Fatal("invalid content 2", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero buffered mid segment", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b := make([]byte, 64)
|
||||||
|
n, err := r.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 64 {
|
||||||
|
t.Fatal("invalid read length", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(64)) {
|
||||||
|
t.Fatal("invalid content 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = r.Buffered()
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content 2", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module code.squareroundforest.org/arpio/buffer
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
145
io_test.go
Normal file
145
io_test.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gen struct {
|
||||||
|
rng []byte
|
||||||
|
max int
|
||||||
|
fastErr bool
|
||||||
|
nullReadAfter []int
|
||||||
|
errAfter []int
|
||||||
|
customContentAfter []int
|
||||||
|
customContent map[int][]byte
|
||||||
|
counter int
|
||||||
|
}
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
written []byte
|
||||||
|
errAfter []int
|
||||||
|
shortAfter []int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
genRange = []byte("abcdefghi")
|
||||||
|
utf8Range = []byte("aábéícóöődúüeű")
|
||||||
|
utf8W2Range = []byte("áéíóöőúüű")
|
||||||
|
errTest = errors.New("test error")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *gen) Read(p []byte) (int, error) {
|
||||||
|
if g.max == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.nullReadAfter) > 0 && g.counter >= g.nullReadAfter[0] {
|
||||||
|
g.nullReadAfter = g.nullReadAfter[1:]
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.errAfter) > 0 && g.counter >= g.errAfter[0] {
|
||||||
|
g.errAfter = g.errAfter[1:]
|
||||||
|
return 0, errTest
|
||||||
|
}
|
||||||
|
|
||||||
|
l := len(p)
|
||||||
|
hasMax := g.max > 0
|
||||||
|
if hasMax && l > g.max {
|
||||||
|
l = g.max
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.rng) == 0 {
|
||||||
|
g.rng = genRange
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for l > 0 {
|
||||||
|
rng := make([]byte, len(g.rng))
|
||||||
|
copy(rng, g.rng)
|
||||||
|
c := g.counter % len(rng)
|
||||||
|
rng = append(rng[c:], rng[:c]...)
|
||||||
|
li := l
|
||||||
|
if li > len(rng) {
|
||||||
|
li = len(rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := len(g.customContentAfter) > 0 &&
|
||||||
|
g.counter <= g.customContentAfter[0] &&
|
||||||
|
g.counter+li > g.customContentAfter[0]
|
||||||
|
if cc && g.counter != g.customContentAfter[0] {
|
||||||
|
li = g.customContentAfter[0] - g.counter
|
||||||
|
cc = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var ni int
|
||||||
|
if cc {
|
||||||
|
content := g.customContent[g.customContentAfter[0]]
|
||||||
|
ni = copy(p[:li], content)
|
||||||
|
if ni == len(content) {
|
||||||
|
g.customContentAfter = g.customContentAfter[1:]
|
||||||
|
} else {
|
||||||
|
g.customContentAfter[0] += ni
|
||||||
|
g.customContent[g.customContentAfter[0]] = content[ni:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ni = copy(p[:li], rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
n += ni
|
||||||
|
p = p[ni:]
|
||||||
|
l -= ni
|
||||||
|
g.counter += ni
|
||||||
|
if cc {
|
||||||
|
c := g.counter % len(g.rng)
|
||||||
|
rng = append(rng[c:], rng[:c]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasMax {
|
||||||
|
g.max -= n
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.errAfter) > 0 && g.counter >= g.errAfter[0] && g.fastErr {
|
||||||
|
g.errAfter = g.errAfter[1:]
|
||||||
|
return n, errTest
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasMax && g.max == 0 && g.fastErr {
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFrom(rng []byte, n int) []byte {
|
||||||
|
g := &gen{
|
||||||
|
rng: rng,
|
||||||
|
max: n,
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := io.ReadAll(g)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(n int) []byte {
|
||||||
|
return generateFrom(genRange, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writer) Write(p []byte) (int, error) {
|
||||||
|
if len(w.errAfter) > 0 && len(w.written) >= w.errAfter[0] {
|
||||||
|
w.errAfter = w.errAfter[1:]
|
||||||
|
return 0, errTest
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p) > 0 && len(w.shortAfter) > 0 && len(w.written) >= w.shortAfter[0] {
|
||||||
|
w.shortAfter = w.shortAfter[1:]
|
||||||
|
p = p[:len(p) / 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
wp := make([]byte, len(p))
|
||||||
|
copy(wp, p)
|
||||||
|
w.written = append(w.written, wp...)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
104
lib.go
Normal file
104
lib.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// alternative to bufio
|
||||||
|
// meant to be used with buffer from pool
|
||||||
|
package buffer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pool interface {
|
||||||
|
Get() ([]byte, error)
|
||||||
|
Put([]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContentWriter interface {
|
||||||
|
WriteTo(io.WriteCloser) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
|
||||||
|
// defaults to new instance created by DefaultPool()
|
||||||
|
Pool Pool
|
||||||
|
|
||||||
|
// may differ, default 512
|
||||||
|
ReadSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize with NewReader or ReaderFrom
|
||||||
|
// uninitialized reader panics
|
||||||
|
// reads from the underlying reader until the first error, but only returns an error when the buffer is empty
|
||||||
|
// once the underlying reader returned an error, it doesn't attempt to read from it anymore
|
||||||
|
// the pool yes, the reader does not support concurrent access
|
||||||
|
type Reader struct {
|
||||||
|
reader *reader
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrZeroAllocation = errors.New("zero allocation")
|
||||||
|
|
||||||
|
func DefaultPool() Pool {
|
||||||
|
return newPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoPool(allocSize int) Pool {
|
||||||
|
return noPool{allocSize: allocSize}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderFrom(in io.Reader, o Options) Reader {
|
||||||
|
if o.Pool == nil {
|
||||||
|
o.Pool = DefaultPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.ReadSize <= 0 {
|
||||||
|
o.ReadSize = 1 << 9
|
||||||
|
}
|
||||||
|
|
||||||
|
return Reader{reader: &reader{options: o, in: in}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReaderFromContent(w ContentWriter, o Options) Reader {
|
||||||
|
return Reader{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// - it returns an error only when the underlying reader returned an error and the internal buffer is empty
|
||||||
|
// - if the underlying reader returns zero read length and nil error, and the buffer is empty, it returns zero
|
||||||
|
// read length and nil error. It's up the calling code to decide how to proceed in such cases
|
||||||
|
func (r Reader) Read(p []byte) (int, error) {
|
||||||
|
return r.reader.read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy
|
||||||
|
// data and err when no delimiter found
|
||||||
|
// - when found the delimiter within max, consumes until and including the delimiter, and returns the consumed
|
||||||
|
// bytes, true, and nil error. If the underlying reader returned meanwhile a non-nil error, including EOF, it
|
||||||
|
// will be returned on subsequent reads after the internal buffer was consumed
|
||||||
|
// - when not found the delimiter within max, and the underlying reader didn't return an error, it returns zero
|
||||||
|
// length bytes, false and nil error
|
||||||
|
// - when not found the delimiter within max, and the underlying reader returned an error, it returns the
|
||||||
|
// buffered bytes, false and a nil error
|
||||||
|
// - when the buffer is empty, and the underlying reader previously returned an error, it returns zero length
|
||||||
|
// bytes, false, and the error
|
||||||
|
func (r Reader) ReadBytes(delimiter []byte, max int) ([]byte, bool, error) {
|
||||||
|
return r.reader.readBytes(delimiter, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only returns an error when the underlying reader returned an error, or the used pool returned an error
|
||||||
|
func (r Reader) ReadUTF8(max int) ([]rune, int, error) {
|
||||||
|
return r.reader.readUTF8(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// not a copy
|
||||||
|
func (r Reader) Peek(max int) ([]byte, error) {
|
||||||
|
return r.reader.peek(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// not a copy
|
||||||
|
// can be wrong after error
|
||||||
|
func (r Reader) Buffered() []byte {
|
||||||
|
return r.reader.buffered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// important that the writer must not modify the slice data, as defined in the io.Writer interface
|
||||||
|
func (r Reader) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
return r.reader.writeTo(w)
|
||||||
|
}
|
||||||
228
peek_test.go
Normal file
228
peek_test.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPeek(t *testing.T) {
|
||||||
|
t.Run("peek across segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p, err := r.Peek(2*128 + 30)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(2*128+30)) {
|
||||||
|
t.Fatal("failed to peek", len(p))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("err before and after consumed", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 12,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(18)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = r.Peek(18)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content", len(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = r.Peek(18)
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal("failed EOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("err immediately on first try to fill", func(t *testing.T) {
|
||||||
|
g := &gen{}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(18)
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal("failed EOF")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek on empty", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(18)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(18)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek multiple segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(160)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(160)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek on partially filled", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(16)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(16)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = r.Peek(48)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(48)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek on partially filled multiple segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(160)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(160)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = r.Peek(144)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(144)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek not enough available", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 12,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(18)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(12)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek not enough available multiple segments", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 144,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(214)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(144)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("peek zero", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("ivnalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
25
pool.go
Normal file
25
pool.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package buffer
|
||||||
|
|
||||||
|
type noPool struct{
|
||||||
|
allocSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
type pool struct{}
|
||||||
|
|
||||||
|
func newPool() *pool {
|
||||||
|
return &pool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p noPool) Get() ([]byte, error) {
|
||||||
|
return make([]byte, p.allocSize), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noPool) Put([]byte) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) Get() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) Put(b []byte) {
|
||||||
|
}
|
||||||
460
pool_test.go
Normal file
460
pool_test.go
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"math/rand"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pool struct {
|
||||||
|
allocSize int
|
||||||
|
alloc, free int
|
||||||
|
errAfter []int
|
||||||
|
zeroAfter []int
|
||||||
|
shortAfter []int
|
||||||
|
longAfter []int
|
||||||
|
randomSize []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p pool) allocCondition(c *[]int) bool {
|
||||||
|
if len(*c) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.alloc < (*c)[0] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = (*c)[1:]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) 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 p.allocCondition(&p.shortAfter) {
|
||||||
|
n /= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.allocCondition(&p.longAfter) {
|
||||||
|
n *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.randomSize) > 1 {
|
||||||
|
n = p.randomSize[0] + rand.Intn(p.randomSize[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.randomSize) == 1 {
|
||||||
|
n = 1 + rand.Intn(p.randomSize[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return make([]byte, n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pool) Put([]byte) {
|
||||||
|
p.free++
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoolUsage(t *testing.T) {
|
||||||
|
t.Run("allocate", func(t *testing.T) {
|
||||||
|
t.Run("read", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
p := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 9}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{allocSize: 1 << 12}
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{
|
||||||
|
allocSize: 1 << 12,
|
||||||
|
zeroAfter: []int{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{
|
||||||
|
allocSize: 1 << 9,
|
||||||
|
zeroAfter: []int{1},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{
|
||||||
|
allocSize: 1 << 12,
|
||||||
|
zeroAfter: []int{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{
|
||||||
|
allocSize: 1 << 12,
|
||||||
|
zeroAfter: []int{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(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 := &pool{
|
||||||
|
allocSize: 1 << 12,
|
||||||
|
zeroAfter: []int{0},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: p}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
_, err := r.WriteTo(&b)
|
||||||
|
if !errors.Is(err, buffer.ErrZeroAllocation) {
|
||||||
|
t.Fatal("failed to fail", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// varying segment sizes: read bytes, peek
|
||||||
|
// one byte segments: read, read bytes, read utf8, peek, write to
|
||||||
|
// pool error on allocate: read, read bytes, read utf8, peek, write to
|
||||||
|
}
|
||||||
371
read_test.go
Normal file
371
read_test.go
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"io"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRead(t *testing.T) {
|
||||||
|
t.Run("small", func(t *testing.T) {
|
||||||
|
g := &gen{max: 3}
|
||||||
|
r := buffer.ReaderFrom(g, buffer.Options{Pool: buffer.NoPool(1 << 12)})
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(3)) {
|
||||||
|
t.Fatal("output does not match", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("large", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 18}
|
||||||
|
r := buffer.ReaderFrom(g, buffer.Options{Pool: buffer.NoPool(1 << 12)})
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero first", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
|
||||||
|
var p []byte
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 0 {
|
||||||
|
t.Fatal("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
p = make([]byte, 256)
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(256)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("large with non-divisible fragments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(1 << 12 / 2),
|
||||||
|
ReadSize: 1 << 12 / 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(1<<15)) {
|
||||||
|
t.Fatal("output does not match", len(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("partial segment", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
r.Peek(30)
|
||||||
|
p := make([]byte, 60)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 60 {
|
||||||
|
t.Fatal("invalid read length", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(60)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("partial segment nth", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
r.Peek(2*128 + 30)
|
||||||
|
p := make([]byte, 2*128+60)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 2*128+60 {
|
||||||
|
t.Fatal("invalid read length", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(2*128+60)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read buffer larger than read size", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 12)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 12 {
|
||||||
|
t.Fatal("invalid read size")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(12)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read buffer larger than segment", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 192)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 192 {
|
||||||
|
t.Fatal("invalid read size")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(192)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read buffer larger than available data", func(t *testing.T) {
|
||||||
|
g := &gen{max: 256}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 384)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("invalid read size")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p[:n], generate(256)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 0 || !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal("invalid post read", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null read on empty", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 64)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 0 || err != nil {
|
||||||
|
t.Fatal("failed to handle null read", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 64 || err != nil || !bytes.Equal(p, generate(64)) {
|
||||||
|
t.Fatal("failed to recover after null read", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null read on non-empty", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{32, 32},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 32)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 32 || err != nil || !bytes.Equal(p, generate(32)) {
|
||||||
|
t.Fatal("initial read failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 0 || err != nil {
|
||||||
|
t.Fatal("failed to handle null read", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 32 || err != nil || !bytes.Equal(p, generate(64)[32:]) {
|
||||||
|
t.Fatal("failed to recover after null read", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("partial read on null read", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{32, 32},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 24)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 24 || err != nil || !bytes.Equal(p, generate(24)) {
|
||||||
|
t.Fatal("initial read failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 8 || err != nil || !bytes.Equal(p[:n], generate(32)[24:]) {
|
||||||
|
t.Fatal("failed to handle null read", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 24 || err != nil || !bytes.Equal(p, generate(56)[32:]) {
|
||||||
|
t.Fatal("failed to recover after null read", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read error without content", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{32},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 64)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 64 || err != nil {
|
||||||
|
t.Fatal("failed to read", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 0 || !errors.Is(err, errTest) {
|
||||||
|
t.Fatal("failed to process read error", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read error with content", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{32},
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 64)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if n != 64 || err != nil {
|
||||||
|
t.Fatal("failed to read", n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = r.Read(p)
|
||||||
|
if n != 0 || !errors.Is(err, errTest) {
|
||||||
|
t.Fatal("failed to process read error", n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read after error", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{32},
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(128),
|
||||||
|
ReadSize: 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []byte
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 9)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if n, err := r.Read(p); n != 9 || err != nil {
|
||||||
|
t.Fatal(n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, p...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := r.Read(p); n != 5 || err != nil {
|
||||||
|
t.Fatal(n, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, p[:5]...)
|
||||||
|
if !bytes.Equal(result, generate(32)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
752
readbytes_test.go
Normal file
752
readbytes_test.go
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadBytes(t *testing.T) {
|
||||||
|
t.Run("find", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("failed to generate right content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok || len(b) != 0 {
|
||||||
|
t.Fatal("failed to not find delimiter")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find across segments", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(14),
|
||||||
|
ReadSize: 14,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to find delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find across multiple segments", func(t *testing.T) {
|
||||||
|
d := generateFrom([]byte("123"), 12)
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{6},
|
||||||
|
customContent: map[int][]byte{6: d},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes(d, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to find delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(6), d...)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not across segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(15),
|
||||||
|
ReadSize: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not across multiple segments", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(15),
|
||||||
|
ReadSize: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not due to max", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("8"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("12")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial and then full", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12, 18},
|
||||||
|
customContent: map[int][]byte{
|
||||||
|
12: []byte("12"),
|
||||||
|
18: []byte("123"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 16)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
check := append(
|
||||||
|
append(generate(12), append([]byte("12"), generate(18)[14:]...)...),
|
||||||
|
[]byte("123")...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !bytes.Equal(b, check) {
|
||||||
|
t.Fatal("invalid content", len(b), string(b), len(check), string(check))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial across segments", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{7},
|
||||||
|
customContent: map[int][]byte{7: []byte("12")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial across multiple segments", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{7},
|
||||||
|
customContent: map[int][]byte{7: []byte("1234567890")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("1234567890123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial across segments and then full", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{7, 15},
|
||||||
|
customContent: map[int][]byte{
|
||||||
|
7: []byte("12"),
|
||||||
|
15: []byte("123"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
check := append(
|
||||||
|
append(generate(7), append([]byte("12"), generate(15)[9:]...)...),
|
||||||
|
[]byte("123")...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !bytes.Equal(b, check) {
|
||||||
|
t.Fatal("invalid content", len(b), string(b), len(check), string(check))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial across multiple segments and then full", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{7, 22},
|
||||||
|
customContent: map[int][]byte{
|
||||||
|
7: []byte("1234567890"),
|
||||||
|
22: []byte("1234567890123"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("1234567890123"), 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("invalid delimiter found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("1234567890123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
check := append(
|
||||||
|
append(append(generate(7), []byte("1234567890")...), generate(22)[17:]...),
|
||||||
|
[]byte("1234567890123")...,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !bytes.Equal(b, check) {
|
||||||
|
t.Fatal("invalid content", len(b), string(b), len(check), string(check))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial due to max", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 14)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find partial due to max and then full", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 14)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error before found", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{8},
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(8)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal("failed to fail", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error when found", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{15},
|
||||||
|
fastErr: true,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter", len(b), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("unexpected delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(16)[15:]) {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("unexpected delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error after found", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{18},
|
||||||
|
fastErr: true,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter", len(b), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("unexpected delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(24)[15:]) {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("unexpected delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content", len(b), string(b))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null delimiter", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes(nil, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("failed to find delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("failed to generate right content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null read", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{8, 8},
|
||||||
|
customContentAfter: []int{12},
|
||||||
|
customContent: map[int][]byte{12: []byte("123")},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(8),
|
||||||
|
ReadSize: 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, ok, err = r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, append(generate(12), []byte("123")...)) {
|
||||||
|
t.Fatal("failed to generate right content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not more than max", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 256,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(256),
|
||||||
|
ReadSize: 256,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(64)) {
|
||||||
|
t.Fatal("failed to generate right content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("find not none consumed", func(t *testing.T) {
|
||||||
|
g := &gen{}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, ok, err := r.ReadBytes([]byte("123"), 64)
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
t.Fatal("failed to find delimiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) != 0 {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// conditions:
|
||||||
|
// - A0: error before read
|
||||||
|
// - A1: error during read
|
||||||
|
// - B0: allocation error
|
||||||
|
// - B1: read error
|
||||||
|
// - B2: eof
|
||||||
|
// - C0: error right before delimiter
|
||||||
|
// - C1: error during delimiter
|
||||||
|
// - C2: error right after full delimiter
|
||||||
|
// - D0: error before max
|
||||||
|
// - D1: error right at max
|
||||||
|
// - D2: error after max
|
||||||
|
// - E0: error at zero segment position
|
||||||
|
// - E1: error at not first segment boundary
|
||||||
|
// - E2: error in first segment
|
||||||
|
// - E3: error in subsequent segment
|
||||||
|
// - F0: error at zero data position
|
||||||
|
// - F1: error at not zero data position
|
||||||
|
// - F3: error at last data position
|
||||||
|
// - G0: less than max buffered
|
||||||
|
// - G1: exactly max buffered
|
||||||
|
// - G2: more than max buffered
|
||||||
|
// - H0: max at first segment start
|
||||||
|
// - H1: max at segment boundary
|
||||||
|
// - H2: max in first segment
|
||||||
|
// - H3: max in subsequent segment
|
||||||
|
// - I0: delimiter in newly read
|
||||||
|
// - I1: delimiter in buffered
|
||||||
|
// - J0: delimiter before max
|
||||||
|
// - J1: delimiter right up to max
|
||||||
|
// - J2: delimiter over max
|
||||||
|
// - J3: delimiter right after max
|
||||||
|
// - J4: delimiter after max
|
||||||
|
// - K0: delimiter at zero segment position
|
||||||
|
// - K1: delimiter at subsequent segment boundary
|
||||||
|
// - K2: delimiter within segment
|
||||||
|
// - L0: delimiter at zero data position
|
||||||
|
// - L1: delimiter at within buffered data
|
||||||
|
// - L2: delimiter at the end of buffered data
|
||||||
|
// - L3: partial delimiter at the end of buffered data
|
||||||
|
// - L4: delimiter right after buffered data
|
||||||
|
// - M0: delimiter found
|
||||||
|
// - M1: delimiter not found
|
||||||
|
//
|
||||||
|
// variations:
|
||||||
|
// - A0B0...
|
||||||
|
}
|
||||||
517
reader.go
Normal file
517
reader.go
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
package buffer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type reader struct {
|
||||||
|
options Options
|
||||||
|
in io.Reader
|
||||||
|
segments [][]byte
|
||||||
|
offset int
|
||||||
|
len int
|
||||||
|
lastSegStart int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) fillSegment(n int) (fn int) {
|
||||||
|
for {
|
||||||
|
if r.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if fn >= n {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seg := r.segments[len(r.segments)-1]
|
||||||
|
start := r.offset + r.len - r.lastSegStart
|
||||||
|
end := start + n - fn
|
||||||
|
if end-start < r.options.ReadSize {
|
||||||
|
end = start + r.options.ReadSize
|
||||||
|
}
|
||||||
|
|
||||||
|
if end > len(seg) {
|
||||||
|
end = len(seg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if end == start {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rn, err := r.in.Read(seg[start:end])
|
||||||
|
if rn == 0 && err == nil {
|
||||||
|
rn, err = r.in.Read(seg[start:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
if rn == 0 && err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fn += rn
|
||||||
|
r.len += rn
|
||||||
|
r.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) allocate() {
|
||||||
|
segment, err := r.options.Pool.Get()
|
||||||
|
if err != nil {
|
||||||
|
r.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segment) == 0 {
|
||||||
|
r.err = ErrZeroAllocation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.segments) > 0 {
|
||||||
|
r.lastSegStart += len(r.segments[len(r.segments)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
r.segments = append(r.segments, segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) fill(n int) {
|
||||||
|
for {
|
||||||
|
if r.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.len >= n {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.segments) == 0 || r.offset+r.len == r.lastSegStart+len(r.segments[len(r.segments)-1]) {
|
||||||
|
r.allocate()
|
||||||
|
if r.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := r.fillSegment(n)
|
||||||
|
if fn == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reader) copy(p []byte) (n int) {
|
||||||
|
var seg, segStart int
|
||||||
|
offset := r.offset
|
||||||
|
for {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if seg == len(r.segments) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := r.offset + r.len - segStart
|
||||||
|
if rl > len(r.segments[seg]) {
|
||||||
|
rl = len(r.segments[seg])
|
||||||
|
}
|
||||||
|
|
||||||
|
ni := copy(p, r.segments[seg][offset:rl])
|
||||||
|
n += ni
|
||||||
|
p = p[ni:]
|
||||||
|
segStart += len(r.segments[seg])
|
||||||
|
seg++
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) consume(n int) {
|
||||||
|
r.offset += n
|
||||||
|
r.len -= n
|
||||||
|
for {
|
||||||
|
if len(r.segments) <= 1 {
|
||||||
|
if r.len == 0 {
|
||||||
|
r.offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.offset < len(r.segments[0]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.offset -= len(r.segments[0])
|
||||||
|
if len(r.segments) > 1 {
|
||||||
|
r.lastSegStart -= len(r.segments[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
r.options.Pool.Put(r.segments[0])
|
||||||
|
r.segments = r.segments[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) free() {
|
||||||
|
for {
|
||||||
|
if len(r.segments) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.options.Pool.Put(r.segments[0])
|
||||||
|
r.segments = r.segments[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reader) findSegmentUp(i int) (int, int) {
|
||||||
|
var seg, segStart int
|
||||||
|
for {
|
||||||
|
if r.offset+i >= segStart && r.offset+i < segStart+len(r.segments[seg]) {
|
||||||
|
return seg, segStart
|
||||||
|
}
|
||||||
|
|
||||||
|
segStart += len(r.segments[seg])
|
||||||
|
seg++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reader) findSegmentDown(i int) (int, int) {
|
||||||
|
seg := len(r.segments) - 1
|
||||||
|
segStart := r.lastSegStart
|
||||||
|
for {
|
||||||
|
if r.offset+i >= segStart && r.offset+i < segStart+len(r.segments[seg]) {
|
||||||
|
return seg, segStart
|
||||||
|
}
|
||||||
|
|
||||||
|
seg--
|
||||||
|
segStart -= len(r.segments[seg])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func match(p0, p1 []byte) (bool, int) {
|
||||||
|
var i int
|
||||||
|
for {
|
||||||
|
if len(p0) == i || len(p1) == i {
|
||||||
|
return true, len(p0) - len(p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p0[i] != p1[i] {
|
||||||
|
return false, len(p0) - len(p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) fillToDelimiter(delimiter []byte, max int) (int, bool) {
|
||||||
|
var i, seg, segStart int
|
||||||
|
d := delimiter
|
||||||
|
for {
|
||||||
|
if i+len(d) > max {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.len < i+len(d) {
|
||||||
|
clen := r.len
|
||||||
|
r.fill(i + len(d))
|
||||||
|
if r.len == clen {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.offset+i >= segStart+len(r.segments[seg]) {
|
||||||
|
segStart += len(r.segments[seg])
|
||||||
|
seg++
|
||||||
|
}
|
||||||
|
|
||||||
|
first := r.offset + i - segStart
|
||||||
|
last := first + len(d)
|
||||||
|
if last > len(r.segments[seg]) {
|
||||||
|
last = len(r.segments[seg])
|
||||||
|
}
|
||||||
|
|
||||||
|
m, c := match(d, r.segments[seg][first:last])
|
||||||
|
if !m {
|
||||||
|
i += 1 - len(delimiter) + len(d)
|
||||||
|
seg, segStart = r.findSegmentDown(i)
|
||||||
|
d = delimiter
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c > 0 {
|
||||||
|
c = len(d) - c
|
||||||
|
i += c
|
||||||
|
d = d[c:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return i + len(d), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reader) view(i, max int) []byte {
|
||||||
|
if len(r.segments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if max == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= r.len {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+max > r.len {
|
||||||
|
max = r.len - i
|
||||||
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
seg, segStart := r.findSegmentUp(i)
|
||||||
|
for {
|
||||||
|
if len(b) == max {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
start := r.offset - segStart + i
|
||||||
|
l := start + max - len(b)
|
||||||
|
if l > len(r.segments[seg]) {
|
||||||
|
l = len(r.segments[seg])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) == 0 {
|
||||||
|
b = r.segments[seg][start:l]
|
||||||
|
} else {
|
||||||
|
b = append(b, r.segments[seg][start:l]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
i += l - start
|
||||||
|
if r.offset+i == segStart+len(r.segments[seg]) {
|
||||||
|
segStart += len(r.segments[seg])
|
||||||
|
seg++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) read(p []byte) (int, error) {
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - consider optimizing such that it reads to and copies from the existing segment
|
||||||
|
// - pool.put errors are not really reader errors
|
||||||
|
// - pool.get errors are not really reader errors, if the reader can still finish its task
|
||||||
|
// - consider optimizing other places
|
||||||
|
// - first implement the pool test cases
|
||||||
|
// - the size parameter for the pool contradicts the pool functionality
|
||||||
|
// - defining the segment sizes for the pool conflicts with the buffer options
|
||||||
|
// - is the read size even necessary? Doesn't it always make sense to fill up the current segment?
|
||||||
|
r.fill(len(p))
|
||||||
|
n := r.copy(p)
|
||||||
|
r.consume(n)
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
r.free()
|
||||||
|
if n == 0 {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) readBytes(delimiter []byte, max int) ([]byte, bool, error) {
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
return nil, false, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(delimiter) == 0 {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
p []byte
|
||||||
|
n int
|
||||||
|
)
|
||||||
|
|
||||||
|
l, ok := r.fillToDelimiter(delimiter, max)
|
||||||
|
if ok {
|
||||||
|
p = make([]byte, l)
|
||||||
|
n = r.copy(p)
|
||||||
|
r.consume(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok && r.err != nil {
|
||||||
|
l = r.len
|
||||||
|
if l > max {
|
||||||
|
l = max
|
||||||
|
}
|
||||||
|
|
||||||
|
p = make([]byte, l)
|
||||||
|
n = r.copy(p)
|
||||||
|
r.consume(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
r.free()
|
||||||
|
if n == 0 {
|
||||||
|
return nil, ok, r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) readUTF8(max int) ([]rune, int, error) {
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
return nil, 0, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
runes []rune
|
||||||
|
n int
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if len(runes) == max {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
b []byte
|
||||||
|
nullRead bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
b = r.view(n, 4)
|
||||||
|
if len(b) == 4 || utf8.FullRune(b) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
clen := r.len
|
||||||
|
r.fill(4 + 2*(max-len(runes)))
|
||||||
|
nullRead = r.len == clen && r.err == nil
|
||||||
|
if nullRead {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nullRead || len(b) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
r, s := utf8.DecodeRune(b)
|
||||||
|
if r == utf8.RuneError && s == 1 && len(runes) == 0 {
|
||||||
|
n = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == utf8.RuneError && s == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
runes = append(runes, r)
|
||||||
|
n += s
|
||||||
|
}
|
||||||
|
|
||||||
|
r.consume(n)
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
r.free()
|
||||||
|
if n == 0 {
|
||||||
|
return nil, 0, r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return runes, n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) peek(max int) ([]byte, error) {
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
return nil, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.fill(max)
|
||||||
|
v := r.view(0, max)
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
r.free()
|
||||||
|
if len(v) == 0 {
|
||||||
|
return v, r.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reader) buffered() []byte {
|
||||||
|
return r.view(0, r.len)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reader) writeTo(w io.Writer) (int64, error) {
|
||||||
|
if errors.Is(r.err, io.EOF) && r.len == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
return 0, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
n int64
|
||||||
|
werr error
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if r.len == 0 {
|
||||||
|
if r.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.segments) == 0 {
|
||||||
|
r.allocate()
|
||||||
|
if r.err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn := r.fillSegment(len(r.segments[len(r.segments) - 1]))
|
||||||
|
if fn == 0 && r.err == nil {
|
||||||
|
return n, io.ErrNoProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wl := len(r.segments[0]) - r.offset
|
||||||
|
if wl > r.len {
|
||||||
|
wl = r.len
|
||||||
|
}
|
||||||
|
|
||||||
|
ni, err := w.Write(r.segments[0][r.offset : r.offset+wl])
|
||||||
|
r.consume(ni)
|
||||||
|
n += int64(ni)
|
||||||
|
if err != nil {
|
||||||
|
werr = err
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ni < wl {
|
||||||
|
werr = io.ErrShortWrite
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.err != nil && r.len == 0 {
|
||||||
|
r.free()
|
||||||
|
}
|
||||||
|
|
||||||
|
if werr != nil {
|
||||||
|
return n, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.err != nil && !errors.Is(r.err, io.EOF) && r.len == 0 {
|
||||||
|
return n, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
232
readutf8_test.go
Normal file
232
readutf8_test.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadUTF8(t *testing.T) {
|
||||||
|
t.Run("read all after error", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
rng: utf8Range,
|
||||||
|
max: 24,
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n0, err := r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 12 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8Range, n0)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
runes, n1, err := r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 3 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8Range, n0+n1)[n0:]) {
|
||||||
|
t.Fatal("invalid content", string(runes), string(generateFrom(utf8Range, n0+n1)[n0:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
runes, n2, err := r.ReadUTF8(12)
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n2 != 0 {
|
||||||
|
t.Fatal("unexpected consumption")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 0 {
|
||||||
|
t.Fatal("unexpected read", len(runes))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ascii", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n, err := r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 12 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generate(n)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("long within segment", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
rng: utf8W2Range,
|
||||||
|
max: 1 << 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n, err := r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 12 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8W2Range, n)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("long across segments", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
rng: utf8W2Range,
|
||||||
|
max: 1 << 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(9)}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n, err := r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 12 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8W2Range, n)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null read", func(t *testing.T) {
|
||||||
|
const numRunes = 6
|
||||||
|
nullReadAfter := len(string([]rune(string(utf8W2Range))[:numRunes]))
|
||||||
|
g := &gen{
|
||||||
|
rng: utf8W2Range,
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{nullReadAfter, nullReadAfter},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(nullReadAfter),
|
||||||
|
ReadSize: nullReadAfter,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, _, err := r.ReadUTF8(numRunes - 2) // -2 for min read in readUTF8
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != numRunes-2 {
|
||||||
|
t.Fatal("short read", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8W2Range, nullReadAfter-4)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
runes, _, err = r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 2 {
|
||||||
|
t.Fatal("short read 2", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8W2Range, nullReadAfter)[nullReadAfter-4:]) {
|
||||||
|
t.Fatal("invalid content 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
runes, _, err = r.ReadUTF8(12)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 12 {
|
||||||
|
t.Fatal("short read 3", len(runes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8W2Range, 3*nullReadAfter)[nullReadAfter:]) {
|
||||||
|
t.Fatal("invalid content 3")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("broken unicode at the end", func(t *testing.T) {
|
||||||
|
brokenRange := []byte{0xc3, 0xc3, 0xc3}
|
||||||
|
g := &gen{
|
||||||
|
rng: utf8Range,
|
||||||
|
max: len(utf8Range) + len(brokenRange),
|
||||||
|
customContentAfter: []int{len(utf8Range)},
|
||||||
|
customContent: map[int][]byte{len(utf8Range): brokenRange},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n, err := r.ReadUTF8(24)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(runes) != string(generateFrom(utf8Range, n)) {
|
||||||
|
t.Fatal("invalid content", string(runes), string(generateFrom(utf8Range, n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len([]byte(string(runes))) != len(utf8Range) {
|
||||||
|
t.Fatal("invalid number of bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
runes, n, err = r.ReadUTF8(24)
|
||||||
|
if len(runes) != 0 || n != 1 || err != nil {
|
||||||
|
t.Fatal("failed to read out broken end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runes, n, err = r.ReadUTF8(24)
|
||||||
|
if len(runes) != 0 || n != 0 || !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal("failed to read EOF", len(runes), n, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("immediate err", func(t *testing.T) {
|
||||||
|
g := &gen{rng: utf8Range}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
runes, n, err := r.ReadUTF8(12)
|
||||||
|
if !errors.Is(err, io.EOF) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 0 {
|
||||||
|
t.Fatal("unexpected read", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(runes) != 0 {
|
||||||
|
t.Fatal("unexpected content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
285
writeto_test.go
Normal file
285
writeto_test.go
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package buffer_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"code.squareroundforest.org/arpio/buffer"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteTo(t *testing.T) {
|
||||||
|
t.Run("write out from zero", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
n, err := r.WriteTo(&b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 1 << 15 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b.Bytes(), generate(1 << 15)) {
|
||||||
|
t.Fatal("content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("write out from started", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 256)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("invalid read count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p, generate(256)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
n64, err := r.WriteTo(&b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n64 != 1 << 15 - 256 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b.Bytes(), generate(1 << 15)[256:]) {
|
||||||
|
t.Fatal("content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after EOF", func(t *testing.T) {
|
||||||
|
g := &gen{max: 256}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(256)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
n, err := r.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf.Bytes(), generate(256)) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after err", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{256},
|
||||||
|
fastErr: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{
|
||||||
|
Pool: buffer.NoPool(64),
|
||||||
|
ReadSize: 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
b, err := r.Peek(512)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b, generate(256)) {
|
||||||
|
t.Fatal("invalid content", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
n, err := r.WriteTo(&buf)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal("failed to test with the right error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(buf.Bytes(), generate(256)) {
|
||||||
|
t.Fatal("invalid content", 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after eof empty", func(t *testing.T) {
|
||||||
|
g := &gen{max: 256}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 512)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n != 256 {
|
||||||
|
t.Fatal("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p[:n], generate(256)) {
|
||||||
|
t.Fatal("invalid content 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
n64, err := r.WriteTo(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n64 != 0 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.Len() != 0 {
|
||||||
|
t.Fatal("invalid content 2")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("after error empty", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{256},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
p := make([]byte, 1 << 15)
|
||||||
|
n, err := r.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 256 {
|
||||||
|
t.Fatal("invalid length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(p[:n], generate(1 << 15)[:n]) {
|
||||||
|
t.Fatal("invalid content 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
n64, err := r.WriteTo(&buf)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n64 != 0 {
|
||||||
|
t.Fatal("write count")
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.Len() != 0 {
|
||||||
|
t.Fatal("invalid content 2")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("null read on fill", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
nullReadAfter: []int{256, 256},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
n, err := r.WriteTo(&b)
|
||||||
|
if !errors.Is(err, io.ErrNoProgress) {
|
||||||
|
t.Fatal("failed to fail with the right error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 256 {
|
||||||
|
t.Fatal("not enough written")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b.Bytes(), generate(1 << 15)[:n]) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("err during fill", func(t *testing.T) {
|
||||||
|
g := &gen{
|
||||||
|
max: 1 << 15,
|
||||||
|
errAfter: []int{256},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(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 < 256 {
|
||||||
|
t.Fatal("not enough written")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(b.Bytes(), generate(1 << 15)[:n]) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("write error", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
w := &writer{errAfter: []int{256}}
|
||||||
|
n, err := r.WriteTo(w)
|
||||||
|
if !errors.Is(err, errTest) {
|
||||||
|
t.Fatal("failed to fail with the right error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 256 {
|
||||||
|
t.Fatal("not enough written")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(w.written, generate(1 << 15)[:n]) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("short write", func(t *testing.T) {
|
||||||
|
g := &gen{max: 1 << 15}
|
||||||
|
o := buffer.Options{Pool: buffer.NoPool(1 << 12)}
|
||||||
|
r := buffer.ReaderFrom(g, o)
|
||||||
|
w := &writer{shortAfter: []int{256}}
|
||||||
|
n, err := r.WriteTo(w)
|
||||||
|
if !errors.Is(err, io.ErrShortWrite) {
|
||||||
|
t.Fatal("failed to fail with the right error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 256 {
|
||||||
|
t.Fatal("not enough written")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(w.written, generate(1 << 15)[:n]) {
|
||||||
|
t.Fatal("invalid content")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user