Files
hauler/pkg/artifacts/file/context_test.go
T
2026-08-05 16:34:41 -04:00

93 lines
2.8 KiB
Go

package file_test
import (
"bytes"
"context"
"io"
"net/url"
"testing"
"time"
"hauler.dev/go/hauler/v2/pkg/artifacts"
"hauler.dev/go/hauler/v2/pkg/artifacts/file"
"hauler.dev/go/hauler/v2/pkg/getter"
)
// blockingGetter's Open blocks on a channel controlled by the test until
// released, unless ctx is cancelled first -- letting tests distinguish
// "the real per-call ctx reached Open" from "compute() built the layer with
// some other, uncancellable ctx" (the context.TODO() bug this file guards
// against).
type blockingGetter struct {
release chan struct{}
data []byte
}
func (g *blockingGetter) Open(ctx context.Context, u *url.URL) (io.ReadCloser, error) {
select {
case <-g.release:
return io.NopCloser(bytes.NewReader(g.data)), nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (g *blockingGetter) Detect(u *url.URL) bool { return true }
func (g *blockingGetter) Name(u *url.URL) string { return "blocked" }
func (g *blockingGetter) Config(u *url.URL) artifacts.Config {
return artifacts.ToConfig(struct {
Reference string `json:"reference"`
}{u.String()}, artifacts.WithConfigMediaType("application/vnd.test.config"))
}
func newBlockingClient(g *blockingGetter) *getter.Client {
return &getter.Client{
Options: getter.ClientOptions{},
Getters: map[string]getter.Getter{"mock": g},
}
}
// TestFile_WithContext_CancellationAbortsInFlightFetch proves that the ctx
// passed via file.WithContext is the ctx that actually reaches the getter's
// Open call -- not some internal context.TODO() that can never be
// cancelled. Without this wiring, cancelling ctx while compute() is
// blocked inside Open would have no effect and this test would time out.
func TestFile_WithContext_CancellationAbortsInFlightFetch(t *testing.T) {
g := &blockingGetter{release: make(chan struct{})}
t.Cleanup(func() { close(g.release) })
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
f := file.NewFile("mock://source", file.WithClient(newBlockingClient(g)), file.WithContext(ctx))
done := make(chan error, 1)
go func() {
_, err := f.Layers()
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("expected an error from Layers() after ctx cancellation, got nil")
}
case <-time.After(5 * time.Second):
t.Fatal("Layers() did not return within 5s of ctx cancellation... file.WithContext's ctx is not reaching the getter")
}
}
// TestFile_WithContext_DefaultsToBackground proves NewFile without
// file.WithContext still works end-to-end (the zero-value/default path),
// matching pre-existing behavior for every caller that never sets it.
func TestFile_WithContext_DefaultsToBackground(t *testing.T) {
f := file.NewFile(filename, file.WithClient(mc))
if _, err := f.Layers(); err != nil {
t.Fatalf("Layers() with default context: %v", err)
}
}