Files
kopia/internal/gather/gather_bytes_test.go
2025-06-10 17:24:14 -07:00

347 lines
8.5 KiB
Go

package gather
import (
"bytes"
"fmt"
"io"
"math"
"testing"
"testing/iotest"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/testutil"
)
var sample1 = []byte("hello! how are you? nice to meet you.")
type failingWriter struct {
err error
}
func (w failingWriter) Write(buf []byte) (int, error) {
return 0, w.err
}
func TestGatherBytes(t *testing.T) {
// split the 'whole' into equivalent Bytes slices in some interesting ways
cases := []struct {
whole []byte
sliced Bytes
}{
{
whole: []byte{},
sliced: Bytes{},
},
{
whole: []byte{},
sliced: Bytes{Slices: [][]byte{
nil,
}},
},
{
whole: []byte{},
sliced: Bytes{Slices: [][]byte{
nil,
{},
nil,
}},
},
{
whole: sample1,
sliced: FromSlice(sample1),
},
{
whole: sample1,
sliced: Bytes{Slices: [][]byte{
nil,
sample1,
nil,
}},
},
{
whole: sample1,
sliced: Bytes{Slices: [][]byte{
sample1[0:20],
sample1[20:],
}},
},
{
whole: sample1,
sliced: Bytes{Slices: [][]byte{
sample1[0:20],
nil, // zero-length
{}, // zero-length
sample1[20:],
}},
},
{
whole: sample1,
sliced: Bytes{Slices: [][]byte{
sample1[0:10],
sample1[10:25],
sample1[25:30],
sample1[30:31],
sample1[31:],
}},
},
}
for _, tc := range cases {
func() {
b := tc.sliced
// length
if got, want := b.Length(), len(tc.whole); got != want {
t.Errorf("unexpected length: %v, want %v", got, want)
}
// reader
all, err := io.ReadAll(b.Reader())
if err != nil {
t.Errorf("unable to read: %v", err)
}
if !bytes.Equal(all, tc.whole) {
t.Errorf("unexpected data read %v, want %v", string(all), string(tc.whole))
}
// GetBytes
all = b.ToByteSlice()
if !bytes.Equal(all, tc.whole) {
t.Errorf("unexpected data from GetBytes() %v, want %v", string(all), string(tc.whole))
}
// AppendSectionTo - test exhaustively all combinations of start, length
var tmp WriteBuffer
defer tmp.Close()
n, err := b.WriteTo(&tmp)
require.NoError(t, err)
require.Equal(t, int64(b.Length()), n)
require.Equal(t, tmp.ToByteSlice(), b.ToByteSlice())
someError := errors.New("some error")
// WriteTo propagates error
if b.Length() > 0 {
_, err = b.WriteTo(failingWriter{someError})
require.ErrorIs(t, err, someError)
}
require.Error(t, b.AppendSectionTo(&tmp, -3, 3))
for i := 0; i <= len(tc.whole); i++ {
for j := i; j <= len(tc.whole); j++ {
tmp.Reset()
require.NoError(t, b.AppendSectionTo(&tmp, i, j-i))
if j > i {
require.ErrorIs(t, b.AppendSectionTo(failingWriter{someError}, i, j-i), someError)
}
require.Equal(t, tmp.ToByteSlice(), tc.whole[i:j])
}
}
}()
}
}
func TestGatherBytesReadSeeker(t *testing.T) {
var tmp WriteBuffer
defer tmp.Close()
buf := make([]byte, 1234567)
tmp.Append(buf)
require.Len(t, buf, tmp.Length())
reader := tmp.inner.Reader()
defer reader.Close() //nolint:errcheck
// TestReader tests that reading from r returns the expected file content.
// It does reads of different sizes, until EOF.
// If r implements [io.ReaderAt] or [io.Seeker], TestReader also checks
// that those operations behave as they should.
//
// If TestReader finds any misbehaviors, it returns an error reporting them.
// The error text may span multiple lines.
require.NoError(t, iotest.TestReader(reader, buf))
_, err := reader.Seek(-3, io.SeekStart)
require.Error(t, err)
_, err = reader.Seek(3, io.SeekEnd)
require.Error(t, err)
_, err = reader.Seek(10000000, io.SeekCurrent)
require.Error(t, err)
}
func TestGatherBytesReaderAtErrorResponses(t *testing.T) {
// 3.7 times the internal chunk size
contentBuf := make([]byte, int(float64(defaultAllocator.chunkSize)*3.7))
for i := range contentBuf {
contentBuf[i] = uint8(i % math.MaxUint8)
}
tcs := []struct {
inBsLen int
inOff int64
expectErr error
expectN int
}{
{
inBsLen: 1 << 10,
inOff: -1,
expectErr: ErrInvalidOffset,
expectN: 0,
},
{
inBsLen: 1 << 10,
inOff: math.MaxInt64,
expectErr: io.EOF,
expectN: 0,
},
{
inBsLen: 0,
inOff: -1,
expectErr: ErrInvalidOffset,
expectN: 0,
},
{
inBsLen: 0,
inOff: math.MaxInt64,
expectN: 0,
},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("%d: %d %d %d", i, tc.inBsLen, tc.inOff, tc.expectN), func(t *testing.T) {
// tmp is an empty buffer that will supply some bytes
// for testing
var wrt WriteBuffer
defer wrt.Close()
wrt.Append(contentBuf)
require.Equalf(t, defaultAllocator.chunkSize, wrt.alloc.chunkSize,
"this test expects that the default-allocator will be used, but we are using: %#v", wrt.alloc)
// get the reader out of the WriteBuffer so we can read what was written
// (presume all 0s)
reader := wrt.inner.Reader()
defer reader.Close() //nolint:errcheck
// get the reader as a ReaderAt
readerAt := testutil.EnsureType[io.ReaderAt](t, reader)
// make an output buffer of the required length
bs := make([]byte, tc.inBsLen)
n, err := readerAt.ReadAt(bs, tc.inOff)
require.ErrorIs(t, err, tc.expectErr)
require.Equal(t, tc.expectN, n)
})
}
}
func TestGatherBytesReaderAtVariableInputBufferSizes(t *testing.T) {
const inputBufferMaxMultiplier = 4.0 // maximum number of times the internal chunk size
contentBuf := make([]byte, defaultAllocator.chunkSize*inputBufferMaxMultiplier)
for i := range contentBuf {
contentBuf[i] = uint8(i % math.MaxUint8)
}
type testCase struct {
name string
inputBufferSize int
}
// Test some interesting input buffer sizes from a 1-byte buffer to many multiples
// of the internal allocator chunk size.
testCases := []testCase{
{"1", 1},
{"0.5x", int(0.5 * float64(defaultAllocator.chunkSize))},
{"x-1", defaultAllocator.chunkSize - 1},
{"x", defaultAllocator.chunkSize},
{"x+1", defaultAllocator.chunkSize + 1},
{"1.5x", int(1.5 * float64(defaultAllocator.chunkSize))},
{"2x-1", 2*defaultAllocator.chunkSize - 1},
{"2x", 2 * defaultAllocator.chunkSize},
{"2x+1", 2*defaultAllocator.chunkSize + 1},
{"2.5x", int(2.5 * float64(defaultAllocator.chunkSize))},
{"3x-1", 3*defaultAllocator.chunkSize - 1},
{"3x", 3 * defaultAllocator.chunkSize},
{"3x+1", 3*defaultAllocator.chunkSize + 1},
{"4x-1", 4*defaultAllocator.chunkSize - 1},
{"4x", 4 * defaultAllocator.chunkSize},
}
// Test the third buffer slice. The idea here is to exercise the part of
// the buffer ReaderAt implementation where it has a longer buffer size
// than the size of the internal chunks of the buffer implementation. When
// we do this, the ReaderAt is forced to draw more data than it actually
// can from the first slice it found after searching for the current
// pointer in read cycle. Finally, it should increment the read index
// correctly.
//
// x.1 ... x.9
for chunkSizeMultiplier := inputBufferMaxMultiplier - 0.9; chunkSizeMultiplier < inputBufferMaxMultiplier; chunkSizeMultiplier += 0.1 {
testCases = append(testCases, testCase{
fmt.Sprintf("%.1fx", chunkSizeMultiplier),
int(float64(defaultAllocator.chunkSize) * chunkSizeMultiplier),
},
)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// each test should have its own writer because t.Run() can be
// parallelized
var preWrt WriteBuffer
defer preWrt.Close()
// assert some preconditions that the reader conforms to ReaderAt
buf := contentBuf[:tc.inputBufferSize]
// write the generated data
n, err := preWrt.Write(buf)
require.NoErrorf(t, err, "Write() failed, inputBufferSize: %v", tc.inputBufferSize)
require.Equalf(t, defaultAllocator.chunkSize, preWrt.alloc.chunkSize,
"this test expects that the default-allocator will be used, but we are using: %#v", preWrt.alloc)
require.Lenf(t, buf, n, "unexpected size of data written, inputBufferSize: %d", tc.inputBufferSize)
// get the reader out of the WriteBuffer so we can read what was written
preRdr := preWrt.inner.Reader()
_, ok := preRdr.(io.ReaderAt)
require.Truef(t, ok, "MUST implement io.ReaderAt, inputBufferSize: %d", tc.inputBufferSize)
// execute standard ReadAt tests.
require.NoErrorf(t, iotest.TestReader(preRdr, buf),
"iotest failed, inputBufferSize: %d", tc.inputBufferSize)
})
}
}
func TestGatherBytesPanicsOnClose(t *testing.T) {
var tmp WriteBuffer
tmp.Append([]byte{1, 2, 3})
tmp.Close()
require.Panics(t, func() {
tmp.Bytes().Reader()
})
}