From 16aa994255596a717de265d1b5da558bc13b262e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 26 Sep 2024 18:48:03 +0200 Subject: [PATCH 1/2] cli/command/container: add unit tests for container restart Signed-off-by: Sebastiaan van Stijn --- cli/command/container/client_test.go | 8 +++ cli/command/container/restart.go | 2 + cli/command/container/restart_test.go | 85 +++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 cli/command/container/restart_test.go diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index deae11a73b7f..8b3b0cd97ea9 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -35,6 +35,7 @@ type fakeClient struct { containerExportFunc func(string) (io.ReadCloser, error) containerExecResizeFunc func(id string, options container.ResizeOptions) error containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error + containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error containerKillFunc func(ctx context.Context, containerID, signal string) error containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) @@ -175,6 +176,13 @@ func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.A return container.PruneReport{}, nil } +func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error { + if f.containerRestartFunc != nil { + return f.containerRestartFunc(ctx, containerID, options) + } + return nil +} + func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { if f.containerAttachFunc != nil { return f.containerAttachFunc(ctx, containerID, options) diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 8e7c0d4a026f..950bb81e0453 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -55,6 +55,8 @@ func runRestart(ctx context.Context, dockerCli command.Cli, opts *restartOptions if opts.timeoutChanged { timeout = &opts.timeout } + + // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" for _, name := range opts.containers { err := dockerCli.Client().ContainerRestart(ctx, name, container.StopOptions{ Signal: opts.signal, diff --git a/cli/command/container/restart_test.go b/cli/command/container/restart_test.go new file mode 100644 index 000000000000..8a5a69c1bcf7 --- /dev/null +++ b/cli/command/container/restart_test.go @@ -0,0 +1,85 @@ +package container + +import ( + "context" + "io" + "sort" + "sync" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/errdefs" + "github.com/pkg/errors" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestRestart(t *testing.T) { + for _, tc := range []struct { + name string + args []string + restarted []string + expectedOpts container.StopOptions + expectedErr string + }{ + { + name: "without options", + args: []string{"container-1", "container-2"}, + restarted: []string{"container-1", "container-2"}, + }, + { + name: "with unknown container", + args: []string{"container-1", "nosuchcontainer", "container-2"}, + expectedErr: "no such container", + restarted: []string{"container-1", "container-2"}, + }, + { + name: "with -t", + args: []string{"-t", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + restarted: []string{"container-1"}, + }, + { + name: "with --time", + args: []string{"--time", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + restarted: []string{"container-1"}, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var restarted []string + mutex := new(sync.Mutex) + + cli := test.NewFakeCli(&fakeClient{ + containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) + if containerID == "nosuchcontainer" { + return errdefs.NotFound(errors.New("Error: no such container: " + containerID)) + } + + // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" + mutex.Lock() + restarted = append(restarted, containerID) + mutex.Unlock() + return nil + }, + Version: "1.36", + }) + cmd := NewRestartCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + + err := cmd.Execute() + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } else { + assert.Check(t, is.Nil(err)) + } + sort.Strings(restarted) + assert.Check(t, is.DeepEqual(restarted, tc.restarted)) + }) + } +} From ac502b5909e644d017c145402c62479568c98702 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 26 Sep 2024 18:53:10 +0200 Subject: [PATCH 2/2] cli/command/container: add unit tests for container stop Signed-off-by: Sebastiaan van Stijn --- cli/command/container/client_test.go | 8 +++ cli/command/container/stop_test.go | 86 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 cli/command/container/stop_test.go diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index 8b3b0cd97ea9..adc39f9bedb4 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -36,6 +36,7 @@ type fakeClient struct { containerExecResizeFunc func(id string, options container.ResizeOptions) error containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error + containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error containerKillFunc func(ctx context.Context, containerID, signal string) error containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) @@ -183,6 +184,13 @@ func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, o return nil } +func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { + if f.containerStopFunc != nil { + return f.containerStopFunc(ctx, containerID, options) + } + return nil +} + func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { if f.containerAttachFunc != nil { return f.containerAttachFunc(ctx, containerID, options) diff --git a/cli/command/container/stop_test.go b/cli/command/container/stop_test.go new file mode 100644 index 000000000000..44893d68abd9 --- /dev/null +++ b/cli/command/container/stop_test.go @@ -0,0 +1,86 @@ +package container + +import ( + "context" + "io" + "sort" + "sync" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/errdefs" + "github.com/pkg/errors" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestStop(t *testing.T) { + for _, tc := range []struct { + name string + args []string + stopped []string + expectedOpts container.StopOptions + expectedErr string + }{ + { + name: "without options", + args: []string{"container-1", "container-2"}, + stopped: []string{"container-1", "container-2"}, + }, + { + name: "with unknown container", + args: []string{"container-1", "nosuchcontainer", "container-2"}, + expectedErr: "no such container", + stopped: []string{"container-1", "container-2"}, + }, + { + name: "with -t", + args: []string{"-t", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + stopped: []string{"container-1"}, + }, + { + name: "with --time", + args: []string{"--time", "2", "container-1"}, + expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + stopped: []string{"container-1"}, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + var stopped []string + mutex := new(sync.Mutex) + + cli := test.NewFakeCli(&fakeClient{ + containerStopFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) + if containerID == "nosuchcontainer" { + return errdefs.NotFound(errors.New("Error: no such container: " + containerID)) + } + + // containerStopFunc is called in parallel for each container + // so append must be synchronized. + mutex.Lock() + stopped = append(stopped, containerID) + mutex.Unlock() + return nil + }, + Version: "1.36", + }) + cmd := NewStopCommand(cli) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(tc.args) + + err := cmd.Execute() + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } else { + assert.Check(t, is.Nil(err)) + } + sort.Strings(stopped) + assert.Check(t, is.DeepEqual(stopped, tc.stopped)) + }) + } +}