commit 3c238b52489ff7ea0ef4ac6cff5c7808edd129f5 Author: Arpad Ryszka Date: Tue Feb 17 16:58:00 2026 +0100 init repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf0f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.cover diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e98dea1 --- /dev/null +++ b/Makefile @@ -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 diff --git a/buffered_test.go b/buffered_test.go new file mode 100644 index 0000000..cc4f151 --- /dev/null +++ b/buffered_test.go @@ -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)) + } + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3fd50f4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.squareroundforest.org/arpio/buffer + +go 1.25.6 diff --git a/io_test.go b/io_test.go new file mode 100644 index 0000000..ee86725 --- /dev/null +++ b/io_test.go @@ -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 +} diff --git a/lib.go b/lib.go new file mode 100644 index 0000000..47a980f --- /dev/null +++ b/lib.go @@ -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) +} diff --git a/license b/license new file mode 100644 index 0000000..e69de29 diff --git a/peek_test.go b/peek_test.go new file mode 100644 index 0000000..8fdc5f3 --- /dev/null +++ b/peek_test.go @@ -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") + } + }) +} diff --git a/pool.go b/pool.go new file mode 100644 index 0000000..9d22665 --- /dev/null +++ b/pool.go @@ -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) { +} diff --git a/pool_test.go b/pool_test.go new file mode 100644 index 0000000..a2c95bd --- /dev/null +++ b/pool_test.go @@ -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 +} diff --git a/read_test.go b/read_test.go new file mode 100644 index 0000000..8d19054 --- /dev/null +++ b/read_test.go @@ -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") + } + }) +} diff --git a/readbytes_test.go b/readbytes_test.go new file mode 100644 index 0000000..2e658ae --- /dev/null +++ b/readbytes_test.go @@ -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... +} diff --git a/reader.go b/reader.go new file mode 100644 index 0000000..576fcf0 --- /dev/null +++ b/reader.go @@ -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 +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/readutf8_test.go b/readutf8_test.go new file mode 100644 index 0000000..40e5e88 --- /dev/null +++ b/readutf8_test.go @@ -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") + } + }) +} diff --git a/writeto_test.go b/writeto_test.go new file mode 100644 index 0000000..d6ed452 --- /dev/null +++ b/writeto_test.go @@ -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") + } + }) +}