mirror of https://github.com/docker/cli.git
feat: standardize error for prompt
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
parent
b8d5454963
commit
7c722c08d0
|
@ -2,6 +2,7 @@ package builder
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -67,9 +69,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("`builder prune` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
||||
|
|
|
@ -5,10 +5,8 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
|
@ -21,8 +19,5 @@ func TestBuilderPromptTermination(t *testing.T) {
|
|||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -54,9 +56,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("`container prune` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||
|
|
|
@ -4,12 +4,10 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
|
@ -22,8 +20,5 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
|||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -68,9 +70,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
warning = allImageWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("`image prune` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)
|
||||
|
|
|
@ -4,9 +4,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
@ -94,13 +95,18 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||
// when prompted, answer "Y" to confirm the prune.
|
||||
// will not be prompted if --force is used.
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader("Y\n"))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,8 +120,5 @@ func TestPrunePromptTermination(t *testing.T) {
|
|||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -50,9 +52,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !r {
|
||||
return "", errdefs.Cancelled(errors.New("`network prune` cancelled has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)
|
||||
|
|
|
@ -4,12 +4,10 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||
|
@ -22,8 +20,5 @@ func TestNetworkPrunePromptTermination(t *testing.T) {
|
|||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
|
@ -115,8 +114,5 @@ func TestNetworkRemovePromptTermination(t *testing.T) {
|
|||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"existing-network"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -63,11 +64,12 @@ func runUpgrade(ctx context.Context, dockerCli command.Cli, opts pluginOptions)
|
|||
|
||||
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?"); !r || err != nil {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "canceling upgrade request")
|
||||
}
|
||||
return errors.New("canceling upgrade request")
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r {
|
||||
return errdefs.Cancelled(errors.New("`plugin upgrade` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,9 @@ import (
|
|||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
|
@ -34,9 +32,6 @@ func TestUpgradePromptTermination(t *testing.T) {
|
|||
// need to set a remote address that does not match the plugin
|
||||
// reference sent by the `pluginInspectFunc`
|
||||
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@ import (
|
|||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -75,9 +77,13 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)); !r || err != nil {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r {
|
||||
return errdefs.Cancelled(errors.New("`system prune` has been cancelled"))
|
||||
}
|
||||
}
|
||||
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
|
||||
container.RunPrune,
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
|
@ -18,7 +17,7 @@ func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) {
|
|||
cli := test.NewFakeCli(&fakeClient{version: "1.30"})
|
||||
cmd := newPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.ErrorContains(t, cmd.Execute(), "`system prune` has been cancelled")
|
||||
expected := `WARNING! This will remove:
|
||||
- all stopped containers
|
||||
- all networks not used by at least one container
|
||||
|
@ -36,7 +35,7 @@ func TestPrunePromptFilters(t *testing.T) {
|
|||
cmd := newPruneCommand(cli)
|
||||
cmd.SetArgs([]string{"--filter", "until=24h", "--filter", "label=hello-world", "--filter", "label!=foo=bar", "--filter", "label=bar=baz"})
|
||||
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.ErrorContains(t, cmd.Execute(), "`system prune` has been cancelled")
|
||||
expected := `WARNING! This will remove:
|
||||
- all stopped containers
|
||||
- all networks not used by at least one container
|
||||
|
@ -69,8 +68,5 @@ func TestSystemPrunePromptTermination(t *testing.T) {
|
|||
})
|
||||
|
||||
cmd := newPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
|
@ -45,12 +46,10 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
|
|||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return errors.Wrap(err, "aborting action")
|
||||
return err
|
||||
}
|
||||
if !deleteRemote {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return nil
|
||||
return errdefs.Cancelled(errors.New("`trust revoke` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
|
@ -58,6 +57,8 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTrustRevokeCommand(t *testing.T) {
|
||||
revokeCancelledError := "`trust revoke` has been cancelled"
|
||||
|
||||
testCases := []struct {
|
||||
doc string
|
||||
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
|
||||
|
@ -69,7 +70,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
|||
doc: "OfflineErrors_Confirm",
|
||||
notaryRepository: notary.GetOfflineNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "OfflineErrors_Offline",
|
||||
|
@ -87,7 +89,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
|||
doc: "UninitializedErrors_Confirm",
|
||||
notaryRepository: notary.GetUninitializedNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "UninitializedErrors_NoTrustData",
|
||||
|
@ -105,7 +108,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
|||
doc: "EmptyNotaryRepo_Confirm",
|
||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||
args: []string{"reg-name.io/image"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
{
|
||||
doc: "EmptyNotaryRepo_NoSignedTags",
|
||||
|
@ -123,7 +127,8 @@ func TestTrustRevokeCommand(t *testing.T) {
|
|||
doc: "AllSigConfirmation",
|
||||
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
|
||||
args: []string{"alpine"},
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.",
|
||||
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] ",
|
||||
expectedErr: revokeCancelledError,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -136,9 +141,9 @@ func TestTrustRevokeCommand(t *testing.T) {
|
|||
cmd.SetOut(io.Discard)
|
||||
if tc.expectedErr != "" {
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
|
||||
return
|
||||
} else {
|
||||
assert.NilError(t, cmd.Execute())
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
|
||||
})
|
||||
}
|
||||
|
@ -159,10 +164,6 @@ func TestRevokeTrustPromptTermination(t *testing.T) {
|
|||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"example/trust-demo"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
|
||||
}
|
||||
|
|
|
@ -132,10 +132,12 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si
|
|||
}
|
||||
|
||||
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
|
||||
if err != nil || !ok {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
||||
return false, err
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
|
||||
Aborting action.
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/docker/docker/api/types/filters"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -75,9 +76,7 @@ func PrettyPrint(i any) string {
|
|||
}
|
||||
}
|
||||
|
||||
type PromptError error
|
||||
|
||||
var ErrPromptTerminated = PromptError(errors.New("prompt terminated"))
|
||||
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
|
||||
|
||||
// PromptForConfirmation requests and checks confirmation from the user.
|
||||
// This will display the provided message followed by ' [y/N] '. If the user
|
||||
|
@ -123,6 +122,8 @@ func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, m
|
|||
|
||||
select {
|
||||
case <-notifyCtx.Done():
|
||||
// print a newline on termination
|
||||
fmt.Fprintln(outs, "")
|
||||
return false, ErrPromptTerminated
|
||||
case r := <-result:
|
||||
return r, nil
|
||||
|
|
|
@ -32,10 +32,6 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||
if err != nil {
|
||||
if errdefs.IsCancelled(err) {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
|
@ -81,8 +77,12 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
|||
warning = allVolumesWarning
|
||||
}
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", errdefs.Cancelled(errors.New("user cancelled operation"))
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if !r {
|
||||
return 0, "", errdefs.Cancelled(errors.New("`volume prune` has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,7 @@ func TestVolumePrunePromptYes(t *testing.T) {
|
|||
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden")
|
||||
}
|
||||
|
@ -171,7 +172,8 @@ func TestVolumePrunePromptNo(t *testing.T) {
|
|||
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input))))
|
||||
cmd := NewPruneCommand(cli)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
cmd.SetArgs([]string{})
|
||||
assert.ErrorContains(t, cmd.Execute(), "`volume prune` has been cancelled")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden")
|
||||
}
|
||||
}
|
||||
|
@ -196,7 +198,7 @@ func TestVolumePrunePromptTerminate(t *testing.T) {
|
|||
})
|
||||
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, nil)
|
||||
|
||||
cmd.SetArgs([]string{})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/docker/cli/cli/version"
|
||||
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -43,6 +44,9 @@ func main() {
|
|||
}
|
||||
os.Exit(sterr.StatusCode)
|
||||
}
|
||||
if errdefs.IsCancelled(err) {
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
package global
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/docker/cli/e2e/testutils"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/icmd"
|
||||
"gotest.tools/v3/poll"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
||||
|
@ -65,3 +76,185 @@ func TestTCPSchemeUsesHTTPProxyEnv(t *testing.T) {
|
|||
assert.Equal(t, strings.TrimSpace(result.Stdout()), "99.99.9")
|
||||
assert.Equal(t, received, "docker.acme.example.com:2376")
|
||||
}
|
||||
|
||||
// Test that the prompt command exits with 0
|
||||
// when the user sends SIGINT/SIGTERM to the process
|
||||
func TestPromptExitCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
dir := fixtures.SetupConfigFile(t)
|
||||
t.Cleanup(dir.Remove)
|
||||
|
||||
defaultCmdOpts := []icmd.CmdOp{
|
||||
fixtures.WithConfig(dir.Path()),
|
||||
fixtures.WithNotary,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
run func(t *testing.T) icmd.Cmd
|
||||
}{
|
||||
{
|
||||
name: "volume prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "volume", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "network", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "container prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "container", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "image", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "system prune",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "system", "prune")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "revoke trust",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
return icmd.Command("docker", "trust", "revoke", "example/trust-demo")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin install",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||
|
||||
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginDir.Remove)
|
||||
|
||||
plugin := "registry:5000/plugin-content-trust-install:latest"
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin, pluginDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
return icmd.Command("docker", "plugin", "install", plugin)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plugin upgrade",
|
||||
run: func(t *testing.T) icmd.Cmd {
|
||||
t.Helper()
|
||||
skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
|
||||
|
||||
pluginLatestDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginLatestDir.Remove)
|
||||
pluginNextDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginNextDir.Remove)
|
||||
|
||||
plugin := "registry:5000/plugin-content-trust-upgrade"
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin+":latest", pluginLatestDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCommand("docker", "plugin", "create", plugin+":next", pluginNextDir.Path()).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
icmd.RunCmd(icmd.Command("docker", "plugin", "install", "--disable", "--grant-all-permissions", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success)
|
||||
return icmd.Command("docker", "plugin", "upgrade", plugin+":latest", plugin+":next")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run("case="+tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
writeDone := make(chan struct{})
|
||||
w := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
writeDone <- struct{}{}
|
||||
})
|
||||
|
||||
drainChCtx, drainChCtxCancel := context.WithCancel(ctx)
|
||||
t.Cleanup(drainChCtxCancel)
|
||||
|
||||
drainChannel(drainChCtx, writeDone)
|
||||
|
||||
r, _ := io.Pipe()
|
||||
defer r.Close()
|
||||
result := icmd.StartCmd(tc.run(t),
|
||||
append(defaultCmdOpts,
|
||||
icmd.WithStdout(w),
|
||||
icmd.WithStderr(w),
|
||||
icmd.WithStdin(r))...)
|
||||
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return poll.Error(ctx.Err())
|
||||
default:
|
||||
|
||||
if err := bufioWriter.Flush(); err != nil {
|
||||
return poll.Continue(err.Error())
|
||||
}
|
||||
if strings.Contains(buf.String(), "[y/N]") {
|
||||
return poll.Success()
|
||||
}
|
||||
|
||||
return poll.Continue("command did not prompt for confirmation, instead prompted:\n%s\n", buf.String())
|
||||
}
|
||||
}, poll.WithDelay(100*time.Millisecond), poll.WithTimeout(1*time.Second))
|
||||
|
||||
drainChCtxCancel()
|
||||
|
||||
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGINT))
|
||||
|
||||
proc, err := result.Cmd.Process.Wait()
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, proc.ExitCode(), 0, "expected exit code to be 0, got %d", proc.ExitCode())
|
||||
|
||||
processCtx, processCtxCancel := context.WithTimeout(ctx, time.Second)
|
||||
t.Cleanup(processCtxCancel)
|
||||
|
||||
select {
|
||||
case <-processCtx.Done():
|
||||
t.Fatal("timed out waiting for new line after process exit")
|
||||
case <-writeDone:
|
||||
buf.Reset()
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, buf.String(), "\n", "expected a new line after the process exits from SIGINT")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/docker/cli/e2e/testutils"
|
||||
"github.com/docker/cli/internal/test/environment"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/icmd"
|
||||
"gotest.tools/v3/skip"
|
||||
)
|
||||
|
@ -31,8 +25,11 @@ func TestInstallWithContentTrust(t *testing.T) {
|
|||
dir := fixtures.SetupConfigFile(t)
|
||||
defer dir.Remove()
|
||||
|
||||
pluginDir := preparePluginDir(t)
|
||||
defer pluginDir.Remove()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
pluginDir := testutils.SetupPlugin(t, ctx)
|
||||
t.Cleanup(pluginDir.Remove)
|
||||
|
||||
icmd.RunCommand("docker", "plugin", "create", pluginName, pluginDir.Path()).Assert(t, icmd.Success)
|
||||
result := icmd.RunCmd(icmd.Command("docker", "plugin", "push", pluginName),
|
||||
|
@ -73,46 +70,3 @@ func TestInstallWithContentTrustUntrusted(t *testing.T) {
|
|||
Err: "Error: remote trust data does not exist",
|
||||
})
|
||||
}
|
||||
|
||||
func preparePluginDir(t *testing.T) *fs.Dir {
|
||||
t.Helper()
|
||||
p := &types.PluginConfig{
|
||||
Interface: types.PluginConfigInterface{
|
||||
Socket: "basic.sock",
|
||||
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
||||
},
|
||||
Entrypoint: []string{"/basic"},
|
||||
}
|
||||
configJSON, err := json.Marshal(p)
|
||||
assert.NilError(t, err)
|
||||
|
||||
binPath, err := ensureBasicPluginBin()
|
||||
assert.NilError(t, err)
|
||||
|
||||
dir := fs.NewDir(t, "plugin_test",
|
||||
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
||||
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
||||
)
|
||||
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
||||
return dir
|
||||
}
|
||||
|
||||
func ensureBasicPluginBin() (string, error) {
|
||||
name := "docker-basic-plugin"
|
||||
p, err := exec.LookPath(name)
|
||||
if err == nil {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", name)
|
||||
cmd := exec.Command(goBin, "build", "-o", installPath, "./basic")
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
||||
}
|
||||
return installPath, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/icmd"
|
||||
)
|
||||
|
||||
//go:embed plugins/*
|
||||
var plugins embed.FS
|
||||
|
||||
// SetupPlugin builds a plugin and creates a temporary
|
||||
// directory with the plugin's config.json and rootfs.
|
||||
func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir {
|
||||
t.Helper()
|
||||
|
||||
p := &types.PluginConfig{
|
||||
Linux: types.PluginConfigLinux{
|
||||
Capabilities: []string{"CAP_SYS_ADMIN"},
|
||||
},
|
||||
Interface: types.PluginConfigInterface{
|
||||
Socket: "basic.sock",
|
||||
Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
|
||||
},
|
||||
Entrypoint: []string{"/basic"},
|
||||
}
|
||||
configJSON, err := json.Marshal(p)
|
||||
assert.NilError(t, err)
|
||||
|
||||
binPath, err := buildPlugin(t, ctx)
|
||||
assert.NilError(t, err)
|
||||
|
||||
dir := fs.NewDir(t, "plugin_test",
|
||||
fs.WithFile("config.json", string(configJSON), fs.WithMode(0o644)),
|
||||
fs.WithDir("rootfs", fs.WithMode(0o755)),
|
||||
)
|
||||
|
||||
icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success)
|
||||
return dir
|
||||
}
|
||||
|
||||
// buildPlugin uses Go to build a plugin from one of the source files in the plugins directory.
|
||||
// It returns the path to the built plugin binary.
|
||||
// To allow for multiple plugins to be built in parallel, the plugin is compiled with a unique
|
||||
// identifier in the binary. This is done by setting a linker flag with the -ldflags option.
|
||||
func buildPlugin(t *testing.T, ctx context.Context) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
randomName, err := randomString()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
goBin, err := exec.LookPath("/usr/local/go/bin/go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
installPath := filepath.Join(os.Getenv("GOPATH"), "bin", randomName)
|
||||
|
||||
pluginContent, err := plugins.ReadFile("plugins/basic.go")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := fs.NewDir(t, "plugin_build")
|
||||
if err := os.WriteFile(dir.Join("main.go"), pluginContent, 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dir.Remove()
|
||||
|
||||
cmd := exec.CommandContext(ctx, goBin, "build", "-ldflags",
|
||||
fmt.Sprintf("-X 'main.UNIQUEME=%s'", randomName),
|
||||
"-o", installPath, dir.Join("main.go"))
|
||||
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
|
||||
}
|
||||
|
||||
return installPath, nil
|
||||
}
|
||||
|
||||
func randomString() (string, error) {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b), nil
|
||||
}
|
|
@ -9,6 +9,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// set by the compile flags to get around content sha being the same
|
||||
var (
|
||||
UNIQUEME string
|
||||
)
|
||||
|
||||
func main() {
|
||||
p, err := filepath.Abs(filepath.Join("run", "docker", "plugins"))
|
||||
if err != nil {
|
|
@ -7,12 +7,13 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli, assertFunc func(*testing.T, error)) {
|
||||
func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli *FakeCli) {
|
||||
t.Helper()
|
||||
|
||||
errChan := make(chan error)
|
||||
|
@ -73,11 +74,6 @@ func TerminatePrompt(ctx context.Context, t *testing.T, cmd *cobra.Command, cli
|
|||
t.Logf("command stderr:\n%s\n", cli.ErrBuffer().String())
|
||||
t.Fatalf("command %s did not return after SIGINT", cmd.Name())
|
||||
case err := <-errChan:
|
||||
if assertFunc != nil {
|
||||
assertFunc(t, err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
}
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,22 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
// WriterWithHook is an io.Writer that calls a hook function
|
||||
// after every write.
|
||||
// This is useful in testing to wait for a write to complete,
|
||||
// or to check what was written.
|
||||
// To create a WriterWithHook use the NewWriterWithHook function.
|
||||
type WriterWithHook struct {
|
||||
type writerWithHook struct {
|
||||
actualWriter io.Writer
|
||||
hook func([]byte)
|
||||
}
|
||||
|
||||
// Write writes p to the actual writer and then calls the hook function.
|
||||
func (w *WriterWithHook) Write(p []byte) (n int, err error) {
|
||||
func (w *writerWithHook) Write(p []byte) (n int, err error) {
|
||||
defer w.hook(p)
|
||||
return w.actualWriter.Write(p)
|
||||
}
|
||||
|
||||
var _ io.Writer = (*WriterWithHook)(nil)
|
||||
var _ io.Writer = (*writerWithHook)(nil)
|
||||
|
||||
// NewWriterWithHook returns a new WriterWithHook that still writes to the actualWriter
|
||||
// but also calls the hook function after every write.
|
||||
// The hook function is useful for testing, or waiting for a write to complete.
|
||||
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *WriterWithHook {
|
||||
return &WriterWithHook{actualWriter: actualWriter, hook: hook}
|
||||
// NewWriterWithHook returns a io.Writer that still
|
||||
// writes to the actualWriter but also calls the hook function
|
||||
// after every write. It is useful to use this function when
|
||||
// you need to wait for a writer to complete writing inside a test.
|
||||
func NewWriterWithHook(actualWriter io.Writer, hook func([]byte)) *writerWithHook {
|
||||
return &writerWithHook{actualWriter: actualWriter, hook: hook}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue