Files
buildx/driver/kubernetes/execconn/execconn_test.go
T

119 lines
3.2 KiB
Go

package execconn
import (
"context"
"errors"
"io"
"testing"
"time"
"github.com/stretchr/testify/require"
"k8s.io/client-go/tools/remotecommand"
)
// fakeExecutor is a fake remotecommand.Executor whose StreamWithContext blocks until
// unblock is closed, then returns err. It stands in for the real SPDY exec stream
// to a builder pod, so the pipe-closing behavior in newExecConn can be tested
// without a real Kubernetes API server.
type fakeExecutor struct {
unblock chan struct{}
err error
}
func (f *fakeExecutor) Stream(_ remotecommand.StreamOptions) error {
panic("unimplemented")
}
func (f *fakeExecutor) StreamWithContext(ctx context.Context, _ remotecommand.StreamOptions) error {
select {
case <-f.unblock:
return f.err
case <-ctx.Done():
return context.Cause(ctx)
}
}
func TestNewExecConnPropagatesStreamEnd(t *testing.T) {
t.Run("stream ends with an error", func(t *testing.T) {
streamErr := errors.New("exec stream terminated")
fe := &fakeExecutor{
unblock: make(chan struct{}),
err: streamErr,
}
conn := newExecConn(context.Background(), fe)
close(fe.unblock)
_, err := conn.Read(make([]byte, 16))
require.ErrorIs(t, err, streamErr)
_, err = conn.Write([]byte("test"))
require.ErrorIs(t, err, streamErr)
})
t.Run("stream ends with no error", func(t *testing.T) {
fe := &fakeExecutor{unblock: make(chan struct{})}
conn := newExecConn(context.Background(), fe)
close(fe.unblock) // StreamWithContext returns nil
_, err := conn.Read(make([]byte, 16))
require.ErrorIs(t, err, io.EOF)
_, err = conn.Write([]byte("test"))
require.ErrorIs(t, err, io.ErrClosedPipe)
})
t.Run("stream still active: reads stay blocked, not closed early", func(t *testing.T) {
fe := &fakeExecutor{unblock: make(chan struct{})}
conn := newExecConn(context.Background(), fe)
defer close(fe.unblock)
done := make(chan struct{})
go func() {
buf := make([]byte, 16)
_, _ = conn.Read(buf) //nolint:errcheck
close(done)
}()
select {
case <-done:
t.Fatal("Read returned before the exec stream ended; it should still be blocked")
case <-time.After(200 * time.Millisecond):
// expected: still blocked, exactly like a real in-progress build
}
})
t.Run("stream still active: writes stay blocked, not closed early", func(t *testing.T) {
fe := &fakeExecutor{unblock: make(chan struct{})}
conn := newExecConn(context.Background(), fe)
defer close(fe.unblock)
done := make(chan struct{})
go func() {
_, _ = conn.Write([]byte("test")) //nolint:errcheck
close(done)
}()
select {
case <-done:
t.Fatal("Write returned before the exec stream ended; it should still be blocked")
case <-time.After(200 * time.Millisecond):
// expected: still blocked, exactly like a real in-progress build
}
})
t.Run("stream cancelled by context", func(t *testing.T) {
fe := &fakeExecutor{unblock: make(chan struct{})}
ctx, cancel := context.WithCancelCause(context.Background())
conn := newExecConn(ctx, fe)
defer close(fe.unblock)
cancel(context.Canceled)
_, err := conn.Read(make([]byte, 16))
require.ErrorIs(t, err, context.Canceled)
_, err = conn.Write([]byte("test"))
require.ErrorIs(t, err, context.Canceled)
})
}