307 lines
7.6 KiB
Go
307 lines
7.6 KiB
Go
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// mockSlowReader simulates a slow reader (like network or disk) by adding artificial delay
|
|
type mockSlowReader struct {
|
|
data []byte
|
|
pos int64
|
|
delay time.Duration
|
|
readCnt int // count of actual reads from source
|
|
}
|
|
|
|
func newMockSlowReader(data []byte, delay time.Duration) *mockSlowReader {
|
|
return &mockSlowReader{
|
|
data: data,
|
|
delay: delay,
|
|
}
|
|
}
|
|
|
|
func (m *mockSlowReader) Read(p []byte) (n int, err error) {
|
|
if m.pos >= int64(len(m.data)) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
// Simulate latency
|
|
time.Sleep(m.delay)
|
|
|
|
m.readCnt++ // track actual reads from source
|
|
n = copy(p, m.data[m.pos:])
|
|
m.pos += int64(n)
|
|
return n, nil
|
|
}
|
|
|
|
func (m *mockSlowReader) Seek(offset int64, whence int) (int64, error) {
|
|
var abs int64
|
|
switch whence {
|
|
case io.SeekStart:
|
|
abs = offset
|
|
case io.SeekCurrent:
|
|
abs = m.pos + offset
|
|
case io.SeekEnd:
|
|
abs = int64(len(m.data)) + offset
|
|
default:
|
|
return 0, errors.New("invalid whence")
|
|
}
|
|
if abs < 0 {
|
|
return 0, errors.New("negative position")
|
|
}
|
|
m.pos = abs
|
|
return abs, nil
|
|
}
|
|
|
|
func (m *mockSlowReader) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func TestCachedReadSeeker_CachingBehavior(t *testing.T) {
|
|
data := []byte("Hello, this is test data for streaming!")
|
|
delay := 10 * time.Millisecond
|
|
|
|
mock := newMockSlowReader(data, delay)
|
|
cached := NewCachedReadSeeker(mock)
|
|
|
|
// First read - should hit the source
|
|
buf1 := make([]byte, 5)
|
|
n, err := cached.Read(buf1)
|
|
if err != nil || n != 5 || string(buf1) != "Hello" {
|
|
t.Errorf("First read failed: got %q, want %q", buf1, "Hello")
|
|
}
|
|
|
|
// Seek back to start - should not hit source
|
|
_, err = cached.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
t.Errorf("Seek failed: %v", err)
|
|
}
|
|
|
|
// Second read of same data - should be from cache
|
|
readCntBefore := mock.readCnt
|
|
buf2 := make([]byte, 5)
|
|
n, err = cached.Read(buf2)
|
|
if err != nil || n != 5 || string(buf2) != "Hello" {
|
|
t.Errorf("Second read failed: got %q, want %q", buf2, "Hello")
|
|
}
|
|
if mock.readCnt != readCntBefore {
|
|
t.Error("Second read hit source when it should have used cache")
|
|
}
|
|
}
|
|
|
|
func TestCachedReadSeeker_Performance(t *testing.T) {
|
|
data := bytes.Repeat([]byte("abcdefghijklmnopqrstuvwxyz"), 1000) // ~26KB of data
|
|
delay := 10 * time.Millisecond
|
|
|
|
t.Run("Without Cache", func(t *testing.T) {
|
|
mock := newMockSlowReader(data, delay)
|
|
start := time.Now()
|
|
|
|
// Read entire data
|
|
if _, err := io.ReadAll(mock); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Seek back and read again
|
|
mock.Seek(0, io.SeekStart)
|
|
if _, err := io.ReadAll(mock); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
uncachedDuration := time.Since(start)
|
|
t.Logf("Without cache duration: %v", uncachedDuration)
|
|
})
|
|
|
|
t.Run("With Cache", func(t *testing.T) {
|
|
mock := newMockSlowReader(data, delay)
|
|
cached := NewCachedReadSeeker(mock)
|
|
start := time.Now()
|
|
|
|
// Read entire data
|
|
if _, err := io.ReadAll(cached); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Seek back and read again
|
|
cached.Seek(0, io.SeekStart)
|
|
if _, err := io.ReadAll(cached); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cachedDuration := time.Since(start)
|
|
t.Logf("With cache duration: %v", cachedDuration)
|
|
})
|
|
}
|
|
|
|
func TestCachedReadSeeker_SeekBehavior(t *testing.T) {
|
|
data := []byte("0123456789")
|
|
mock := newMockSlowReader(data, 0)
|
|
cached := NewCachedReadSeeker(mock)
|
|
|
|
tests := []struct {
|
|
name string
|
|
offset int64
|
|
whence int
|
|
wantPos int64
|
|
wantRead string
|
|
readBufSize int
|
|
}{
|
|
{"SeekStart", 3, io.SeekStart, 3, "3456", 4},
|
|
{"SeekCurrent", 2, io.SeekCurrent, 9, "9", 4},
|
|
{"SeekEnd", -5, io.SeekEnd, 5, "56789", 5},
|
|
{"SeekStartZero", 0, io.SeekStart, 0, "0123", 4},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pos, err := cached.Seek(tt.offset, tt.whence)
|
|
if err != nil {
|
|
t.Errorf("Seek failed: %v", err)
|
|
return
|
|
}
|
|
if pos != tt.wantPos {
|
|
t.Errorf("Seek position = %d, want %d", pos, tt.wantPos)
|
|
}
|
|
|
|
buf := make([]byte, tt.readBufSize)
|
|
n, err := cached.Read(buf)
|
|
if err != nil && err != io.EOF {
|
|
t.Errorf("Read failed: %v", err)
|
|
return
|
|
}
|
|
got := string(buf[:n])
|
|
if got != tt.wantRead {
|
|
t.Errorf("Read after seek = %q, want %q", got, tt.wantRead)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCachedReadSeeker_LargeReads(t *testing.T) {
|
|
// Test with larger data to simulate real streaming scenarios
|
|
data := bytes.Repeat([]byte("abcdefghijklmnopqrstuvwxyz"), 1000) // ~26KB
|
|
mock := newMockSlowReader(data, 0)
|
|
cached := NewCachedReadSeeker(mock)
|
|
|
|
// Read in chunks
|
|
chunkSize := 1024
|
|
buf := make([]byte, chunkSize)
|
|
|
|
var totalRead int
|
|
for {
|
|
n, err := cached.Read(buf)
|
|
totalRead += n
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Read error: %v", err)
|
|
}
|
|
}
|
|
|
|
if totalRead != len(data) {
|
|
t.Errorf("Total read = %d, want %d", totalRead, len(data))
|
|
}
|
|
|
|
// Verify cache by seeking back and reading again
|
|
cached.Seek(0, io.SeekStart)
|
|
readCntBefore := mock.readCnt
|
|
|
|
totalRead = 0
|
|
for {
|
|
n, err := cached.Read(buf)
|
|
totalRead += n
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Second read error: %v", err)
|
|
}
|
|
}
|
|
|
|
if mock.readCnt != readCntBefore {
|
|
t.Error("Second read hit source when it should have used cache")
|
|
}
|
|
}
|
|
|
|
func TestCachedReadSeeker_ChunkedReadsAndSeeks(t *testing.T) {
|
|
// Create ~1MB of test data
|
|
data := bytes.Repeat([]byte("abcdefghijklmnopqrstuvwxyz0123456789"), 30_000)
|
|
delay := 300 * time.Millisecond // 10ms delay per read to simulate network/disk latency
|
|
|
|
// Define read patterns to simulate real-world streaming
|
|
type readOp struct {
|
|
seekOffset int64
|
|
seekWhence int
|
|
readSize int
|
|
desc string
|
|
}
|
|
|
|
// Simulate typical streaming behavior with repeated reads
|
|
ops := []readOp{
|
|
{0, io.SeekStart, 10 * 1024 * 1024, "initial header"}, // Read first 10MB (headers)
|
|
{500_000, io.SeekStart, 15 * 1024 * 1024, "middle preview"}, // Seek to middle, read 15MB
|
|
{0, io.SeekStart, len(data), "full read after random seeks"}, // Read entire file
|
|
{0, io.SeekStart, len(data), "re-read entire file"}, // Re-read entire file (should be cached)
|
|
}
|
|
|
|
var uncachedDuration, cachedDuration time.Duration
|
|
var uncachedReads, cachedReads int
|
|
|
|
runTest := func(name string, useCache bool) {
|
|
t.Run(name, func(t *testing.T) {
|
|
mock := newMockSlowReader(data, delay)
|
|
var reader io.ReadSeekCloser = mock
|
|
if useCache {
|
|
reader = NewCachedReadSeeker(mock)
|
|
}
|
|
|
|
start := time.Now()
|
|
var totalRead int64
|
|
|
|
for i, op := range ops {
|
|
pos, err := reader.Seek(op.seekOffset, op.seekWhence)
|
|
if err != nil {
|
|
t.Fatalf("op %d (%s) - seek failed: %v", i, op.desc, err)
|
|
}
|
|
|
|
buf := make([]byte, op.readSize)
|
|
n, err := io.ReadFull(reader, buf)
|
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
|
t.Fatalf("op %d (%s) - read failed: %v", i, op.desc, err)
|
|
}
|
|
|
|
totalRead += int64(n)
|
|
t.Logf("op %d (%s) - seek to %d, read %d bytes", i, op.desc, pos, n)
|
|
}
|
|
|
|
duration := time.Since(start)
|
|
t.Logf("Total bytes read: %d", totalRead)
|
|
t.Logf("Total time: %v", duration)
|
|
t.Logf("Read count from source: %d", mock.readCnt)
|
|
|
|
if useCache {
|
|
cachedDuration = duration
|
|
cachedReads = mock.readCnt
|
|
} else {
|
|
uncachedDuration = duration
|
|
uncachedReads = mock.readCnt
|
|
}
|
|
})
|
|
}
|
|
|
|
// Run both tests
|
|
runTest("Without Cache", false)
|
|
runTest("With Cache", true)
|
|
|
|
// Report performance comparison
|
|
t.Logf("\nPerformance comparison:")
|
|
t.Logf("Uncached: %v (%d reads from source)", uncachedDuration, uncachedReads)
|
|
t.Logf("Cached: %v (%d reads from source)", cachedDuration, cachedReads)
|
|
t.Logf("Speed improvement: %.2fx", float64(uncachedDuration)/float64(cachedDuration))
|
|
t.Logf("Read reduction: %.2fx", float64(uncachedReads)/float64(cachedReads))
|
|
}
|