diff --git a/buffered_test.go b/buffered_test.go index cc4f151..fc358a8 100644 --- a/buffered_test.go +++ b/buffered_test.go @@ -1,172 +1,151 @@ package buffer_test import ( - "testing" - "code.squareroundforest.org/arpio/buffer" "bytes" + "code.squareroundforest.org/arpio/buffer" + "testing" ) 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") - } - }) + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(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 := cr(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, - } + 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) - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + b, err := r.Peek(18) + if err != nil { + t.Fatal(err) + } - if !bytes.Equal(b, generate(12)) { - t.Fatal("invalid content") - } + if !bytes.Equal(b, generate(12)) { + t.Fatal("invalid content") + } - b = r.Buffered() - 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, - } + t.Run("all buffered", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(64)} + r := cr(g, o) + b, err := r.Peek(48) + if err != nil { + t.Fatal(err) + } - 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)) + } - if !bytes.Equal(b, generate(48)) { - t.Fatal("invalid content 1", len(b)) - } + b = r.Buffered() + if !bytes.Equal(b, generate(64)) { + t.Fatal("invalid content 2", 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)} + r := cr(g, o) + b, err := r.Peek(144) + if err != nil { + t.Fatal(err) + } - t.Run("buffered across segments", func(t *testing.T) { - g := &gen{max: 1 << 15} - o := buffer.Options{ - Pool: buffer.NoPool(64), - ReadSize: 16, - } + if !bytes.Equal(b, generate(144)) { + t.Fatal("invalid content 1", len(b)) + } - r := buffer.ReaderFrom(g, o) - b, err := r.Peek(144) - if err != nil { - t.Fatal(err) - } + b = r.Buffered() + if !bytes.Equal(b, generate(192)) { + t.Fatal("invalid content 2", len(b)) + } + }) - if !bytes.Equal(b, generate(144)) { - t.Fatal("invalid content 1", len(b)) - } + t.Run("buffered mid segment", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(g, o) + b := make([]byte, 32) + n, err := r.Read(b) + if err != nil { + t.Fatal(err) + } - b = r.Buffered() - if !bytes.Equal(b, generate(192)) { - t.Fatal("invalid content 2", len(b)) - } - }) + if n != 32 { + t.Fatal("invalid read length", n) + } - t.Run("buffered mid segment", func(t *testing.T) { - g := &gen{max: 1 << 15} - o := buffer.Options{ - Pool: buffer.NoPool(128), - ReadSize: 64, - } + if !bytes.Equal(b, generate(32)) { + t.Fatal("invalid content 1") + } - r := buffer.ReaderFrom(g, o) - b := make([]byte, 32) - n, err := r.Read(b) - if err != nil { - t.Fatal(err) - } + b = r.Buffered() + if !bytes.Equal(b, generate(128)[32:]) { + t.Fatal("invalid content 2") + } + }) - if n != 32 { - t.Fatal("invalid read length", n) - } + t.Run("buffered mid segment across segments", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(g, o) + b, err := r.Peek(288) + if err != nil { + t.Fatal(err) + } - if !bytes.Equal(b, generate(32)) { - t.Fatal("invalid content") - } + if !bytes.Equal(b, generate(288)) { + t.Fatal("invalid content 1") + } - b = r.Buffered() - if !bytes.Equal(b, generate(64)[32:]) { - t.Fatal("invalid content") - } - }) + b = r.Buffered() + if !bytes.Equal(b, generate(384)) { + t.Fatal("invalid content 2", len(b)) + } + }) - 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, - } + t.Run("zero buffered mid segment", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(64)} + r := cr(g, o) + b := make([]byte, 64) + n, err := r.Read(b) + if err != nil { + t.Fatal(err) + } - r := buffer.ReaderFrom(g, o) - b := make([]byte, 288) - n, err := r.Read(b) - if err != nil { - t.Fatal(err) - } + if n != 64 { + t.Fatal("invalid read length", n) + } - if n != 288 { - t.Fatal("invalid read length", n) - } + if !bytes.Equal(b, generate(64)) { + t.Fatal("invalid content 1") + } - 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)) - } - }) + b = r.Buffered() + if len(b) != 0 { + t.Fatal("invalid content 2", len(b)) + } + }) + }) + } } diff --git a/content.go b/content.go new file mode 100644 index 0000000..6ffad0d --- /dev/null +++ b/content.go @@ -0,0 +1,120 @@ +package buffer + +// TODO: +// - the write internally can get a max in the request, and this way multiple writes can be executed without +// synchronization steps in between +// - the current segment can be passed in to the writer to copy to +// - additional segments can be requested from the writer if necessary +// - a single channel with buffer=1 can be used to exchange +// - the message can be called 'syncMessage' +// - filter out the errors coming from the writer, but originated from the reader. Can errors.Is used for this? +// If yes, replace the captured error +// - check if the existing stdlib interface is good for the content writer + +import ( + "errors" + "io" +) + +type syncMessage struct { + p []byte + n int + err error +} + +type contentWriter struct { + w chan<- syncMessage + r <-chan syncMessage +} + +type content struct { + wrt io.WriterTo + started bool + w chan syncMessage + r chan syncMessage +} + +func mkcontent(wrt io.WriterTo) *content { + return &content{ + wrt: wrt, + w: make(chan syncMessage, 1), + r: make(chan syncMessage, 1), + } +} + +func (w contentWriter) Write(p []byte) (int, error) { + var n int + for { + if len(p) == 0 { + return n, nil + } + + sm, ok := <-w.r + if !ok { + return n, ErrContentAbort + } + + ni := copy(sm.p, p) + n += ni + p = p[ni:] + w.w <- syncMessage{n: ni} + } +} + +func (c *content) writeTo() { + w := contentWriter{ + w: c.r, + r: c.w, + } + + var sm syncMessage + _, err := c.wrt.WriteTo(w) + if err != nil { + sm = syncMessage{err: err} + } + + if err == nil { + sm = syncMessage{err: io.EOF} + } + + w.w <- sm + close(w.w) +} + +func (c *content) Read(p []byte) (int, error) { + if !c.started { + go c.writeTo() + c.started = true + } + + c.w <- syncMessage{p: p} + sm := <-c.r + return sm.n, sm.err +} + +func (c *content) close() error { + if !c.started { + return nil + } + + // there can be only zero or one messages to receive, but we need to wait for the channel be closed by + // writeTo: + var errs []error + close(c.w) + for sm := range c.r { + + // not using errors.Is, because the writer logic may have combined the abort error with another + // user logic error, that needs to be reported: + if sm.err == ErrContentAbort { + continue + } + + if sm.err == io.EOF { + continue + } + + errs = append(errs, sm.err) + } + + return errors.Join(errs...) +} diff --git a/content_test.go b/content_test.go new file mode 100644 index 0000000..627876a --- /dev/null +++ b/content_test.go @@ -0,0 +1,341 @@ +package buffer + +import ( + "testing" + "io" + "errors" +) + +type testPool struct { + size int + failAfter []int + count int +} + +var ( + errTest = errors.New("test error") + errTest2 = errors.New("test error 2") +) + +func (p *testPool) Get() ([]byte, error) { + defer func() { + p.count++ + }() + + if len(p.failAfter) > 0 && p.count == p.failAfter[0] { + p.failAfter = p.failAfter[1:] + return nil, errTest + } + + return make([]byte, p.size), nil +} + +func (p *testPool) Put([]byte) {} + +func TestContent(t *testing.T) { + t.Run("eof", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + var n int64 + for i := 0; i < 3; i++ { + ni, err := w.Write([]byte("123456789")[i * 3:i * 3 + 3]) + n += int64(ni) + if err != nil { + return n, err + } + } + + return n, nil + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + for i := 0; i < 3; i++ { + n, err := r.Read(b) + if n != 3 || err != nil { + t.Fatal(n, err) + } + + if string(b) != "123456789"[i * 3:i * 3 + 3] { + t.Fatal(string(b)) + } + } + + n, err := r.Read(b) + if n != 0 || !errors.Is(err, io.EOF) { + t.Fatal(n, err) + } + }) + + t.Run("eof right away", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + return 0, nil + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + n, err := r.Read(b) + if n != 0 || !errors.Is(err, io.EOF) { + t.Fatal(n, err) + } + }) + + t.Run("writer error", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + var n int64 + for i := 0; i < 3; i++ { + ni, err := w.Write([]byte("123456789")[i * 3:i * 3 + 3]) + n += int64(ni) + if err != nil { + return n, err + } + } + + return n, errTest + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + for i := 0; i < 3; i++ { + n, err := r.Read(b) + if n != 3 || err != nil { + t.Fatal(n, err) + } + + if string(b) != "123456789"[i * 3:i * 3 + 3] { + t.Fatal(string(b)) + } + } + + n, err := r.Read(b) + if n != 0 || !errors.Is(err, errTest) { + t.Fatal(n, err) + } + }) + + t.Run("writer error right away", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + return 0, errTest + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + n, err := r.Read(b) + if n != 0 || !errors.Is(err, errTest) { + t.Fatal(n, err) + } + }) + + t.Run("abort", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + var n int64 + for i := 0; i < 3; i++ { + ni, err := w.Write([]byte("123456789")[i * 3:i * 3 + 3]) + n += int64(ni) + if err != nil { + return n, err + } + } + + return n, nil + }) + + p := &testPool{ + size: 2, + failAfter: []int{1}, + } + + o := Options{Pool: p} + r := BufferedContent(c, o) + b, ok, err := r.ReadBytes([]byte("67"), 12) + if string(b) != "12" /* segment size og 2 by the pool */ || ok || err != nil { + t.Fatal(string(b), ok, err) + } + + b, ok, err = r.ReadBytes([]byte("67"), 12) + if len(b) != 0 || ok || !errors.Is(err, errTest) { + t.Fatal(string(b), ok, err) + } + }) + + t.Run("abort right away", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + var n int64 + for i := 0; i < 3; i++ { + ni, err := w.Write([]byte("123456789")[i * 3:i * 3 + 3]) + n += int64(ni) + if err != nil { + return n, err + } + } + + return n, nil + }) + + p := &testPool{ + size: 2, + failAfter: []int{0}, + } + + o := Options{Pool: p} + r := BufferedContent(c, o) + b, ok, err := r.ReadBytes([]byte("67"), 12) + if len(b) != 0 || ok || !errors.Is(err, errTest) { + t.Fatal(string(b), ok, err) + } + }) + + t.Run("close when implementation ignores writer errors", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + w.Write([]byte("123")) + w.Write([]byte("456")) + w.Write([]byte("123")) + return 0, nil + }) + + p := &testPool{ + size: 2, + failAfter: []int{1}, + } + + o := Options{Pool: p} + r := BufferedContent(c, o) + b, ok, err := r.ReadBytes([]byte("67"), 12) + if string(b) != "12" /* segment size og 2 by the pool */ || ok || err != nil { + t.Fatal(string(b), ok, err) + } + + b, ok, err = r.ReadBytes([]byte("67"), 12) + if len(b) != 0 || ok || !errors.Is(err, errTest) { + t.Fatal(string(b), ok, err) + } + }) + + t.Run("zero write", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + w.Write([]byte("123")) + w.Write(nil) + w.Write([]byte("456")) + w.Write([]byte("789")) + return 0, nil + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + for i := 0; i < 3; i++ { + n, err := r.Read(b) + if n != 3 || err != nil { + t.Fatal(n, err) + } + + if string(b) != "123456789"[i * 3:i * 3 + 3] { + t.Fatal(string(b)) + } + } + + n, err := r.Read(b) + if n != 0 || !errors.Is(err, io.EOF) { + t.Fatal(n, err) + } + }) + + t.Run("zero write right away", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + w.Write(nil) + w.Write([]byte("123")) + w.Write([]byte("456")) + w.Write([]byte("789")) + return 0, nil + }) + + p := &testPool{size: 2} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + for i := 0; i < 3; i++ { + n, err := r.Read(b) + if n != 3 || err != nil { + t.Fatal(n, err) + } + + if string(b) != "123456789"[i * 3:i * 3 + 3] { + t.Fatal(string(b)) + } + } + + n, err := r.Read(b) + if n != 0 || !errors.Is(err, io.EOF) { + t.Fatal(n, err) + } + }) + + t.Run("custom error", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + var n int64 + for i := 0; i < 3; i++ { + ni, err := w.Write([]byte("123456789")[i * 3:i * 3 + 3]) + n += int64(ni) + if err != nil { + return n, err + } + } + + return n, errTest + }) + + p := &testPool{size: 3} + o := Options{Pool: p} + r := BufferedContent(c, o) + b := make([]byte, 3) + for i := 0; i < 3; i++ { + n, err := r.Read(b) + if n != 3 || err != nil { + t.Fatal(n, err) + } + + if string(b) != "123456789"[i * 3:i * 3 + 3] { + t.Fatal(string(b)) + } + } + + n, err := r.Read(b) + if n != 0 || !errors.Is(err, errTest) { + t.Fatal(n, err) + } + }) + + t.Run("custom error with pool error", func(t *testing.T) { + c := ContentFunc(func(w io.Writer) (int64, error) { + w.Write([]byte("123")) + w.Write([]byte("456")) + w.Write([]byte("123")) + return 0, errTest2 + }) + + p := &testPool{ + size: 2, + failAfter: []int{1}, + } + + o := Options{Pool: p} + r := BufferedContent(c, o) + b, ok, err := r.ReadBytes([]byte("67"), 12) + if string(b) != "12" /* segment size og 2 by the pool */ || ok || err != nil { + t.Fatal(string(b), ok, err) + } + + b, ok, err = r.ReadBytes([]byte("67"), 12) + if len(b) != 0 || ok || !errors.Is(err, errTest) || !errors.Is(err, errTest2) { + t.Fatal(string(b), ok, err) + } + }) +} diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/io_test.go b/io_test.go index ee86725..d67515d 100644 --- a/io_test.go +++ b/io_test.go @@ -1,10 +1,13 @@ package buffer_test import ( + "code.squareroundforest.org/arpio/buffer" "errors" "io" ) +type createReader func(r io.Reader, o buffer.Options) buffer.Reader + type gen struct { rng []byte max int @@ -17,8 +20,8 @@ type gen struct { } type writer struct { - written []byte - errAfter []int + written []byte + errAfter []int shortAfter []int } @@ -135,7 +138,7 @@ func (w *writer) Write(p []byte) (int, error) { if len(p) > 0 && len(w.shortAfter) > 0 && len(w.written) >= w.shortAfter[0] { w.shortAfter = w.shortAfter[1:] - p = p[:len(p) / 2] + p = p[:len(p)/2] } wp := make([]byte, len(p)) @@ -143,3 +146,12 @@ func (w *writer) Write(p []byte) (int, error) { w.written = append(w.written, wp...) return len(p), nil } + +func testContent(r io.Reader, o buffer.Options) buffer.Reader { + return buffer.BufferedContent( + buffer.ContentFunc(func(w io.Writer) (int64, error) { + return io.Copy(w, r) + }), + o, + ) +} diff --git a/lib.go b/lib.go index 47a980f..6069625 100644 --- a/lib.go +++ b/lib.go @@ -3,8 +3,8 @@ package buffer import ( - "io" "errors" + "io" ) type Pool interface { @@ -12,20 +12,14 @@ type Pool interface { 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 +type ContentFunc func(io.Writer) (int64, error) + // 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 @@ -34,7 +28,10 @@ type Reader struct { reader *reader } -var ErrZeroAllocation = errors.New("zero allocation") +var ( + ErrZeroAllocation = errors.New("zero allocation") + ErrContentAbort = errors.New("content pipe aborted") +) func DefaultPool() Pool { return newPool() @@ -44,26 +41,49 @@ func NoPool(allocSize int) Pool { return noPool{allocSize: allocSize} } -func ReaderFrom(in io.Reader, o Options) Reader { - if o.Pool == nil { - o.Pool = DefaultPool() +func (f ContentFunc) WriteTo(w io.Writer) (int64, error) { + return f(w) +} + +// similar to bufio.Reader, but with pooled buffering internally +func BufferedReader(in io.Reader, o Options) Reader { + if in == nil { + return Reader{} } - if o.ReadSize <= 0 { - o.ReadSize = 1 << 9 + if o.Pool == nil { + o.Pool = DefaultPool() } return Reader{reader: &reader{options: o, in: in}} } -func ReaderFromContent(w ContentWriter, o Options) Reader { - return Reader{} +// similar to a pipe, but with dynamic and pooled buffering internally +// the individual Write calls are blocked until the reading end requests more data +// WriterTo instances need to be safe to call in goroutines other than they were created in +// if it returns with nil error, it will be interpreted as EOF on the reader side +// unfinished calls to the passed in io.Writer will return with ErrContentAbort when the buffer +// needs to be consumed +func BufferedContent(c io.WriterTo, o Options) Reader { + if c == nil { + return Reader{} + } + + if o.Pool == nil { + o.Pool = DefaultPool() + } + + return Reader{reader: &reader{options: o, in: mkcontent(c)}} } // - 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) { + if r.reader == nil { + return 0, io.EOF + } + return r.reader.read(p) } @@ -79,26 +99,47 @@ func (r Reader) Read(p []byte) (int, 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) { + if r.reader == nil { + return nil, false, io.EOF + } + 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) { + if r.reader == nil { + return nil, 0, io.EOF + } + return r.reader.readUTF8(max) } // not a copy +// error only when nothing buffered func (r Reader) Peek(max int) ([]byte, error) { + if r.reader == nil { + return nil, io.EOF + } + return r.reader.peek(max) } // not a copy // can be wrong after error func (r Reader) Buffered() []byte { + if r.reader == nil { + return nil + } + 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) { + if r.reader == nil { + return 0, nil + } + return r.reader.writeTo(w) } diff --git a/peek_test.go b/peek_test.go index 8fdc5f3..ef5c753 100644 --- a/peek_test.go +++ b/peek_test.go @@ -1,228 +1,212 @@ package buffer_test import ( - "testing" - "code.squareroundforest.org/arpio/buffer" "bytes" - "io" + "code.squareroundforest.org/arpio/buffer" "errors" + "io" + "testing" ) 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, - } + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(t *testing.T) { + t.Run("peek across segments", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(g, o) + p, err := r.Peek(2*128 + 30) + if err != nil { + t.Fatal(err) + } - 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)) + } + }) - 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, + } - 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 := cr(g, o) + b, err := r.Peek(18) + if err != nil { + t.Fatal(err) + } - 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)) + } - if !bytes.Equal(b, generate(12)) { - t.Fatal("invalid content", len(b)) - } + b, err = r.Peek(18) + if err != nil { + t.Fatal(err) + } - b, err = r.Peek(18) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(b, generate(12)) { + t.Fatal("invalid content", len(b)) + } - if !bytes.Equal(b, generate(12)) { - t.Fatal("invalid content", len(b)) - } + b, err = io.ReadAll(r) + if err != nil { + t.Fatal(err) + } - b, err = io.ReadAll(r) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(b, generate(12)) { + t.Fatal("invalid content", len(b)) + } - 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") + } - 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)) + } + }) - 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 := cr(g, o) + b, err := r.Peek(18) + if !errors.Is(err, io.EOF) { + t.Fatal("failed EOF") + } - 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") + } + }) - 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 := cr(g, o) + b, err := r.Peek(18) + if err != nil { + t.Fatal(err) + } - 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") + } + }) - 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)} + r := cr(g, o) + b, err := r.Peek(160) + if err != nil { + t.Fatal(err) + } - t.Run("peek multiple segments", func(t *testing.T) { - g := &gen{max: 1 << 15} - o := buffer.Options{ - Pool: buffer.NoPool(64), - ReadSize: 16, - } + if !bytes.Equal(b, generate(160)) { + t.Fatal("invalid content") + } + }) - r := buffer.ReaderFrom(g, o) - b, err := r.Peek(160) - if err != nil { - t.Fatal(err) - } + t.Run("peek on partially filled", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(64)} + r := cr(g, o) + b, err := r.Peek(16) + if err != nil { + t.Fatal(err) + } - if !bytes.Equal(b, generate(160)) { - t.Fatal("invalid content") - } - }) + if !bytes.Equal(b, generate(16)) { + 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, - } + b, err = r.Peek(48) + if err != nil { + t.Fatal(err) + } - r := buffer.ReaderFrom(g, o) - b, err := r.Peek(16) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(b, generate(48)) { + t.Fatal("invalid content") + } + }) - if !bytes.Equal(b, generate(16)) { - 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)} + r := cr(g, o) + b, err := r.Peek(160) + if err != nil { + t.Fatal(err) + } - b, err = r.Peek(48) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(b, generate(160)) { + t.Fatal("invalid content") + } - if !bytes.Equal(b, generate(48)) { - t.Fatal("invalid content") - } - }) + b, err = r.Peek(144) + if err != nil { + t.Fatal(err) + } - 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, - } + if !bytes.Equal(b, generate(144)) { + t.Fatal("invalid content") + } + }) - r := buffer.ReaderFrom(g, o) - b, err := r.Peek(160) - if err != nil { - t.Fatal(err) - } + t.Run("peek not enough available", func(t *testing.T) { + g := &gen{ + max: 12, + fastErr: true, + } - if !bytes.Equal(b, generate(160)) { - t.Fatal("invalid content") - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + b, err := r.Peek(18) + if err != nil { + t.Fatal(err) + } - b, err = r.Peek(144) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(b, generate(12)) { + t.Fatal("invalid content") + } + }) - if !bytes.Equal(b, generate(144)) { - t.Fatal("invalid content") - } - }) + t.Run("peek not enough available multiple segments", func(t *testing.T) { + g := &gen{ + max: 144, + fastErr: true, + } - t.Run("peek not enough available", func(t *testing.T) { - g := &gen{ - max: 12, - fastErr: true, - } + o := buffer.Options{Pool: buffer.NoPool(64)} + r := cr(g, o) + b, err := r.Peek(214) + if err != nil { + t.Fatal(err) + } - 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(144)) { + t.Fatal("invalid content") + } + }) - if !bytes.Equal(b, generate(12)) { - 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 := cr(g, o) + b, err := r.Peek(0) + if err != nil { + t.Fatal(err) + } - 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") - } - }) + if len(b) != 0 { + t.Fatal("ivnalid content") + } + }) + }) + } } diff --git a/pool.go b/pool.go index 9d22665..4be2f9e 100644 --- a/pool.go +++ b/pool.go @@ -1,6 +1,6 @@ package buffer -type noPool struct{ +type noPool struct { allocSize int } diff --git a/pool_test.go b/pool_test.go index a2c95bd..7dccbbe 100644 --- a/pool_test.go +++ b/pool_test.go @@ -1,22 +1,21 @@ package buffer_test import ( - "testing" - "math/rand" - "code.squareroundforest.org/arpio/buffer" "bytes" - "io" + "code.squareroundforest.org/arpio/buffer" "errors" + "io" + "math/rand" + "testing" ) type pool struct { - allocSize int + allocSize int alloc, free int - errAfter []int - zeroAfter []int - shortAfter []int - longAfter []int - randomSize []int + errAfter []int + zeroAfter []int + varyingSize []int + rand *rand.Rand } func (p pool) allocCondition(c *[]int) bool { @@ -32,6 +31,14 @@ func (p pool) allocCondition(c *[]int) bool { return true } +func (p *pool) ensureRand() { + if p.rand != nil { + return + } + + p.rand = rand.New(rand.NewSource(9)) +} + func (p *pool) Get() ([]byte, error) { defer func() { p.alloc++ @@ -46,20 +53,14 @@ func (p *pool) Get() ([]byte, error) { } n := p.allocSize - if p.allocCondition(&p.shortAfter) { - n /= 2 + if len(p.varyingSize) > 1 { + p.ensureRand() + n = p.varyingSize[0] + p.rand.Intn(p.varyingSize[1]) } - 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]) + if len(p.varyingSize) == 1 { + p.ensureRand() + n = 1 + p.rand.Intn(p.varyingSize[0]) } return make([]byte, n), nil @@ -70,391 +71,768 @@ func (p *pool) Put([]byte) { } 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 - } - + for _, cr := range []createReader{buffer.BufferedReader, testContent} { + t.Run("allocate", func(t *testing.T) { + t.Run("read", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + b := make([]byte, 256) + n, err := r.Read(b) if err != nil { t.Fatal(err) } - if n != 1 << 12 { + 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 := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<12) + if err != nil { + t.Fatal(err) + } + + if ok { t.Fatal("unexpected delimiter") } - if len(runes) != 1 << 12 { + if len(b) != 0 { 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 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 := cr(g, o) + runes, n, err := r.ReadUTF8(1 << 12) if err != nil { t.Fatal(err) } - } - if p.alloc != 3 { - t.Fatal("invalid allocation count") - } + if n != 1<<12 { + t.Fatal("unexpected delimiter") + } - if p.free != 3 { - t.Fatal("invalid allocation count") - } + 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 := cr(g, o) + b, err := r.Peek(3 * 1 << 12) + if err != nil { + t.Fatal(err) + } + + if len(b) != 3*1<<12 { + t.Fatal("invalid content") + } + + if p.alloc != 3 { + t.Fatal("invalid allocation count") + } + }) + + t.Run("buffered", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + b := r.Buffered() + if len(b) != 0 { + t.Fatal("invalid content") + } + + if p.alloc != 0 { + t.Fatal("invalid allocation count") + } + }) + + t.Run("write to", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + + var b bytes.Buffer + n, err := r.WriteTo(&b) + if err != nil { + t.Fatal(err) + } + + if n != 1<<15 { + t.Fatal("invalid write length") + } + + if !bytes.Equal(b.Bytes(), generate(1<<15)) { + t.Fatal("invalid content") + } + + if p.alloc != 1 { + t.Fatal("invalid allocation count") + } + }) }) - t.Run("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") - } + 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 := cr(g, o) + b := make([]byte, 1<<9) + for { + _, err := r.Read(b) + if errors.Is(err, io.EOF) { + break + } - if p.alloc != 0 || p.free != 0 { - t.Fatal("invalid allocation count") - } + 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 := cr(g, o) + _, _, err := r.ReadBytes([]byte("123"), 1<<15+3) + if err != nil { + t.Fatal(err) + } + + _, _, err = r.ReadBytes([]byte("123"), 1<<15+3) + if !errors.Is(err, io.EOF) { + t.Fatal(err) + } + + if p.alloc != 9 { + t.Fatal("invalid allocation count", p.alloc) + } + + if p.free != 9 { + t.Fatal("invalid free count", p.free) + } + }) + + t.Run("read utf8", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + for { + runes, n, err := r.ReadUTF8(1 << 12) + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + t.Fatal(err) + } + + if n != 1<<12 { + t.Fatal("unexpected delimiter") + } + + if len(runes) != 1<<12 { + t.Fatal("invalid content") + } + } + + if p.alloc != p.free { + t.Fatal("invalid allocation count", p.alloc, p.free) + } + }) + + t.Run("peek", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := r.Peek(3 * 1 << 12) + if err != nil { + t.Fatal(err) + } + + if len(b) != 3*1<<12 { + t.Fatal("invalid content") + } + + if p.alloc != 3 { + t.Fatal("invalid allocation count") + } + + if p.free != 0 { + t.Fatal("invalid allocation count") + } + + b = make([]byte, 1<<9) + for { + _, err := r.Read(b) + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + t.Fatal(err) + } + } + + if p.alloc != 3 { + t.Fatal("invalid allocation count") + } + + if p.free != 3 { + t.Fatal("invalid allocation count") + } + }) + + t.Run("buffered", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + b := r.Buffered() + if len(b) != 0 { + t.Fatal("invalid content") + } + + if p.alloc != 0 || p.free != 0 { + t.Fatal("invalid allocation count") + } + }) + + t.Run("write to", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1 << 12} + o := buffer.Options{Pool: p} + r := cr(g, o) + + var b bytes.Buffer + n, err := r.WriteTo(&b) + if err != nil { + t.Fatal(err) + } + + if n != 1<<15 { + t.Fatal("invalid write length") + } + + if !bytes.Equal(b.Bytes(), generate(1<<15)) { + t.Fatal("invalid content") + } + + if p.alloc != 1 { + t.Fatal("invalid allocation count") + } + + if p.free != 1 { + t.Fatal("invalid free count") + } + }) }) - t.Run("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) - } + 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}, + } - if n != 1 << 15 { - t.Fatal("invalid write length") - } + o := buffer.Options{Pool: p} + r := cr(g, o) + b := make([]byte, 256) + _, err := r.Read(b) + if !errors.Is(err, buffer.ErrZeroAllocation) { + t.Fatal("failed to fail", err) + } + }) - if !bytes.Equal(b.Bytes(), generate(1 << 15)) { - t.Fatal("invalid content") - } + t.Run("read bytes", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 9, + zeroAfter: []int{1}, + } - if p.alloc != 1 { - t.Fatal("invalid allocation count") - } + o := buffer.Options{Pool: p} + r := cr(g, o) + _, _, err := r.ReadBytes([]byte("123"), 1<<12) + if err != nil { + t.Fatal(err) + } - if p.free != 1 { - t.Fatal("invalid free count") - } - }) - }) + _, _, err = r.ReadBytes([]byte("123"), 1<<12) + if !errors.Is(err, buffer.ErrZeroAllocation) { + t.Fatal("failed to fail", err) + } + }) - 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}, - } + 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) - b := make([]byte, 256) - _, err := r.Read(b) - if !errors.Is(err, buffer.ErrZeroAllocation) { - t.Fatal("failed to fail", err) - } + o := buffer.Options{Pool: p} + r := cr(g, o) + _, _, err := r.ReadUTF8(1 << 12) + if !errors.Is(err, buffer.ErrZeroAllocation) { + t.Fatal("failed to fail", err) + } + }) + + t.Run("peek", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 12, + zeroAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + _, err := r.Peek(3 * 1 << 12) + if !errors.Is(err, buffer.ErrZeroAllocation) { + t.Fatal("failed to fail", err) + } + }) + + t.Run("write to", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 12, + zeroAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + + var b bytes.Buffer + _, err := r.WriteTo(&b) + if !errors.Is(err, buffer.ErrZeroAllocation) { + t.Fatal("failed to fail", err) + } + }) }) - t.Run("read bytes", func(t *testing.T) { - g := &gen{max: 1 << 15} - p := &pool{ - allocSize: 1 << 9, - zeroAfter: []int{1}, - } + t.Run("varying segment sizes", func(t *testing.T) { + t.Run("read bytes", func(t *testing.T) { + t.Run("find", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + customContentAfter: []int{1 << 11}, + customContent: map[int][]byte{1 << 11: []byte("123")}, + } - o := buffer.Options{Pool: p} - r := buffer.ReaderFrom(g, o) - _, _, err := r.ReadBytes([]byte("123"), 1 << 12) - if err != nil { - t.Fatal(err) - } + p := &pool{varyingSize: []int{8, 256}} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<15) + if err != nil { + t.Fatal(err) + } - _, _, err = r.ReadBytes([]byte("123"), 1 << 12) - if !errors.Is(err, buffer.ErrZeroAllocation) { - t.Fatal("failed to fail", err) - } + if !ok { + t.Fatal("failed to find delimiter") + } + + if !bytes.Equal(b, append(generate(1<<11), []byte("123")...)) { + t.Fatal("invalid content") + } + }) + + t.Run("find not", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{varyingSize: []int{8, 256}} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<15) + if err != nil { + t.Fatal(err) + } + + if ok { + t.Fatal("invalid delimiter") + } + + if !bytes.Equal(b, generate(1<<15)) { + t.Fatal("invalid content") + } + }) + }) + + t.Run("peek", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{varyingSize: []int{8, 256}} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := r.Peek(1 << 11) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(b, generate(1<<11)) { + t.Fatal("invalid content") + } + }) }) - t.Run("read utf8", func(t *testing.T) { - g := &gen{max: 1 << 15} - p := &pool{ - allocSize: 1 << 12, - zeroAfter: []int{0}, - } + t.Run("one byte segments", func(t *testing.T) { + t.Run("read", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } - 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) - } + if !bytes.Equal(b, generate(1<<15)) { + t.Fatal("invalid content") + } + }) + + t.Run("read bytes", func(t *testing.T) { + t.Run("find", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + customContentAfter: []int{1 << 11}, + customContent: map[int][]byte{1 << 11: []byte("123")}, + } + + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<15) + if err != nil { + t.Fatal(err) + } + + if !ok { + t.Fatal("failed to find delimiter") + } + + if !bytes.Equal(b, append(generate(1<<11), []byte("123")...)) { + t.Fatal("invalid content") + } + }) + + t.Run("find not", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<15) + if err != nil { + t.Fatal(err) + } + + if ok { + t.Fatal("invalid delimiter found") + } + + if len(b) != 0 { + t.Fatal("invalid content", len(b)) + } + }) + }) + + t.Run("read utf8", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + rng: utf8W2Range, + } + + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + runes, n, err := r.ReadUTF8(1 << 14) + if err != nil { + t.Fatal(err) + } + + if n != 1<<15 { + t.Fatal("invalid read length") + } + + if string(runes) != string(generateFrom(utf8W2Range, 1<<15)) { + t.Fatal("invalid content") + } + }) + + t.Run("peek", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := r.Peek(1 << 14) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(b, generate(1<<14)) { + t.Fatal("invalid content") + } + }) + + t.Run("write to", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{allocSize: 1} + o := buffer.Options{Pool: p} + r := cr(g, o) + + var b bytes.Buffer + n, err := r.WriteTo(&b) + if err != nil { + t.Fatal(err) + } + + if n != 1<<15 { + t.Fatal("invalid write length") + } + + if !bytes.Equal(b.Bytes(), generate(1<<15)) { + t.Fatal("invalid content") + } + }) }) - t.Run("peek", func(t *testing.T) { - g := &gen{max: 1 << 15} - p := &pool{ - allocSize: 1 << 12, - zeroAfter: []int{0}, - } + t.Run("pool error on allocate", func(t *testing.T) { + t.Run("read", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []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) - } + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := io.ReadAll(r) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail") + } + + if len(b) != 0 { + t.Fatal("invalid content") + } + }) + + t.Run("read bytes", func(t *testing.T) { + t.Run("immediate", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<12) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error", err) + } + + if ok { + t.Fatal("invalid delimiter found") + } + + if len(b) != 0 { + t.Fatal("invalid content") + } + }) + + t.Run("on grow", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{1}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 1<<15) + if err != nil { + t.Fatal(err) + } + + if ok { + t.Fatal("invalid delimiter found") + } + + if !bytes.Equal(b, generate(1<<11)) { + t.Fatal("invalid content") + } + + b, ok, err = r.ReadBytes([]byte("123"), 1<<15) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error", err) + } + + if ok { + t.Fatal("invalid delimiter found") + } + + if len(b) != 0 { + t.Fatal("invalid content") + } + }) + }) + + t.Run("read utf8", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + rng: utf8W2Range, + } + + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + runes, n, err := r.ReadUTF8(1 << 14) + if !errors.Is(err, errTest) { + t.Fatal(err) + } + + if n != 0 { + t.Fatal("invalid read length") + } + + if len(runes) != 0 { + t.Fatal("invalid content") + } + }) + + t.Run("peek", func(t *testing.T) { + t.Run("immediate", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := r.Peek(1 << 12) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error", err) + } + + if len(b) != 0 { + t.Fatal("invalid content") + } + }) + + t.Run("on grow", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{1}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + b, err := r.Peek(1 << 15) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(b, generate(1<<11)) { + t.Fatal("invalid content") + } + + b, ok, err := r.ReadBytes([]byte("123"), 1<<11) + if err != nil { + t.Fatal(err) + } + + if ok { + t.Fatal("invalid delimiter found") + } + + if !bytes.Equal(b, generate(1<<11)) { + t.Fatal("invalid content") + } + + b, err = r.Peek(1 << 15) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error") + } + + if len(b) != 0 { + t.Fatal("invalid content") + } + }) + }) + + t.Run("write to", func(t *testing.T) { + g := &gen{max: 1 << 15} + p := &pool{ + allocSize: 1 << 11, + errAfter: []int{0}, + } + + o := buffer.Options{Pool: p} + r := cr(g, o) + + var b bytes.Buffer + n, err := r.WriteTo(&b) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error") + } + + if n != 0 { + t.Fatal("invalid write length") + } + + if b.Len() != 0 { + t.Fatal("invalid content") + } + }) }) - - 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 index 8d19054..c303c50 100644 --- a/read_test.go +++ b/read_test.go @@ -1,371 +1,280 @@ package buffer_test import ( - "testing" - "code.squareroundforest.org/arpio/buffer" - "io" "bytes" + "code.squareroundforest.org/arpio/buffer" "errors" + "io" + "testing" ) 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) - } + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(t *testing.T) { + t.Run("small", func(t *testing.T) { + g := &gen{max: 3} + r := cr(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)) - } - }) + 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) - } + t.Run("large", func(t *testing.T) { + g := &gen{max: 1 << 18} + r := cr(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)) - } - }) + 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) + t.Run("zero first", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) - var p []byte - n, err := r.Read(p) - if err != nil { - t.Fatal(err) - } + var p []byte + n, err := r.Read(p) + if err != nil { + t.Fatal(err) + } - if n != 0 { - t.Fatal("invalid length") - } + if n != 0 { + t.Fatal("invalid length") + } - p = make([]byte, 256) - n, err = r.Read(p) - if err != nil { - t.Fatal(err) - } + p = make([]byte, 256) + n, err = r.Read(p) + if err != nil { + t.Fatal(err) + } - if n != 256 { - t.Fatal("invalid length") - } + if n != 256 { + t.Fatal("invalid length") + } - if !bytes.Equal(p, generate(256)) { - t.Fatal("invalid content") - } - }) + 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, - } + 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)} + r := cr(g, o) + b, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } - 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)) + } + }) - 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)} + r := cr(g, o) + r.Peek(30) + p := make([]byte, 60) + n, err := r.Read(p) + if n != 60 { + t.Fatal("invalid read length", n) + } - t.Run("partial segment", func(t *testing.T) { - g := &gen{max: 1 << 15} - o := buffer.Options{ - Pool: buffer.NoPool(128), - ReadSize: 8, - } + if err != nil { + t.Fatal(err) + } - 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 !bytes.Equal(p, generate(60)) { + t.Fatal("invalid content") + } + }) - if err != nil { - t.Fatal(err) - } + t.Run("partial segment nth", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(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 !bytes.Equal(p, generate(60)) { - t.Fatal("invalid content") - } - }) + if err != nil { + t.Fatal(err) + } - t.Run("partial segment nth", func(t *testing.T) { - g := &gen{max: 1 << 15} - o := buffer.Options{ - Pool: buffer.NoPool(128), - ReadSize: 8, - } + if !bytes.Equal(p, generate(2*128+60)) { + t.Fatal("invalid content") + } + }) - 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) - } + t.Run("read buffer larger than read size", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(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 err != nil { + t.Fatal(err) + } - if !bytes.Equal(p, generate(2*128+60)) { - t.Fatal("invalid content") - } - }) + if !bytes.Equal(p, generate(12)) { + 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, - } + t.Run("read buffer larger than segment", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(g, o) + p := make([]byte, 192) + n, err := r.Read(p) + if n != 192 { + t.Fatal("invalid read size") + } - 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 err != nil { - t.Fatal(err) - } + if !bytes.Equal(p, generate(192)) { + t.Fatal("invalid content") + } + }) - if !bytes.Equal(p, generate(12)) { - 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)} + r := cr(g, o) + p := make([]byte, 384) + n, err := r.Read(p) + if n != 256 { + t.Fatal("invalid read size") + } - 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, - } + if err != nil { + t.Fatal(err) + } - r := buffer.ReaderFrom(g, o) - p := make([]byte, 192) - n, err := r.Read(p) - if n != 192 { - t.Fatal("invalid read size") - } + if !bytes.Equal(p[:n], generate(256)) { + t.Fatal("invalid content") + } - if err != nil { - t.Fatal(err) - } + n, err = r.Read(p) + if n != 0 || !errors.Is(err, io.EOF) { + t.Fatal("invalid post read", n, err) + } + }) + }) + } - if !bytes.Equal(p, generate(192)) { - t.Fatal("invalid content") - } - }) + t.Run("reader only", func(t *testing.T) { + cr := buffer.BufferedReader + t.Run("null read on empty", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + nullReadAfter: []int{0, 0}, + } - 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, - } + o := buffer.Options{Pool: buffer.NoPool(128)} + r := cr(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) + } - r := buffer.ReaderFrom(g, o) - p := make([]byte, 384) - n, err := r.Read(p) - if n != 256 { - t.Fatal("invalid read size") - } + 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) + } + }) - if err != nil { - t.Fatal(err) - } + t.Run("read error without content", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + errAfter: []int{32}, + } - if !bytes.Equal(p[:n], generate(256)) { - t.Fatal("invalid content") - } + o := buffer.Options{Pool: buffer.NoPool(32)} + r := cr(g, o) + p := make([]byte, 64) + n, err := r.Read(p) + if n != 32 || err != nil { + t.Fatal("failed to read", n, err) + } - n, err = r.Read(p) - if n != 0 || !errors.Is(err, io.EOF) { - t.Fatal("invalid post 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("null read on empty", func(t *testing.T) { - g := &gen{ - max: 1 << 15, - nullReadAfter: []int{0, 0}, - } + 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: 8, - } + o := buffer.Options{Pool: buffer.NoPool(32)} + r := cr(g, o) + p := make([]byte, 64) + n, err := r.Read(p) + if n != 32 || err != nil { + t.Fatal("failed to read", n, err) + } - 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 != 0 || !errors.Is(err, errTest) { + t.Fatal("failed to process read error", 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("read after error", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + errAfter: []int{32}, + fastErr: true, + } - 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(32)} + var result []byte + r := cr(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) + } - o := buffer.Options{ - Pool: buffer.NoPool(128), - ReadSize: 32, - } + result = append(result, p...) + } - 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 { + if n, err := r.Read(p); n != 5 || 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") - } + 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 index 2e658ae..30e1153 100644 --- a/readbytes_test.go +++ b/readbytes_test.go @@ -1,752 +1,660 @@ package buffer_test import ( - "testing" - "code.squareroundforest.org/arpio/buffer" "bytes" + "code.squareroundforest.org/arpio/buffer" "errors" "io" + "testing" ) 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")}, - } + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(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) - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + b, ok, err := r.ReadBytes([]byte("123"), 64) + if err != nil { + t.Fatal(err) + } - if !ok { - t.Fatal("delimiter") - } + if !ok { + t.Fatal("delimiter") + } - if !bytes.Equal(b, append(generate(12), []byte("123")...)) { - t.Fatal("failed to generate right content") - } + 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 := cr(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)} + r := cr(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)} + r := cr(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)} + r := cr(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)} + r := cr(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 := cr(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 := cr(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 := cr(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)} + r := cr(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)} + r := cr(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)} + r := cr(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)} + r := cr(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 := cr(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 := cr(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("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 := cr(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("find not none consumed", func(t *testing.T) { + g := &gen{} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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") + } + }) + }) + } + + t.Run("reader only", func(t *testing.T) { + cr := buffer.BufferedReader + 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)} + r := cr(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)} + r := cr(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)} + r := cr(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 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)} + r := cr(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)} + r := cr(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", 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 index 576fcf0..9c99b3d 100644 --- a/reader.go +++ b/reader.go @@ -16,34 +16,21 @@ type reader struct { err error } -func (r *reader) fillSegment(n int) (fn int) { +func (r *reader) fillSegment() (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 { + if start == len(seg) { return } - rn, err := r.in.Read(seg[start:end]) + rn, err := r.in.Read(seg[start:len(seg)]) if rn == 0 && err == nil { - rn, err = r.in.Read(seg[start:end]) + rn, err = r.in.Read(seg[start:len(seg)]) } if rn == 0 && err == nil { @@ -92,7 +79,7 @@ func (r *reader) fill(n int) { } } - fn := r.fillSegment(n) + fn := r.fillSegment() if fn == 0 { return } @@ -154,12 +141,18 @@ func (r *reader) consume(n int) { func (r *reader) free() { for { if len(r.segments) == 0 { - return + break } r.options.Pool.Put(r.segments[0]) r.segments = r.segments[1:] } + + if c, ok := r.in.(interface{ close() error }); ok { + if err := c.close(); err != nil { + r.err = errors.Join(r.err, err) + } + } } func (r reader) findSegmentUp(i int) (int, int) { @@ -297,18 +290,29 @@ func (r *reader) read(p []byte) (int, error) { 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) + var n int + for len(p) > 0 { + if len(r.segments) == 0 { + r.allocate() + if r.err != nil { + break + } + } + + if r.len == 0 { + r.fillSegment() + } + + ni := r.copy(p) + if ni == 0 { + break + } + + r.consume(ni) + p = p[ni:] + n += ni + } + if r.err != nil && r.len == 0 { r.free() if n == 0 { @@ -476,7 +480,7 @@ func (r *reader) writeTo(w io.Writer) (int64, error) { } } - fn := r.fillSegment(len(r.segments[len(r.segments) - 1])) + fn := r.fillSegment() if fn == 0 && r.err == nil { return n, io.ErrNoProgress } diff --git a/readutf8_test.go b/readutf8_test.go index 40e5e88..b040f51 100644 --- a/readutf8_test.go +++ b/readutf8_test.go @@ -1,232 +1,235 @@ package buffer_test import ( - "testing" "code.squareroundforest.org/arpio/buffer" "errors" "io" + "testing" ) func TestReadUTF8(t *testing.T) { - t.Run("read all after error", func(t *testing.T) { - g := &gen{ - rng: utf8Range, - max: 24, - fastErr: true, - } + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(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) - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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 len(runes) != 12 { + t.Fatal("short read", len(runes)) + } - if string(runes) != string(generateFrom(utf8Range, n0)) { - t.Fatal("invalid content") - } + if string(runes) != string(generateFrom(utf8Range, n0)) { + t.Fatal("invalid content") + } - runes, n1, err := r.ReadUTF8(12) - if err != nil { - t.Fatal(err) - } + runes, n1, err := r.ReadUTF8(12) + if err != nil { + t.Fatal(err) + } - if len(runes) != 3 { - t.Fatal("short read", len(runes)) - } + 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:])) - } + 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) - } + runes, n2, err := r.ReadUTF8(12) + if !errors.Is(err, io.EOF) { + t.Fatal(err) + } - if n2 != 0 { - t.Fatal("unexpected consumption") - } + if n2 != 0 { + t.Fatal("unexpected consumption") + } - if len(runes) != 0 { - t.Fatal("unexpected read", len(runes)) - } - }) + 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) - } + t.Run("ascii", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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 len(runes) != 12 { + t.Fatal("short read", len(runes)) + } - if string(runes) != string(generate(n)) { - t.Fatal("invalid content") - } - }) + 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, - } + 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) - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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 len(runes) != 12 { + t.Fatal("short read", len(runes)) + } - if string(runes) != string(generateFrom(utf8W2Range, n)) { - t.Fatal("invalid content") - } - }) + 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, - } + t.Run("long across segments", func(t *testing.T) { + g := &gen{ + rng: utf8W2Range, + max: 1 << 15, + } - o := buffer.Options{Pool: buffer.NoPool(9)} + o := buffer.Options{Pool: buffer.NoPool(9)} - r := buffer.ReaderFrom(g, o) - runes, n, err := r.ReadUTF8(12) - if err != nil { - t.Fatal(err) - } + r := cr(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 len(runes) != 12 { + t.Fatal("short read", len(runes)) + } - if string(runes) != string(generateFrom(utf8W2Range, n)) { - t.Fatal("invalid content") - } - }) + 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}, - } + 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(nullReadAfter), - ReadSize: nullReadAfter, - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + runes, n, err := r.ReadUTF8(24) + if err != nil { + t.Fatal(err) + } - r := buffer.ReaderFrom(g, o) - runes, _, err := r.ReadUTF8(numRunes - 2) // -2 for min read in readUTF8 - 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(runes) != numRunes-2 { - t.Fatal("short read", len(runes)) - } + if len([]byte(string(runes))) != len(utf8Range) { + t.Fatal("invalid number of bytes") + } - if string(runes) != string(generateFrom(utf8W2Range, nullReadAfter-4)) { - t.Fatal("invalid content") - } + 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, _, err = r.ReadUTF8(12) - if err != nil { - t.Fatal(err) - } + 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) + } + }) - if len(runes) != 2 { - t.Fatal("short read 2", len(runes)) - } + t.Run("immediate err", func(t *testing.T) { + g := &gen{rng: utf8Range} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + runes, n, err := r.ReadUTF8(12) + if !errors.Is(err, io.EOF) { + t.Fatal(err) + } - if string(runes) != string(generateFrom(utf8W2Range, nullReadAfter)[nullReadAfter-4:]) { - t.Fatal("invalid content 2") - } + if n != 0 { + t.Fatal("unexpected read", n) + } - runes, _, err = r.ReadUTF8(12) - if err != nil { - t.Fatal(err) - } + if len(runes) != 0 { + t.Fatal("unexpected content") + } + }) + }) + } - 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") + t.Run("reader only", func(t *testing.T) { + cr := buffer.BufferedReader + 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}, } - } - 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) - } - }) + o := buffer.Options{Pool: buffer.NoPool(nullReadAfter)} + r := cr(g, o) + runes, _, err := r.ReadUTF8(numRunes - 2) // -2 for min read in readUTF8 + if err != nil { + t.Fatal(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 len(runes) != numRunes-2 { + t.Fatal("short read", len(runes)) + } - if n != 0 { - t.Fatal("unexpected read", n) - } + if string(runes) != string(generateFrom(utf8W2Range, nullReadAfter-4)) { + t.Fatal("invalid content") + } - if len(runes) != 0 { - t.Fatal("unexpected 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") + } + }) }) } diff --git a/writeto_test.go b/writeto_test.go index d6ed452..5dfd199 100644 --- a/writeto_test.go +++ b/writeto_test.go @@ -1,285 +1,288 @@ package buffer_test import ( - "testing" - "code.squareroundforest.org/arpio/buffer" "bytes" + "code.squareroundforest.org/arpio/buffer" "errors" "io" + "testing" ) 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) + for title, cr := range map[string]createReader{"reader": buffer.BufferedReader, "content": testContent} { + t.Run(title, func(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 := cr(g, o) - var b bytes.Buffer - n, err := r.WriteTo(&b) - if err != nil { - t.Fatal(err) - } + var b bytes.Buffer + n, err := r.WriteTo(&b) + if err != nil { + t.Fatal(err) + } - if n != 1 << 15 { - t.Fatal("write count") - } + if n != 1<<15 { + t.Fatal("write count") + } - if !bytes.Equal(b.Bytes(), generate(1 << 15)) { - t.Fatal("content") - } - }) + 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) - } + t.Run("write out from started", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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 n != 256 { + t.Fatal("invalid read count") + } - if !bytes.Equal(p, generate(256)) { - t.Fatal("invalid content") - } + 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) - } + 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 n64 != 1<<15-256 { + t.Fatal("write count") + } - if !bytes.Equal(b.Bytes(), generate(1 << 15)[256:]) { - t.Fatal("content") - } - }) + 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) - } + t.Run("after EOF", func(t *testing.T) { + g := &gen{max: 256} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + b, err := r.Peek(512) + if err != nil { + t.Fatal(err) + } - if !bytes.Equal(b, generate(256)) { - t.Fatal("invalid content") - } + 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) - } + var buf bytes.Buffer + n, err := r.WriteTo(&buf) + if err != nil { + t.Fatal(err) + } - if n != 256 { - t.Fatal("write count") - } + if n != 256 { + t.Fatal("write count") + } - if !bytes.Equal(buf.Bytes(), generate(256)) { - t.Fatal("invalid content") - } - }) + 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, - } + t.Run("after eof empty", func(t *testing.T) { + g := &gen{max: 256} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + p := make([]byte, 512) + n, err := r.Read(p) + if err != nil { + t.Fatal(err) + } - o := buffer.Options{ - Pool: buffer.NoPool(64), - ReadSize: 16, - } + if n != 256 { + t.Fatal("invalid length") + } - r := buffer.ReaderFrom(g, o) - b, err := r.Peek(512) - if err != nil { - t.Fatal(err) - } + if !bytes.Equal(p[:n], generate(256)) { + t.Fatal("invalid content 1") + } - if !bytes.Equal(b, generate(256)) { - t.Fatal("invalid content", 1) - } + var buf bytes.Buffer + n64, err := r.WriteTo(&buf) + if err != nil { + t.Fatal(err) + } - 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 n64 != 0 { + t.Fatal("write count") + } - if n != 256 { - t.Fatal("write count") - } + if buf.Len() != 0 { + t.Fatal("invalid content 2") + } + }) - if !bytes.Equal(buf.Bytes(), generate(256)) { - t.Fatal("invalid content", 2) - } - }) + t.Run("write error", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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) + } - 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("not enough written") + } - if n != 256 { - t.Fatal("invalid length") - } + if !bytes.Equal(w.written, generate(1 << 15)[:n]) { + t.Fatal("invalid content") + } + }) - if !bytes.Equal(p[:n], generate(256)) { - t.Fatal("invalid content 1") - } + t.Run("short write", func(t *testing.T) { + g := &gen{max: 1 << 15} + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(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) + } - var buf bytes.Buffer - n64, err := r.WriteTo(&buf) - if err != nil { - t.Fatal(err) - } + if n < 256 { + t.Fatal("not enough written") + } - if n64 != 0 { - t.Fatal("write count") - } + if !bytes.Equal(w.written, generate(1 << 15)[:n]) { + t.Fatal("invalid content") + } + }) + }) + } - if buf.Len() != 0 { - t.Fatal("invalid content 2") - } - }) + t.Run("reader only", func(t *testing.T) { + cr := buffer.BufferedReader + t.Run("after err", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + errAfter: []int{256}, + fastErr: true, + } - t.Run("after error empty", func(t *testing.T) { - g := &gen{ - max: 1 << 15, - errAfter: []int{256}, - } + o := buffer.Options{Pool: buffer.NoPool(64)} + r := cr(g, o) + b, err := r.Peek(512) + if err != nil { + t.Fatal(err) + } - 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 !bytes.Equal(b, generate(256)) { + t.Fatal("invalid content", 1) + } - if n < 256 { - t.Fatal("invalid length") - } + 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 !bytes.Equal(p[:n], generate(1 << 15)[:n]) { - t.Fatal("invalid content 1") - } + if n != 256 { + t.Fatal("write count") + } - var buf bytes.Buffer - n64, err := r.WriteTo(&buf) - if !errors.Is(err, errTest) { - t.Fatal(err) - } + if !bytes.Equal(buf.Bytes(), generate(256)) { + t.Fatal("invalid content", 2) + } + }) - if n64 != 0 { - t.Fatal("write count") - } + t.Run("after error empty", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + errAfter: []int{256}, + } - if buf.Len() != 0 { - t.Fatal("invalid content 2") - } - }) + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) + p := make([]byte, 1<<15) + n, err := r.Read(p) + if err != nil { + t.Fatal(err) + } - t.Run("null read on fill", func(t *testing.T) { - g := &gen{ - max: 1 << 15, - nullReadAfter: []int{256, 256}, - } + if n < 256 { + t.Fatal("invalid length") + } - o := buffer.Options{Pool: buffer.NoPool(1 << 12)} - r := buffer.ReaderFrom(g, o) + if !bytes.Equal(p[:n], generate(1 << 15)[:n]) { + t.Fatal("invalid content 1") + } - var b bytes.Buffer - n, err := r.WriteTo(&b) - if !errors.Is(err, io.ErrNoProgress) { - t.Fatal("failed to fail with the right error") - } + var buf bytes.Buffer + n64, err := r.WriteTo(&buf) + if !errors.Is(err, errTest) { + t.Fatal(err) + } - if n < 256 { - t.Fatal("not enough written") - } + if n64 != 0 { + t.Fatal("write count") + } - if !bytes.Equal(b.Bytes(), generate(1 << 15)[:n]) { - t.Fatal("invalid content") - } - }) + if buf.Len() != 0 { + t.Fatal("invalid content 2") + } + }) - t.Run("err during fill", func(t *testing.T) { - g := &gen{ - max: 1 << 15, - errAfter: []int{256}, - } + 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) + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) - var b bytes.Buffer - n, err := r.WriteTo(&b) - if !errors.Is(err, errTest) { - t.Fatal("failed to fail with the right error") - } + 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 n < 256 { + t.Fatal("not enough written") + } - if !bytes.Equal(b.Bytes(), generate(1 << 15)[:n]) { - t.Fatal("invalid content") - } - }) + 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) - } + t.Run("err during fill", func(t *testing.T) { + g := &gen{ + max: 1 << 15, + errAfter: []int{256}, + } - if n < 256 { - t.Fatal("not enough written") - } + o := buffer.Options{Pool: buffer.NoPool(1 << 12)} + r := cr(g, o) - if !bytes.Equal(w.written, generate(1 << 15)[:n]) { - t.Fatal("invalid content") - } - }) + var b bytes.Buffer + n, err := r.WriteTo(&b) + if !errors.Is(err, errTest) { + t.Fatal("failed to fail with the right error") + } - 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 n < 256 { - t.Fatal("not enough written") - } - - if !bytes.Equal(w.written, generate(1 << 15)[:n]) { - t.Fatal("invalid content") - } + if !bytes.Equal(b.Bytes(), generate(1 << 15)[:n]) { + t.Fatal("invalid content") + } + }) }) }