diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index 8fa5abf0e8..e0ffd2a66e 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -22,9 +22,17 @@ type fakeClient struct { containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error) waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error) + containerListFunc func(types.ContainerListOptions) ([]types.Container, error) Version string } +func (f *fakeClient) ContainerList(_ context.Context, options types.ContainerListOptions) ([]types.Container, error) { + if f.containerListFunc != nil { + return f.containerListFunc(options) + } + return []types.Container{}, nil +} + func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) { if f.inspectFunc != nil { return f.inspectFunc(containerID) diff --git a/cli/command/container/list_test.go b/cli/command/container/list_test.go new file mode 100644 index 0000000000..848d2a60a9 --- /dev/null +++ b/cli/command/container/list_test.go @@ -0,0 +1,165 @@ +package container + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" + // Import builders to get the builder function as package function + . "github.com/docker/cli/internal/test/builders" + "github.com/gotestyourself/gotestyourself/golden" +) + +func TestContainerListErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + containerListFunc func(types.ContainerListOptions) ([]types.Container, error) + expectedError string + }{ + { + flags: map[string]string{ + "format": "{{invalid}}", + }, + expectedError: `function "invalid" not defined`, + }, + { + flags: map[string]string{ + "format": "{{join}}", + }, + expectedError: `wrong number of args for join`, + }, + { + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return nil, fmt.Errorf("error listing containers") + }, + expectedError: "error listing containers", + }, + } + for _, tc := range testCases { + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + containerListFunc: tc.containerListFunc, + }), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestContainerListWithoutFormat(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1"), + *Container("c2", WithName("foo")), + *Container("c3", WithPort(80, 80, TCP), WithPort(81, 81, TCP), WithPort(82, 82, TCP)), + *Container("c4", WithPort(81, 81, UDP)), + *Container("c5", WithPort(82, 82, IP("8.8.8.8"), TCP)), + }, nil + }, + }) + cmd := newListCommand(cli) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden") +} + +func TestContainerListNoTrunc(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1"), + *Container("c2", WithName("foo/bar")), + }, nil + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("no-trunc", "true") + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden") +} + +// Test for GitHub issue docker/docker#21772 +func TestContainerListNamesMultipleTime(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1"), + *Container("c2", WithName("foo/bar")), + }, nil + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{.Names}} {{.Names}}") + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden") +} + +// Test for GitHub issue docker/docker#30291 +func TestContainerListFormatTemplateWithArg(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1", WithLabel("some.label", "value")), + *Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")), + }, nil + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden") +} + +func TestContainerListFormatSizeSetsOption(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(options types.ContainerListOptions) ([]types.Container, error) { + assert.True(t, options.Size) + return []types.Container{}, nil + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", `{{.Size}}`) + assert.NoError(t, cmd.Execute()) +} + +func TestContainerListWithConfigFormat(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1", WithLabel("some.label", "value")), + *Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")), + }, nil + }, + }) + cli.SetConfigFile(&configfile.ConfigFile{ + PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }}", + }) + cmd := newListCommand(cli) + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden") +} + +func TestContainerListWithFormat(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ types.ContainerListOptions) ([]types.Container, error) { + return []types.Container{ + *Container("c1", WithLabel("some.label", "value")), + *Container("c2", WithName("foo/bar"), WithLabel("foo", "bar")), + }, nil + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}") + assert.NoError(t, cmd.Execute()) + golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden") +} diff --git a/cli/command/container/testdata/container-list-format-name-name.golden b/cli/command/container/testdata/container-list-format-name-name.golden new file mode 100644 index 0000000000..858ec9619a --- /dev/null +++ b/cli/command/container/testdata/container-list-format-name-name.golden @@ -0,0 +1,2 @@ +c1 c1 +c2 c2 diff --git a/cli/command/container/testdata/container-list-format-with-arg.golden b/cli/command/container/testdata/container-list-format-with-arg.golden new file mode 100644 index 0000000000..782ace9416 --- /dev/null +++ b/cli/command/container/testdata/container-list-format-with-arg.golden @@ -0,0 +1,2 @@ +c1 value +c2 diff --git a/cli/command/container/testdata/container-list-with-config-format.golden b/cli/command/container/testdata/container-list-with-config-format.golden new file mode 100644 index 0000000000..6333bf57ee --- /dev/null +++ b/cli/command/container/testdata/container-list-with-config-format.golden @@ -0,0 +1,2 @@ +c1 busybox:latest some.label=value +c2 busybox:latest foo=bar diff --git a/cli/command/container/testdata/container-list-with-format.golden b/cli/command/container/testdata/container-list-with-format.golden new file mode 100644 index 0000000000..6333bf57ee --- /dev/null +++ b/cli/command/container/testdata/container-list-with-format.golden @@ -0,0 +1,2 @@ +c1 busybox:latest some.label=value +c2 busybox:latest foo=bar diff --git a/cli/command/container/testdata/container-list-without-format-no-trunc.golden b/cli/command/container/testdata/container-list-without-format-no-trunc.golden new file mode 100644 index 0000000000..5b0d652e96 --- /dev/null +++ b/cli/command/container/testdata/container-list-without-format-no-trunc.golden @@ -0,0 +1,3 @@ +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +container_id busybox:latest "top" Less than a second ago Up 1 second c1 +container_id busybox:latest "top" Less than a second ago Up 1 second c2,foo/bar diff --git a/cli/command/container/testdata/container-list-without-format.golden b/cli/command/container/testdata/container-list-without-format.golden new file mode 100644 index 0000000000..7acd4045cf --- /dev/null +++ b/cli/command/container/testdata/container-list-without-format.golden @@ -0,0 +1,6 @@ +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +container_id busybox:latest "top" Less than a second ago Up 1 second c1 +container_id busybox:latest "top" Less than a second ago Up 1 second c2 +container_id busybox:latest "top" Less than a second ago Up 1 second 80-82/tcp c3 +container_id busybox:latest "top" Less than a second ago Up 1 second 81/udp c4 +container_id busybox:latest "top" Less than a second ago Up 1 second 8.8.8.8:82->82/tcp c5 diff --git a/internal/test/builders/container.go b/internal/test/builders/container.go new file mode 100644 index 0000000000..6dce74097e --- /dev/null +++ b/internal/test/builders/container.go @@ -0,0 +1,79 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types" +) + +// Container creates a container with default values. +// Any number of container function builder can be passed to augment it. +func Container(name string, builders ...func(container *types.Container)) *types.Container { + // now := time.Now() + // onehourago := now.Add(-120 * time.Minute) + container := &types.Container{ + ID: "container_id", + Names: []string{"/" + name}, + Command: "top", + Image: "busybox:latest", + Status: "Up 1 second", + Created: time.Now().UnixNano(), + } + + for _, builder := range builders { + builder(container) + } + + return container +} + +// WithLabel adds a label to the container +func WithLabel(key, value string) func(*types.Container) { + return func(c *types.Container) { + if c.Labels == nil { + c.Labels = map[string]string{} + } + c.Labels[key] = value + } +} + +// WithName adds a name to the container +func WithName(name string) func(*types.Container) { + return func(c *types.Container) { + c.Names = append(c.Names, "/"+name) + } +} + +// WithPort adds a port mapping to the container +func WithPort(privateport, publicport uint16, builders ...func(*types.Port)) func(*types.Container) { + return func(c *types.Container) { + if c.Ports == nil { + c.Ports = []types.Port{} + } + port := &types.Port{ + PrivatePort: privateport, + PublicPort: publicport, + } + for _, builder := range builders { + builder(port) + } + c.Ports = append(c.Ports, *port) + } +} + +// IP sets the ip of the port +func IP(ip string) func(*types.Port) { + return func(p *types.Port) { + p.IP = ip + } +} + +// TCP sets the port to tcp +func TCP(p *types.Port) { + p.Type = "tcp" +} + +// UDP sets the port to udp +func UDP(p *types.Port) { + p.Type = "udp" +} diff --git a/internal/test/builders/volume.go b/internal/test/builders/volume.go index 9b84df4238..c3c879905d 100644 --- a/internal/test/builders/volume.go +++ b/internal/test/builders/volume.go @@ -5,7 +5,7 @@ import ( ) // Volume creates a volume with default values. -// Any number of volume function builder can be pass to augment it. +// Any number of volume function builder can be passed to augment it. func Volume(builders ...func(volume *types.Volume)) *types.Volume { volume := &types.Volume{ Name: "volume",