Add local batched Git blob reader
This commit is contained in:
195
internal/git/blob_reader_test.go
Normal file
195
internal/git/blob_reader_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBlobReaderReadsLooseAndPackedObjects(t *testing.T) {
|
||||
for _, packed := range []bool{false, true} {
|
||||
name := "loose"
|
||||
if packed {
|
||||
name = "packed"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
fixture := newRemoteFixture(t)
|
||||
binary := []byte{0, '\n', 0xff, 'b', '\n', 0}
|
||||
writeRepoFile(t, fixture.work, "empty.dat", "")
|
||||
writeRepoFile(t, fixture.work, "binary.dat", string(binary))
|
||||
gitRun(t, fixture.work, "add", ".")
|
||||
gitRun(t, fixture.work, "commit", "-m", "add binary fixtures")
|
||||
want := map[string][]byte{
|
||||
gitText(t, fixture.work, "rev-parse", "HEAD:source.txt"): []byte("candidate"),
|
||||
gitText(t, fixture.work, "rev-parse", "HEAD:empty.dat"): {},
|
||||
gitText(t, fixture.work, "rev-parse", "HEAD:binary.dat"): binary,
|
||||
}
|
||||
if packed {
|
||||
gitRun(t, fixture.work, "gc", "--aggressive", "--prune=now")
|
||||
}
|
||||
reader := NewBlobReader(t.Context(), fixture.work)
|
||||
defer reader.Close()
|
||||
for repeat := 0; repeat < 2; repeat++ {
|
||||
for objectID, expected := range want {
|
||||
got, err := reader.Read(t.Context(), objectID)
|
||||
if err != nil {
|
||||
t.Fatalf("Read(%s) error = %v", objectID, err)
|
||||
}
|
||||
if !bytes.Equal(got, expected) {
|
||||
t.Errorf("Read(%s) = %v, want %v", objectID, got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderMissingObjectKeepsBatchSessionUsable(t *testing.T) {
|
||||
fixture := newRemoteFixture(t)
|
||||
objectID := gitText(t, fixture.work, "rev-parse", "HEAD:source.txt")
|
||||
reader := NewBlobReader(t.Context(), fixture.work)
|
||||
defer reader.Close()
|
||||
missing := strings.Repeat("0", 40)
|
||||
if _, err := reader.Read(t.Context(), missing); err == nil || !strings.Contains(err.Error(), "missing") {
|
||||
t.Fatalf("Read(missing) error = %v, want contextual missing-object error", err)
|
||||
}
|
||||
contents, err := reader.Read(t.Context(), objectID)
|
||||
if err != nil || string(contents) != "candidate" {
|
||||
t.Fatalf("Read(existing) = %q, %v after missing request", contents, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderPreservesHexCaseAndArbitraryBodyBytes(t *testing.T) {
|
||||
installFakeGit(t, `while IFS= read -r object; do
|
||||
upper=$(printf '%s' "$object" | tr 'a-f' 'A-F')
|
||||
printf '%s blob 4\n' "$upper"
|
||||
printf 'a\000b\n\n'
|
||||
done`)
|
||||
objectID := strings.Repeat("abcdef0123", 4)
|
||||
reader := NewBlobReader(t.Context(), t.TempDir())
|
||||
contents, err := reader.Read(t.Context(), objectID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := []byte{'a', 0, 'b', '\n'}; !bytes.Equal(contents, want) {
|
||||
t.Fatalf("Read() = %v, want %v", contents, want)
|
||||
}
|
||||
if err := reader.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderRejectsInvalidHeadersAndFraming(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
response string
|
||||
want string
|
||||
}{
|
||||
{name: "wrong object", response: `printf '%040d blob 0\n\n' 0`, want: "does not match request"},
|
||||
{name: "wrong type", response: `printf '%s tree 0\n\n' "$object"`, want: `has type "tree"`},
|
||||
{name: "invalid size", response: `printf '%s blob -1\n' "$object"`, want: "invalid Git blob size"},
|
||||
{name: "invalid framing", response: `printf '%s blob 0\nx' "$object"`, want: "invalid Git blob framing"},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
installFakeGit(t, "IFS= read -r object\n"+testCase.response)
|
||||
reader := NewBlobReader(t.Context(), t.TempDir())
|
||||
_, err := reader.Read(t.Context(), strings.Repeat("d", 40))
|
||||
if err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("Read() error = %v, want %q", err, testCase.want)
|
||||
}
|
||||
if closeErr := reader.Close(); !strings.Contains(closeErr.Error(), testCase.want) {
|
||||
t.Fatalf("Close() error = %v, want original protocol error", closeErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderTruncatedBodyAbortsAndKeepsOriginalCause(t *testing.T) {
|
||||
installFakeGit(t, `IFS= read -r object
|
||||
printf '%s blob 8\n' "$object"
|
||||
printf 'short'
|
||||
exit 0`)
|
||||
reader := NewBlobReader(t.Context(), t.TempDir())
|
||||
_, err := reader.Read(t.Context(), strings.Repeat("a", 40))
|
||||
if !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
t.Fatalf("Read() error = %v, want truncated-body cause", err)
|
||||
}
|
||||
if closeErr := reader.Close(); !errors.Is(closeErr, io.ErrUnexpectedEOF) {
|
||||
t.Fatalf("Close() error = %v, want original read cause", closeErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderCancellationDuringBodyReadReapsChild(t *testing.T) {
|
||||
ready := filepath.Join(t.TempDir(), "ready")
|
||||
installFakeGit(t, `IFS= read -r object
|
||||
printf '%s blob 1048576\npartial' "$object"
|
||||
: > "$RELEASEKIT_GIT_READER_READY"
|
||||
exec sleep 60`)
|
||||
t.Setenv("RELEASEKIT_GIT_READER_READY", ready)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
reader := NewBlobReader(ctx, t.TempDir())
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := reader.Read(ctx, strings.Repeat("b", 40))
|
||||
result <- err
|
||||
}()
|
||||
waitForFile(t, ready)
|
||||
cancel()
|
||||
select {
|
||||
case err := <-result:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Read() error = %v, want context cancellation", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Read() did not return after cancellation")
|
||||
}
|
||||
if err := reader.Close(); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("Close() error = %v, want original cancellation cause", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobReaderPreservesGitFailureAndStderr(t *testing.T) {
|
||||
installFakeGit(t, `printf 'fixture Git failure\n' >&2
|
||||
exit 23`)
|
||||
reader := NewBlobReader(t.Context(), t.TempDir())
|
||||
_, err := reader.Read(t.Context(), strings.Repeat("c", 40))
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.ExitCode() != 23 {
|
||||
t.Fatalf("Read() error = %v, want Git exit code 23", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "fixture Git failure") {
|
||||
t.Fatalf("Read() error = %v, want captured Git stderr", err)
|
||||
}
|
||||
}
|
||||
|
||||
func installFakeGit(t *testing.T, script string) {
|
||||
t.Helper()
|
||||
directory := t.TempDir()
|
||||
path := filepath.Join(directory, "git")
|
||||
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+script+"\n"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
func waitForFile(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatal(fmt.Errorf("inspect readiness signal: %w", err))
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("child did not create readiness signal")
|
||||
}
|
||||
Reference in New Issue
Block a user