mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 04:16:27 +00:00
93 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|