Files
cli/internal/test/cmd.go
T
Hamir dd50814004 fix: flaky TestPrunePromptTermination test
TerminatePrompt created a pipe for the prompt's stdin but discarded the
write end. Once the garbage collector finalized it, the write end was
closed, the prompt read EOF and treated it as "no", and the command
returned "prune has been cancelled" instead of prompt.ErrTerminated.

This keeps both ends of the pipe referenced and closes them in t.Cleanup.

This change also drops the OutBuffer().Len() check:
the writer hook only fires after
the write has completed, so the check proved nothing, and it raced with
the prompt's later writes under -race.

This change also applies the same fix to the identical copy in cmd/docker-trust.

Signed-off-by: Hamir <hirehamir@gmail.com>
2026-10-22 10:04:59 -07:00

85 lines
1.9 KiB
Go

package test
import (
"context"
"os"
"testing"
"time"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli) {
t.Helper()
errChan := make(chan error)
defer close(errChan)
// wrap the out stream to detect when the prompt is ready
writerHookChan := make(chan struct{})
defer close(writerHookChan)
outStream := streams.NewOut(NewWriterWithHook(cli.OutBuffer(), func(p []byte) {
writerHookChan <- struct{}{}
}))
cli.SetOut(outStream)
r, w, err := os.Pipe()
assert.NilError(t, err)
t.Cleanup(func() {
_ = w.Close()
_ = r.Close()
})
cli.SetIn(streams.NewIn(r))
notifyCtx, notifyCancel := context.WithCancel(ctx)
t.Cleanup(notifyCancel)
go func() {
errChan <- cmd.ExecuteContext(notifyCtx)
}()
writeCtx, writeCancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer writeCancel()
// wait for the prompt to be ready
select {
case <-writeCtx.Done():
t.Fatalf("command %s did not write prompt to stdout", cmd.Name())
case <-writerHookChan:
// drain the channel for future buffer writes
go func() {
for {
select {
case <-ctx.Done():
return
case <-writerHookChan:
}
}
}()
}
// a small delay to ensure the plugin is prompting
time.Sleep(100 * time.Microsecond)
errCtx, errCancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer errCancel()
// sigint and sigterm are caught by the prompt
// this allows us to gracefully exit the prompt with a 0 exit code
notifyCancel()
select {
case <-errCtx.Done():
t.Logf("command stdout:\n%s\n", cli.OutBuffer().String())
t.Logf("command stderr:\n%s\n", cli.ErrBuffer().String())
t.Fatalf("command %s did not return after SIGINT", cmd.Name())
case err := <-errChan:
assert.ErrorIs(t, err, prompt.ErrTerminated)
}
}