481 lines
12 KiB
Go
481 lines
12 KiB
Go
package iobuf
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestBufferedReaderSeekerRead(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reader io.Reader
|
|
reads []int
|
|
expectedReads []int
|
|
expectedBytes [][]byte
|
|
expectedBytesRead int64
|
|
expectedIndex int64
|
|
expectedBuffer []byte
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "read from seekable reader",
|
|
reader: strings.NewReader("test data"),
|
|
reads: []int{4},
|
|
expectedReads: []int{4},
|
|
expectedBytes: [][]byte{[]byte("test")},
|
|
expectedBytesRead: 4,
|
|
expectedIndex: 4,
|
|
},
|
|
{
|
|
name: "read from non-seekable reader with buffering",
|
|
reader: bytes.NewBufferString("test data"),
|
|
reads: []int{4},
|
|
expectedReads: []int{4},
|
|
expectedBytes: [][]byte{[]byte("test")},
|
|
expectedBytesRead: 4,
|
|
expectedIndex: 4,
|
|
expectedBuffer: []byte("test"),
|
|
},
|
|
{
|
|
name: "read from non-seekable reader without buffering",
|
|
reader: bytes.NewBufferString("test data"),
|
|
reads: []int{4},
|
|
expectedReads: []int{4},
|
|
expectedBytes: [][]byte{[]byte("test")},
|
|
expectedBytesRead: 4,
|
|
expectedIndex: 4,
|
|
},
|
|
{
|
|
name: "read beyond buffer",
|
|
reader: strings.NewReader("test data"),
|
|
reads: []int{10},
|
|
expectedReads: []int{9},
|
|
expectedBytes: [][]byte{[]byte("test data")},
|
|
expectedBytesRead: 9,
|
|
expectedIndex: 9,
|
|
},
|
|
{
|
|
name: "read with empty reader",
|
|
reader: strings.NewReader(""),
|
|
reads: []int{4},
|
|
expectedReads: []int{0},
|
|
expectedBytes: [][]byte{[]byte("")},
|
|
expectedBytesRead: 0,
|
|
expectedIndex: 0,
|
|
expectedError: io.EOF,
|
|
},
|
|
{
|
|
name: "read exact buffer size",
|
|
reader: strings.NewReader("test"),
|
|
reads: []int{4},
|
|
expectedReads: []int{4},
|
|
expectedBytes: [][]byte{[]byte("test")},
|
|
expectedBytesRead: 4,
|
|
expectedIndex: 4,
|
|
},
|
|
{
|
|
name: "read less than buffer size",
|
|
reader: strings.NewReader("te"),
|
|
reads: []int{4},
|
|
expectedReads: []int{2},
|
|
expectedBytes: [][]byte{[]byte("te")},
|
|
expectedBytesRead: 2,
|
|
expectedIndex: 2,
|
|
},
|
|
{
|
|
name: "read more than buffer size without buffering",
|
|
reader: bytes.NewBufferString("test data"),
|
|
reads: []int{4},
|
|
expectedReads: []int{4},
|
|
expectedBytes: [][]byte{[]byte("test")},
|
|
expectedBytesRead: 4,
|
|
expectedIndex: 4,
|
|
},
|
|
{
|
|
name: "multiple reads with buffering",
|
|
reader: bytes.NewBufferString("test data"),
|
|
reads: []int{4, 5},
|
|
expectedReads: []int{4, 5},
|
|
expectedBytes: [][]byte{[]byte("test"), []byte(" data")},
|
|
expectedBytesRead: 9,
|
|
expectedIndex: 9,
|
|
expectedBuffer: []byte("test data"),
|
|
},
|
|
{
|
|
name: "multiple reads without buffering",
|
|
reader: bytes.NewBufferString("test data"),
|
|
reads: []int{4, 5},
|
|
expectedReads: []int{4, 5},
|
|
expectedBytes: [][]byte{[]byte("test"), []byte(" data")},
|
|
expectedBytesRead: 9,
|
|
expectedIndex: 9,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
brs := NewBufferedReaderSeeker(tt.reader)
|
|
|
|
for i, readSize := range tt.reads {
|
|
buf := make([]byte, readSize)
|
|
n, err := brs.Read(buf)
|
|
|
|
assert.Equal(t, tt.expectedReads[i], n, "read %d: unexpected number of bytes read", i+1)
|
|
assert.Equal(t, tt.expectedBytes[i], buf[:n], "read %d: unexpected bytes", i+1)
|
|
|
|
if i == len(tt.reads)-1 {
|
|
if tt.expectedError != nil {
|
|
assert.ErrorIs(t, err, tt.expectedError)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, tt.expectedBytesRead, brs.bytesRead)
|
|
if brs.seeker == nil {
|
|
assert.Equal(t, tt.expectedIndex, brs.index)
|
|
}
|
|
|
|
if brs.buf != nil && len(tt.expectedBuffer) > 0 {
|
|
assert.Equal(t, tt.expectedBuffer, brs.buf.Bytes())
|
|
} else {
|
|
assert.Nil(t, tt.expectedBuffer)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBufferedReaderSeekerSeek(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reader io.Reader
|
|
offset int64
|
|
whence int
|
|
expectedPos int64
|
|
expectedErr bool
|
|
expectedRead []byte
|
|
}{
|
|
{
|
|
name: "seek on seekable reader with SeekStart",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 4,
|
|
whence: io.SeekStart,
|
|
expectedPos: 4,
|
|
expectedErr: false,
|
|
expectedRead: []byte(" dat"),
|
|
},
|
|
{
|
|
name: "seek on seekable reader with SeekCurrent",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 4,
|
|
whence: io.SeekCurrent,
|
|
expectedPos: 4,
|
|
expectedErr: false,
|
|
expectedRead: []byte(" dat"),
|
|
},
|
|
{
|
|
name: "seek on seekable reader with SeekEnd",
|
|
reader: strings.NewReader("test data"),
|
|
offset: -4,
|
|
whence: io.SeekEnd,
|
|
expectedPos: 5,
|
|
expectedErr: false,
|
|
expectedRead: []byte("data"),
|
|
},
|
|
{
|
|
name: "seek on non-seekable reader with SeekStart",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: 4,
|
|
whence: io.SeekStart,
|
|
expectedPos: 4,
|
|
expectedErr: false,
|
|
expectedRead: []byte{},
|
|
},
|
|
{
|
|
name: "seek on non-seekable reader with SeekCurrent",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: 4,
|
|
whence: io.SeekCurrent,
|
|
expectedPos: 4,
|
|
expectedErr: false,
|
|
expectedRead: []byte{},
|
|
},
|
|
{
|
|
name: "seek on non-seekable reader with SeekEnd",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: -4,
|
|
whence: io.SeekEnd,
|
|
expectedPos: 5,
|
|
expectedErr: false,
|
|
expectedRead: []byte{},
|
|
},
|
|
{
|
|
name: "seek to negative position",
|
|
reader: strings.NewReader("test data"),
|
|
offset: -1,
|
|
whence: io.SeekStart,
|
|
expectedPos: 0,
|
|
expectedErr: true,
|
|
expectedRead: nil,
|
|
},
|
|
{
|
|
name: "seek beyond EOF on non-seekable reader",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: 20,
|
|
whence: io.SeekEnd,
|
|
expectedPos: 29,
|
|
expectedErr: false,
|
|
expectedRead: []byte{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
brs := NewBufferedReaderSeeker(tt.reader)
|
|
pos, err := brs.Seek(tt.offset, tt.whence)
|
|
if tt.expectedErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedPos, pos)
|
|
if len(tt.expectedRead) > 0 {
|
|
buf := make([]byte, len(tt.expectedRead))
|
|
nn, err := brs.Read(buf)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, len(tt.expectedRead), nn)
|
|
assert.Equal(t, tt.expectedRead, buf[:nn])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBufferedReaderSeekerReadAt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reader io.Reader
|
|
offset int64
|
|
length int
|
|
expectedN int
|
|
expectErr bool
|
|
expectedOut []byte
|
|
}{
|
|
{
|
|
name: "read within buffer on seekable reader",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 5,
|
|
length: 4,
|
|
expectedN: 4,
|
|
expectedOut: []byte("data"),
|
|
},
|
|
{
|
|
name: "read within buffer on non-seekable reader",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: 5,
|
|
length: 4,
|
|
expectedN: 4,
|
|
expectedOut: []byte("data"),
|
|
},
|
|
{
|
|
name: "read beyond buffer",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 9,
|
|
length: 1,
|
|
expectedN: 0,
|
|
expectErr: true,
|
|
expectedOut: []byte{},
|
|
},
|
|
{
|
|
name: "read at start",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 0,
|
|
length: 4,
|
|
expectedN: 4,
|
|
expectedOut: []byte("test"),
|
|
},
|
|
{
|
|
name: "read with zero length",
|
|
reader: strings.NewReader("test data"),
|
|
offset: 0,
|
|
length: 0,
|
|
expectedN: 0,
|
|
expectedOut: []byte{},
|
|
},
|
|
{
|
|
name: "read negative offset",
|
|
reader: strings.NewReader("test data"),
|
|
offset: -1,
|
|
length: 4,
|
|
expectedN: 0,
|
|
expectErr: true,
|
|
expectedOut: []byte{},
|
|
},
|
|
{
|
|
name: "read beyond end on non-seekable reader",
|
|
reader: bytes.NewBufferString("test data"),
|
|
offset: 20,
|
|
length: 4,
|
|
expectedN: 0,
|
|
expectErr: true,
|
|
expectedOut: []byte{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
brs := NewBufferedReaderSeeker(tt.reader)
|
|
|
|
out := make([]byte, tt.length)
|
|
n, err := brs.ReadAt(out, tt.offset)
|
|
if tt.expectErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedN, n)
|
|
assert.Equal(t, tt.expectedOut, out[:n])
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBufferedReadSeekerSize tests the Size method of BufferedReadSeeker.
|
|
func TestBufferedReadSeekerSize(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reader io.Reader
|
|
setup func(*BufferedReadSeeker)
|
|
expectedSize int64
|
|
expectError bool
|
|
verifyPosition func(*BufferedReadSeeker, int64)
|
|
}{
|
|
{
|
|
name: "size of seekable reader",
|
|
reader: strings.NewReader("Hello, World!"),
|
|
expectedSize: 13,
|
|
},
|
|
{
|
|
name: "size of non-seekable reader",
|
|
reader: bytes.NewBufferString("Hello, World!"),
|
|
expectedSize: 13,
|
|
},
|
|
{
|
|
name: "size of empty seekable reader",
|
|
reader: strings.NewReader(""),
|
|
expectedSize: 0,
|
|
},
|
|
{
|
|
name: "size of empty non-seekable reader",
|
|
reader: bytes.NewBufferString(""),
|
|
expectedSize: 0,
|
|
},
|
|
{
|
|
name: "size of non-seekable reader after partial read",
|
|
reader: bytes.NewBufferString("Partial read data"),
|
|
setup: func(brs *BufferedReadSeeker) {
|
|
// Read first 7 bytes ("Partial").
|
|
buf := make([]byte, 7)
|
|
_, _ = brs.Read(buf)
|
|
},
|
|
expectedSize: 17, // "Partial read data" is 16 bytes
|
|
expectError: false,
|
|
verifyPosition: func(brs *BufferedReadSeeker, expectedSize int64) {
|
|
// After Size is called, the read position should remain at 7
|
|
currentPos, err := brs.Seek(0, io.SeekCurrent)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(7), currentPos)
|
|
},
|
|
},
|
|
{
|
|
name: "repeated Size calls",
|
|
reader: strings.NewReader("Repeated Size Calls Test"),
|
|
expectedSize: 24,
|
|
expectError: false,
|
|
setup: func(brs *BufferedReadSeeker) {
|
|
// Call Size multiple times.
|
|
size1, err1 := brs.Size()
|
|
assert.NoError(t, err1)
|
|
assert.Equal(t, int64(24), size1)
|
|
|
|
size2, err2 := brs.Size()
|
|
assert.NoError(t, err2)
|
|
assert.Equal(t, int64(24), size2)
|
|
},
|
|
},
|
|
{
|
|
name: "size with error during reading",
|
|
reader: &errorReader{
|
|
data: "Data before error",
|
|
errorAfter: 5, // Return error after reading 5 bytes
|
|
},
|
|
expectedSize: 0,
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "size with limited reader simulating EOF",
|
|
reader: io.LimitReader(strings.NewReader("Limited data"), 7),
|
|
expectedSize: 7,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
brs := NewBufferedReaderSeeker(tt.reader)
|
|
|
|
if tt.setup != nil {
|
|
tt.setup(brs)
|
|
}
|
|
|
|
size, err := brs.Size()
|
|
if tt.expectError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.expectedSize, size)
|
|
}
|
|
|
|
if tt.verifyPosition != nil {
|
|
tt.verifyPosition(brs, tt.expectedSize)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// errorReader is an io.Reader that returns an error after reading a specified number of bytes.
|
|
// It's used to simulate non-EOF errors during read operations.
|
|
type errorReader struct {
|
|
data string
|
|
errorAfter int // Number of bytes to read before returning an error
|
|
readBytes int
|
|
}
|
|
|
|
func (er *errorReader) Read(p []byte) (int, error) {
|
|
if er.readBytes >= er.errorAfter {
|
|
return 0, errors.New("simulated read error")
|
|
}
|
|
remaining := er.errorAfter - er.readBytes
|
|
toRead := len(p)
|
|
if toRead > remaining {
|
|
toRead = remaining
|
|
}
|
|
copy(p, er.data[er.readBytes:er.readBytes+toRead])
|
|
er.readBytes += toRead
|
|
return toRead, nil
|
|
}
|