From 1333b49194c7bd6934a041b6ef491fa95a8bf2af Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 10 Jul 2017 12:43:57 +0300 Subject: [PATCH] Sort swarm stacks and nodes using natural sorting This commit changes the order stacks and nodes are displayed. For example, running "docker stack ls" is expected to display the following list: NAME SERVICES service-1 1 service-2 1 service-10 1 However, currently this is what is printed: NAME SERVICES service-1 1 service-10 1 service-2 1 To fix this, "docker stack ls" and "docker node ls" are using natural sorting to make it more human readable. Signed-off-by: Boaz Shuster --- cli/command/node/list.go | 13 ++++ cli/command/node/list_test.go | 21 +++++ .../node/testdata/node-list-sort.golden | 3 + cli/command/stack/list.go | 3 +- cli/command/stack/list_test.go | 52 ++++++++++--- .../testdata/stack-list-sort-natural.golden | 4 + vendor.conf | 1 + vendor/vbom.ml/util/LICENSE | 17 +++++ vendor/vbom.ml/util/README.md | 5 ++ vendor/vbom.ml/util/sortorder/README.md | 5 ++ vendor/vbom.ml/util/sortorder/doc.go | 5 ++ vendor/vbom.ml/util/sortorder/natsort.go | 76 +++++++++++++++++++ 12 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 cli/command/node/testdata/node-list-sort.golden create mode 100644 cli/command/stack/testdata/stack-list-sort-natural.golden create mode 100644 vendor/vbom.ml/util/LICENSE create mode 100644 vendor/vbom.ml/util/README.md create mode 100644 vendor/vbom.ml/util/sortorder/README.md create mode 100644 vendor/vbom.ml/util/sortorder/doc.go create mode 100644 vendor/vbom.ml/util/sortorder/natsort.go diff --git a/cli/command/node/list.go b/cli/command/node/list.go index 0c3d7e1abc..7dac795663 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -1,15 +1,27 @@ package node import ( + "sort" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/opts" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" "github.com/spf13/cobra" "golang.org/x/net/context" + "vbom.ml/util/sortorder" ) +type byHostname []swarm.Node + +func (n byHostname) Len() int { return len(n) } +func (n byHostname) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n byHostname) Less(i, j int) bool { + return sortorder.NaturalLess(n[i].Description.Hostname, n[j].Description.Hostname) +} + type listOptions struct { quiet bool format string @@ -68,5 +80,6 @@ func runList(dockerCli command.Cli, options listOptions) error { Output: dockerCli.Out(), Format: formatter.NewNodeFormat(format, options.quiet), } + sort.Sort(byHostname(nodes)) return formatter.NodeWrite(nodesCtx, nodes, info) } diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go index eb58d7709e..f579ebc88a 100644 --- a/cli/command/node/list_test.go +++ b/cli/command/node/list_test.go @@ -9,6 +9,8 @@ import ( "github.com/docker/cli/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil" + "github.com/docker/docker/pkg/testutil/golden" "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/cli/cli/internal/test/builders" @@ -156,3 +158,22 @@ func TestNodeListFormat(t *testing.T) { assert.Contains(t, buf.String(), `nodeHostname1: Leader`) assert.Contains(t, buf.String(), `nodeHostname2: Reachable`) } + +func TestNodeListOrder(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(Hostname("node-2-foo"), Manager(Leader())), + *Node(Hostname("node-10-foo"), Manager()), + *Node(Hostname("node-1-foo")), + }, nil + + }, + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}") + assert.NoError(t, cmd.Execute()) + actual := cli.OutBuffer().String() + expected := golden.Get(t, []byte(actual), "node-list-sort.golden") + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) +} diff --git a/cli/command/node/testdata/node-list-sort.golden b/cli/command/node/testdata/node-list-sort.golden new file mode 100644 index 0000000000..e2f2811994 --- /dev/null +++ b/cli/command/node/testdata/node-list-sort.golden @@ -0,0 +1,3 @@ +node-1-foo: +node-2-foo: Leader +node-10-foo: Reachable diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index f3781d260c..24da30512f 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" + "vbom.ml/util/sortorder" ) type listOptions struct { @@ -60,7 +61,7 @@ type byName []*formatter.Stack func (n byName) Len() int { return len(n) } func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } -func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } +func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Name, n[j].Name) } func getStacks(ctx context.Context, apiclient client.APIClient) ([]*formatter.Stack, error) { services, err := apiclient.ServiceList( diff --git a/cli/command/stack/list_test.go b/cli/command/stack/list_test.go index de1aa02365..48b141653f 100644 --- a/cli/command/stack/list_test.go +++ b/cli/command/stack/list_test.go @@ -98,10 +98,13 @@ func TestListWithoutFormat(t *testing.T) { } func TestListOrder(t *testing.T) { - buf := new(bytes.Buffer) - cmd := newListCommand(test.NewFakeCliWithOutput(&fakeClient{ - serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ + usecases := []struct { + golden string + swarmServices []swarm.Service + }{ + { + golden: "stack-list-sort.golden", + swarmServices: []swarm.Service{ *Service( ServiceLabels(map[string]string{ "com.docker.stack.namespace": "service-name-foo", @@ -112,11 +115,40 @@ func TestListOrder(t *testing.T) { "com.docker.stack.namespace": "service-name-bar", }), ), - }, nil + }, }, - }, buf)) - assert.NoError(t, cmd.Execute()) - actual := buf.String() - expected := golden.Get(t, []byte(actual), "stack-list-sort.golden") - testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) + { + golden: "stack-list-sort-natural.golden", + swarmServices: []swarm.Service{ + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-1-foo", + }), + ), + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-10-foo", + }), + ), + *Service( + ServiceLabels(map[string]string{ + "com.docker.stack.namespace": "service-name-2-foo", + }), + ), + }, + }, + } + + for _, uc := range usecases { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCliWithOutput(&fakeClient{ + serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) { + return uc.swarmServices, nil + }, + }, buf)) + assert.NoError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), uc.golden) + testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected)) + } } diff --git a/cli/command/stack/testdata/stack-list-sort-natural.golden b/cli/command/stack/testdata/stack-list-sort-natural.golden new file mode 100644 index 0000000000..09507fbf40 --- /dev/null +++ b/cli/command/stack/testdata/stack-list-sort-natural.golden @@ -0,0 +1,4 @@ +NAME SERVICES +service-name-1-foo 1 +service-name-2-foo 1 +service-name-10-foo 1 diff --git a/vendor.conf b/vendor.conf index a212dd0a3a..a71e77c0c0 100755 --- a/vendor.conf +++ b/vendor.conf @@ -52,3 +52,4 @@ gopkg.in/yaml.v2 4c78c975fe7c825c6d1466c42be594d1d6f3aba6 github.com/tonistiigi/fsutil 0ac4c11b053b9c5c7c47558f81f96c7100ce50fb github.com/stevvooe/continuity cd7a8e21e2b6f84799f5dd4b65faf49c8d3ee02d golang.org/x/sync de49d9dcd27d4f764488181bea099dfe6179bcf0 +vbom.ml/util 928aaa586d7718c70f4090ddf83f2b34c16fdc8d diff --git a/vendor/vbom.ml/util/LICENSE b/vendor/vbom.ml/util/LICENSE new file mode 100644 index 0000000000..5c695fb590 --- /dev/null +++ b/vendor/vbom.ml/util/LICENSE @@ -0,0 +1,17 @@ +The MIT License (MIT) +Copyright (c) 2015 Frits van Bommel +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/vbom.ml/util/README.md b/vendor/vbom.ml/util/README.md new file mode 100644 index 0000000000..72de507980 --- /dev/null +++ b/vendor/vbom.ml/util/README.md @@ -0,0 +1,5 @@ +## util [![GoDoc](https://godoc.org/vbom.ml/util?status.svg)](https://godoc.org/vbom.ml/util) + + import "vbom.ml/util" + +Go utility packages. diff --git a/vendor/vbom.ml/util/sortorder/README.md b/vendor/vbom.ml/util/sortorder/README.md new file mode 100644 index 0000000000..ed8da0e29b --- /dev/null +++ b/vendor/vbom.ml/util/sortorder/README.md @@ -0,0 +1,5 @@ +## sortorder [![GoDoc](https://godoc.org/vbom.ml/util/sortorder?status.svg)](https://godoc.org/vbom.ml/util/sortorder) + + import "vbom.ml/util/sortorder" + +Sort orders and comparison functions. diff --git a/vendor/vbom.ml/util/sortorder/doc.go b/vendor/vbom.ml/util/sortorder/doc.go new file mode 100644 index 0000000000..61b37a9374 --- /dev/null +++ b/vendor/vbom.ml/util/sortorder/doc.go @@ -0,0 +1,5 @@ +// Package sortorder implements sort orders and comparison functions. +// +// Currently, it only implements so-called "natural order", where integers +// embedded in strings are compared by value. +package sortorder // import "vbom.ml/util/sortorder" diff --git a/vendor/vbom.ml/util/sortorder/natsort.go b/vendor/vbom.ml/util/sortorder/natsort.go new file mode 100644 index 0000000000..1af08c1bd1 --- /dev/null +++ b/vendor/vbom.ml/util/sortorder/natsort.go @@ -0,0 +1,76 @@ +package sortorder + +// Natural implements sort.Interface to sort strings in natural order. This +// means that e.g. "abc2" < "abc12". +// +// Non-digit sequences and numbers are compared separately. The former are +// compared bytewise, while the latter are compared numerically (except that +// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitation: only ASCII digits (0-9) are considered. +type Natural []string + +func (n Natural) Len() int { return len(n) } +func (n Natural) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n Natural) Less(i, j int) bool { return NaturalLess(n[i], n[j]) } + +func isdigit(b byte) bool { return '0' <= b && b <= '9' } + +// NaturalLess compares two strings using natural ordering. This means that e.g. +// "abc2" < "abc12". +// +// Non-digit sequences and numbers are compared separately. The former are +// compared bytewise, while the latter are compared numerically (except that +// the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02") +// +// Limitation: only ASCII digits (0-9) are considered. +func NaturalLess(str1, str2 string) bool { + idx1, idx2 := 0, 0 + for idx1 < len(str1) && idx2 < len(str2) { + c1, c2 := str1[idx1], str2[idx2] + dig1, dig2 := isdigit(c1), isdigit(c2) + switch { + case dig1 != dig2: // Digits before other characters. + return dig1 // True if LHS is a digit, false if the RHS is one. + case !dig1: // && !dig2, because dig1 == dig2 + // UTF-8 compares bytewise-lexicographically, no need to decode + // codepoints. + if c1 != c2 { + return c1 < c2 + } + idx1++ + idx2++ + default: // Digits + // Eat zeros. + for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ { + } + for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ { + } + // Eat all digits. + nonZero1, nonZero2 := idx1, idx2 + for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ { + } + for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ { + } + // If lengths of numbers with non-zero prefix differ, the shorter + // one is less. + if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { + return len1 < len2 + } + // If they're not equal, string comparison is correct. + if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { + return nr1 < nr2 + } + // Otherwise, the one with less zeros is less. + // Because everything up to the number is equal, comparing the index + // after the zeros is sufficient. + if nonZero1 != nonZero2 { + return nonZero1 < nonZero2 + } + } + // They're identical so far, so continue comparing. + } + // So far they are identical. At least one is ended. If the other continues, + // it sorts last. + return len(str1) < len(str2) +}