1
0
Fork 0
mirror of https://github.com/imjasonh/gcsbuf synced 2026-07-08 17:16:10 +00:00
gcsbuf/reader_test.go
Jason Hall 5292972f87 first agent crack
Signed-off-by: Jason Hall <imjasonh@gmail.com>
2026-03-24 12:18:58 -04:00

234 lines
5.2 KiB
Go

package buf
import (
"bytes"
"context"
"io"
"strings"
"sync"
"testing"
"time"
"cloud.google.com/go/storage"
)
// writeObject writes content to a GCS object with optional metadata.
func writeObject(t *testing.T, client *storage.Client, bucket, object, content string, metadata map[string]string) {
t.Helper()
ctx := context.Background()
w := client.Bucket(bucket).Object(object).NewWriter(ctx)
if metadata != nil {
w.Metadata = metadata
}
if _, err := io.Copy(w, bytes.NewReader([]byte(content))); err != nil {
t.Fatalf("writing object %s/%s: %v", bucket, object, err)
}
if err := w.Close(); err != nil {
t.Fatalf("closing object writer %s/%s: %v", bucket, object, err)
}
}
func TestReaderFromCompleteObject(t *testing.T) {
client := setup(t)
ctx := context.Background()
// Pre-populate an object with done metadata.
writeObject(t, client, "test", "complete", "hello reader", map[string]string{"done": "true"})
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "complete",
PollInterval: 10 * time.Millisecond,
})
got, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if string(got) != "hello reader" {
t.Fatalf("got %q, want %q", string(got), "hello reader")
}
}
func TestReaderPollsForObjectCreation(t *testing.T) {
client := setup(t)
ctx := context.Background()
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "delayed",
PollInterval: 10 * time.Millisecond,
})
// Create the object after a short delay.
go func() {
time.Sleep(50 * time.Millisecond)
writeObject(t, client, "test", "delayed", "delayed data", map[string]string{"done": "true"})
}()
got, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if string(got) != "delayed data" {
t.Fatalf("got %q, want %q", string(got), "delayed data")
}
}
func TestReaderPollsForMoreData(t *testing.T) {
client := setup(t)
ctx := context.Background()
// Create initial object without done.
writeObject(t, client, "test", "growing", "part1", nil)
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "growing",
PollInterval: 10 * time.Millisecond,
})
// Read first chunk.
buf := make([]byte, 100)
n, err := r.Read(buf)
if err != nil {
t.Fatal(err)
}
if string(buf[:n]) != "part1" {
t.Fatalf("got %q, want %q", string(buf[:n]), "part1")
}
// Overwrite with more data and set done.
go func() {
time.Sleep(50 * time.Millisecond)
writeObject(t, client, "test", "growing", "part1part2", map[string]string{"done": "true"})
}()
rest, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if string(rest) != "part2" {
t.Fatalf("got %q, want %q", string(rest), "part2")
}
}
func TestReaderContextCancellation(t *testing.T) {
client := setup(t)
ctx, cancel := context.WithCancel(context.Background())
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "nonexistent",
PollInterval: 10 * time.Millisecond,
})
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
_, err := r.Read(make([]byte, 100))
if err == nil {
t.Fatal("expected error from cancelled context")
}
}
func TestIntegrationWriterReader(t *testing.T) {
client := setup(t)
ctx := context.Background()
w := NewWriter(ctx, WriterOptions{
Client: client,
Bucket: "test",
Object: "stream",
FlushInterval: 50 * time.Millisecond,
FlushSize: 1024 * 1024,
})
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "stream",
PollInterval: 10 * time.Millisecond,
})
// Write data in chunks.
chunks := []string{"hello ", "world ", "from ", "gcsbuf"}
var wg sync.WaitGroup
wg.Go(func() {
for _, chunk := range chunks {
if _, err := w.Write([]byte(chunk)); err != nil {
t.Errorf("write: %v", err)
return
}
time.Sleep(30 * time.Millisecond)
}
if err := w.Close(); err != nil {
t.Errorf("close: %v", err)
}
})
got, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
wg.Wait()
want := "hello world from gcsbuf"
if string(got) != want {
t.Fatalf("got %q, want %q", string(got), want)
}
}
func TestIntegrationMultipleFlushCyclesWithReader(t *testing.T) {
client := setup(t)
ctx := context.Background()
w := NewWriter(ctx, WriterOptions{
Client: client,
Bucket: "test",
Object: "multi",
FlushInterval: time.Hour,
FlushSize: 10, // flush every 10 bytes
})
r := NewReader(ctx, ReaderOptions{
Client: client,
Bucket: "test",
Object: "multi",
PollInterval: 10 * time.Millisecond,
})
// Write enough data to trigger multiple auto-flushes.
var wg sync.WaitGroup
wg.Go(func() {
for i := range 5 {
chunk := strings.Repeat("x", 15) // > flushSize, triggers auto-flush each time
if _, err := w.Write([]byte(chunk)); err != nil {
t.Errorf("write %d: %v", i, err)
return
}
}
if err := w.Close(); err != nil {
t.Errorf("close: %v", err)
}
})
got, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
wg.Wait()
want := strings.Repeat("x", 75)
if string(got) != want {
t.Fatalf("got len=%d, want len=%d", len(got), len(want))
}
}