Files
buildx/util/sourcemeta/resolver_test.go
T
Tonis Tiigi dce5289722 policy: separate sourcemetaresolver utility to reusable pkg
Turn the current utility used by policy helper commands that
allows getting gateway.Client lazily into reusable package.

This is preparation for using this utility also in the policy
verification, outside of commands.

Only expose SourceMetaResolver as other methods of gateway client
are not needed atm by any callers.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-02-13 13:59:33 -08:00

194 lines
4.8 KiB
Go

package sourcemeta
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/moby/buildkit/client/llb/sourceresolver"
"github.com/moby/buildkit/solver/pb"
"github.com/stretchr/testify/require"
)
type fakeMetaResolver struct {
calls atomic.Int32
resp *sourceresolver.MetaResponse
err error
}
func (f *fakeMetaResolver) ResolveSourceMetadata(ctx context.Context, op *pb.SourceOp, opt sourceresolver.Opt) (*sourceresolver.MetaResponse, error) {
f.calls.Add(1)
return f.resp, f.err
}
func TestResolverCloseNoopBeforeResolve(t *testing.T) {
t.Parallel()
var called atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
called.Add(1)
return nil
})
require.NoError(t, r.Close())
require.EqualValues(t, 0, called.Load())
}
func TestResolverResolveOpensOnce(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
op := &pb.SourceOp{}
_, err := r.ResolveSourceMetadata(t.Context(), op, sourceresolver.Opt{})
require.NoError(t, err)
_, err = r.ResolveSourceMetadata(t.Context(), op, sourceresolver.Opt{})
require.NoError(t, err)
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, 2, mr.calls.Load())
require.NoError(t, r.Close())
}
func TestResolverCloseAfterOpenCancelsBuild(t *testing.T) {
t.Parallel()
var canceled atomic.Bool
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
ready <- &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
<-ctx.Done()
canceled.Store(true)
return context.Cause(ctx)
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.NoError(t, err)
require.NoError(t, r.Close())
require.True(t, canceled.Load())
}
func TestResolverOpenFailureIsSticky(t *testing.T) {
t.Parallel()
expected := errors.New("boom")
var runs atomic.Int32
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
return expected
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, expected)
_, err = r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, expected)
require.EqualValues(t, 1, runs.Load())
require.ErrorIs(t, r.Close(), expected)
}
func TestResolverCloseIgnoresTerminalContextErrors(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
err error
}{
{name: "canceled", err: context.Canceled},
{name: "deadline", err: context.DeadlineExceeded},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
return tc.err
})
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, tc.err)
require.NoError(t, r.Close())
})
}
}
func TestResolverConcurrentResolveUsesSingleOpen(t *testing.T) {
t.Parallel()
var runs atomic.Int32
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
runs.Add(1)
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
const n = 16
errCh := make(chan error, n)
var wg sync.WaitGroup
wg.Add(n)
for range n {
go func() {
defer wg.Done()
_, err := r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
errCh <- err
}()
}
wg.Wait()
close(errCh)
for err := range errCh {
require.NoError(t, err)
}
require.EqualValues(t, 1, runs.Load())
require.EqualValues(t, n, mr.calls.Load())
done := make(chan struct{})
closeErr := make(chan error, 1)
go func() {
defer close(done)
closeErr <- r.Close()
}()
select {
case <-done:
require.NoError(t, <-closeErr)
case <-time.After(2 * time.Second):
t.Fatal("close timed out")
}
}
func TestResolverFirstCanceledContextDoesNotPoisonFutureCalls(t *testing.T) {
t.Parallel()
mr := &fakeMetaResolver{resp: &sourceresolver.MetaResponse{}}
started := make(chan struct{})
release := make(chan struct{})
r := newWithRun(func(ctx context.Context, ready chan<- sourceresolver.MetaResolver) error {
close(started)
<-release
ready <- mr
<-ctx.Done()
return context.Cause(ctx)
})
canceledCtx, cancel := context.WithCancelCause(t.Context())
cancel(context.Canceled)
_, err := r.ResolveSourceMetadata(canceledCtx, &pb.SourceOp{}, sourceresolver.Opt{})
require.ErrorIs(t, err, context.Canceled)
<-started
close(release)
_, err = r.ResolveSourceMetadata(t.Context(), &pb.SourceOp{}, sourceresolver.Opt{})
require.NoError(t, err)
require.NoError(t, r.Close())
}