mirror of https://github.com/docker/cli.git
Merge pull request #941 from dnephin/fix-compose-network-name
Fix compose network name
This commit is contained in:
commit
236a84759a
|
@ -39,7 +39,7 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts options.Deplo
|
||||||
networks := make(map[string]types.NetworkCreate)
|
networks := make(map[string]types.NetworkCreate)
|
||||||
for _, service := range bundle.Services {
|
for _, service := range bundle.Services {
|
||||||
for _, networkName := range service.Networks {
|
for _, networkName := range service.Networks {
|
||||||
networks[networkName] = types.NetworkCreate{
|
networks[namespace.Scope(networkName)] = types.NetworkCreate{
|
||||||
Labels: convert.AddStackLabel(namespace, nil),
|
Labels: convert.AddStackLabel(namespace, nil),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,8 +181,7 @@ func createNetworks(
|
||||||
existingNetworkMap[network.Name] = network
|
existingNetworkMap[network.Name] = network
|
||||||
}
|
}
|
||||||
|
|
||||||
for internalName, createOpts := range networks {
|
for name, createOpts := range networks {
|
||||||
name := namespace.Scope(internalName)
|
|
||||||
if _, exists := existingNetworkMap[name]; exists {
|
if _, exists := existingNetworkMap[name]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -193,7 +192,7 @@ func createNetworks(
|
||||||
|
|
||||||
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
|
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
|
||||||
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
|
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||||
return errors.Wrapf(err, "failed to create network %s", internalName)
|
return errors.Wrapf(err, "failed to create network %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -87,7 +87,12 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||||
}
|
}
|
||||||
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
|
createOpts.IPAM.Config = append(createOpts.IPAM.Config, config)
|
||||||
}
|
}
|
||||||
result[internalName] = createOpts
|
|
||||||
|
networkName := namespace.Scope(internalName)
|
||||||
|
if network.Name != "" {
|
||||||
|
networkName = network.Name
|
||||||
|
}
|
||||||
|
result[networkName] = createOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, externalNetworks
|
return result, externalNetworks
|
||||||
|
|
|
@ -35,6 +35,7 @@ func TestNetworks(t *testing.T) {
|
||||||
"outside": {},
|
"outside": {},
|
||||||
"default": {},
|
"default": {},
|
||||||
"attachablenet": {},
|
"attachablenet": {},
|
||||||
|
"named": {},
|
||||||
}
|
}
|
||||||
source := networkMap{
|
source := networkMap{
|
||||||
"normal": composetypes.NetworkConfig{
|
"normal": composetypes.NetworkConfig{
|
||||||
|
@ -62,14 +63,17 @@ func TestNetworks(t *testing.T) {
|
||||||
Driver: "overlay",
|
Driver: "overlay",
|
||||||
Attachable: true,
|
Attachable: true,
|
||||||
},
|
},
|
||||||
|
"named": composetypes.NetworkConfig{
|
||||||
|
Name: "othername",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
expected := map[string]types.NetworkCreate{
|
expected := map[string]types.NetworkCreate{
|
||||||
"default": {
|
"foo_default": {
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelNamespace: "foo",
|
LabelNamespace: "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"normal": {
|
"foo_normal": {
|
||||||
Driver: "overlay",
|
Driver: "overlay",
|
||||||
IPAM: &network.IPAM{
|
IPAM: &network.IPAM{
|
||||||
Driver: "driver",
|
Driver: "driver",
|
||||||
|
@ -87,18 +91,21 @@ func TestNetworks(t *testing.T) {
|
||||||
"something": "labeled",
|
"something": "labeled",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"attachablenet": {
|
"foo_attachablenet": {
|
||||||
Driver: "overlay",
|
Driver: "overlay",
|
||||||
Attachable: true,
|
Attachable: true,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
LabelNamespace: "foo",
|
LabelNamespace: "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"othername": {
|
||||||
|
Labels: map[string]string{LabelNamespace: "foo"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
networks, externals := Networks(namespace, source, serviceNetworks)
|
networks, externals := Networks(namespace, source, serviceNetworks)
|
||||||
assert.Check(t, is.DeepEqual(expected, networks))
|
assert.DeepEqual(t, expected, networks)
|
||||||
assert.Check(t, is.DeepEqual([]string{"special"}, externals))
|
assert.DeepEqual(t, []string{"special"}, externals)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSecrets(t *testing.T) {
|
func TestSecrets(t *testing.T) {
|
||||||
|
|
|
@ -229,7 +229,7 @@ func convertServiceNetworks(
|
||||||
aliases = network.Aliases
|
aliases = network.Aliases
|
||||||
}
|
}
|
||||||
target := namespace.Scope(networkName)
|
target := namespace.Scope(networkName)
|
||||||
if networkConfig.External.External {
|
if networkConfig.Name != "" {
|
||||||
target = networkConfig.Name
|
target = networkConfig.Name
|
||||||
}
|
}
|
||||||
netAttachConfig := swarm.NetworkAttachmentConfig{
|
netAttachConfig := swarm.NetworkAttachmentConfig{
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/types"
|
"github.com/docker/cli/cli/compose/types"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/gotestyourself/gotestyourself/assert"
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
is "github.com/gotestyourself/gotestyourself/assert/cmp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -1357,3 +1358,40 @@ networks:
|
||||||
assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name")
|
assert.ErrorContains(t, err, "network.external.name and network.name conflict; only use network.name")
|
||||||
assert.ErrorContains(t, err, "foo")
|
assert.ErrorContains(t, err, "foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadNetworkWithName(t *testing.T) {
|
||||||
|
config, err := loadYAML(`
|
||||||
|
version: '3.5'
|
||||||
|
services:
|
||||||
|
hello-world:
|
||||||
|
image: redis:alpine
|
||||||
|
networks:
|
||||||
|
- network1
|
||||||
|
- network3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
network1:
|
||||||
|
name: network2
|
||||||
|
network3:
|
||||||
|
`)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
expected := &types.Config{
|
||||||
|
Filename: "filename.yml",
|
||||||
|
Version: "3.5",
|
||||||
|
Services: types.Services{
|
||||||
|
{
|
||||||
|
Name: "hello-world",
|
||||||
|
Image: "redis:alpine",
|
||||||
|
Networks: map[string]*types.ServiceNetworkConfig{
|
||||||
|
"network1": nil,
|
||||||
|
"network3": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Networks: map[string]types.NetworkConfig{
|
||||||
|
"network1": {Name: "network2"},
|
||||||
|
"network3": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty())
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotestyourself/gotestyourself/assert"
|
||||||
|
"github.com/gotestyourself/gotestyourself/golden"
|
||||||
|
"github.com/gotestyourself/gotestyourself/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeployWithNamedResources(t *testing.T) {
|
||||||
|
stackname := "test-stack-deploy-with-names"
|
||||||
|
composefile := golden.Path("stack-with-named-resources.yml")
|
||||||
|
|
||||||
|
result := icmd.RunCommand(
|
||||||
|
"docker", "stack", "deploy", "-c", composefile, stackname)
|
||||||
|
|
||||||
|
result.Assert(t, icmd.Success)
|
||||||
|
stdout := strings.Split(result.Stdout(), "\n")
|
||||||
|
expected := strings.Split(string(golden.Get(t, "stack-deploy-with-nanes.golden")), "\n")
|
||||||
|
sort.Strings(stdout)
|
||||||
|
sort.Strings(expected)
|
||||||
|
assert.DeepEqual(t, stdout, expected)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
A file with some text
|
|
@ -0,0 +1,7 @@
|
||||||
|
Creating network test-stack-deploy-with-names_network2
|
||||||
|
Creating network named-network
|
||||||
|
Creating secret named-secret
|
||||||
|
Creating secret test-stack-deploy-with-names_secret2
|
||||||
|
Creating config test-stack-deploy-with-names_config2
|
||||||
|
Creating config named-config
|
||||||
|
Creating service test-stack-deploy-with-names_web
|
|
@ -0,0 +1,30 @@
|
||||||
|
version: '3.5'
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: registry:5000/alpine:3.6
|
||||||
|
command: top
|
||||||
|
networks: [network1, network2]
|
||||||
|
volumes: [volume1, volume2]
|
||||||
|
secrets: [secret1, secret2]
|
||||||
|
configs: [config1, config2]
|
||||||
|
|
||||||
|
networks:
|
||||||
|
network1:
|
||||||
|
name: named-network
|
||||||
|
network2:
|
||||||
|
volumes:
|
||||||
|
volume1:
|
||||||
|
name: named-volume
|
||||||
|
volume2:
|
||||||
|
secrets:
|
||||||
|
secret1:
|
||||||
|
name: named-secret
|
||||||
|
file: ./data
|
||||||
|
secret2:
|
||||||
|
file: ./data
|
||||||
|
configs:
|
||||||
|
config1:
|
||||||
|
name: named-config
|
||||||
|
file: ./data
|
||||||
|
config2:
|
||||||
|
file: ./data
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
// Package cmpopts provides common options for the cmp package.
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func equateAlways(_, _ interface{}) bool { return true }
|
||||||
|
|
||||||
|
// EquateEmpty returns a Comparer option that determines all maps and slices
|
||||||
|
// with a length of zero to be equal, regardless of whether they are nil.
|
||||||
|
//
|
||||||
|
// EquateEmpty can be used in conjunction with SortSlices and SortMaps.
|
||||||
|
func EquateEmpty() cmp.Option {
|
||||||
|
return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEmpty(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||||
|
(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) &&
|
||||||
|
(vx.Len() == 0 && vy.Len() == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquateApprox returns a Comparer option that determines float32 or float64
|
||||||
|
// values to be equal if they are within a relative fraction or absolute margin.
|
||||||
|
// This option is not used when either x or y is NaN or infinite.
|
||||||
|
//
|
||||||
|
// The fraction determines that the difference of two values must be within the
|
||||||
|
// smaller fraction of the two values, while the margin determines that the two
|
||||||
|
// values must be within some absolute margin.
|
||||||
|
// To express only a fraction or only a margin, use 0 for the other parameter.
|
||||||
|
// The fraction and margin must be non-negative.
|
||||||
|
//
|
||||||
|
// The mathematical expression used is equivalent to:
|
||||||
|
// |x-y| ≤ max(fraction*min(|x|, |y|), margin)
|
||||||
|
//
|
||||||
|
// EquateApprox can be used in conjunction with EquateNaNs.
|
||||||
|
func EquateApprox(fraction, margin float64) cmp.Option {
|
||||||
|
if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) {
|
||||||
|
panic("margin or fraction must be a non-negative number")
|
||||||
|
}
|
||||||
|
a := approximator{fraction, margin}
|
||||||
|
return cmp.Options{
|
||||||
|
cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)),
|
||||||
|
cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type approximator struct{ frac, marg float64 }
|
||||||
|
|
||||||
|
func areRealF64s(x, y float64) bool {
|
||||||
|
return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0)
|
||||||
|
}
|
||||||
|
func areRealF32s(x, y float32) bool {
|
||||||
|
return areRealF64s(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
func (a approximator) compareF64(x, y float64) bool {
|
||||||
|
relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y))
|
||||||
|
return math.Abs(x-y) <= math.Max(a.marg, relMarg)
|
||||||
|
}
|
||||||
|
func (a approximator) compareF32(x, y float32) bool {
|
||||||
|
return a.compareF64(float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EquateNaNs returns a Comparer option that determines float32 and float64
|
||||||
|
// NaN values to be equal.
|
||||||
|
//
|
||||||
|
// EquateNaNs can be used in conjunction with EquateApprox.
|
||||||
|
func EquateNaNs() cmp.Option {
|
||||||
|
return cmp.Options{
|
||||||
|
cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)),
|
||||||
|
cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func areNaNsF64s(x, y float64) bool {
|
||||||
|
return math.IsNaN(x) && math.IsNaN(y)
|
||||||
|
}
|
||||||
|
func areNaNsF32s(x, y float32) bool {
|
||||||
|
return areNaNsF64s(float64(x), float64(y))
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IgnoreFields returns an Option that ignores exported fields of the
|
||||||
|
// given names on a single struct type.
|
||||||
|
// The struct type is specified by passing in a value of that type.
|
||||||
|
//
|
||||||
|
// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a
|
||||||
|
// specific sub-field that is embedded or nested within the parent struct.
|
||||||
|
//
|
||||||
|
// This does not handle unexported fields; use IgnoreUnexported instead.
|
||||||
|
func IgnoreFields(typ interface{}, names ...string) cmp.Option {
|
||||||
|
sf := newStructFilter(typ, names...)
|
||||||
|
return cmp.FilterPath(sf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreTypes returns an Option that ignores all values assignable to
|
||||||
|
// certain types, which are specified by passing in a value of each type.
|
||||||
|
func IgnoreTypes(typs ...interface{}) cmp.Option {
|
||||||
|
tf := newTypeFilter(typs...)
|
||||||
|
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type typeFilter []reflect.Type
|
||||||
|
|
||||||
|
func newTypeFilter(typs ...interface{}) (tf typeFilter) {
|
||||||
|
for _, typ := range typs {
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil {
|
||||||
|
// This occurs if someone tries to pass in sync.Locker(nil)
|
||||||
|
panic("cannot determine type; consider using IgnoreInterfaces")
|
||||||
|
}
|
||||||
|
tf = append(tf, t)
|
||||||
|
}
|
||||||
|
return tf
|
||||||
|
}
|
||||||
|
func (tf typeFilter) filter(p cmp.Path) bool {
|
||||||
|
if len(p) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := p.Last().Type()
|
||||||
|
for _, ti := range tf {
|
||||||
|
if t.AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreInterfaces returns an Option that ignores all values or references of
|
||||||
|
// values assignable to certain interface types. These interfaces are specified
|
||||||
|
// by passing in an anonymous struct with the interface types embedded in it.
|
||||||
|
// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}.
|
||||||
|
func IgnoreInterfaces(ifaces interface{}) cmp.Option {
|
||||||
|
tf := newIfaceFilter(ifaces)
|
||||||
|
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type ifaceFilter []reflect.Type
|
||||||
|
|
||||||
|
func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) {
|
||||||
|
t := reflect.TypeOf(ifaces)
|
||||||
|
if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct {
|
||||||
|
panic("input must be an anonymous struct")
|
||||||
|
}
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
fi := t.Field(i)
|
||||||
|
switch {
|
||||||
|
case !fi.Anonymous:
|
||||||
|
panic("struct cannot have named fields")
|
||||||
|
case fi.Type.Kind() != reflect.Interface:
|
||||||
|
panic("embedded field must be an interface type")
|
||||||
|
case fi.Type.NumMethod() == 0:
|
||||||
|
// This matches everything; why would you ever want this?
|
||||||
|
panic("cannot ignore empty interface")
|
||||||
|
default:
|
||||||
|
tf = append(tf, fi.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tf
|
||||||
|
}
|
||||||
|
func (tf ifaceFilter) filter(p cmp.Path) bool {
|
||||||
|
if len(p) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t := p.Last().Type()
|
||||||
|
for _, ti := range tf {
|
||||||
|
if t.AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreUnexported returns an Option that only ignores the immediate unexported
|
||||||
|
// fields of a struct, including anonymous fields of unexported types.
|
||||||
|
// In particular, unexported fields within the struct's exported fields
|
||||||
|
// of struct types, including anonymous fields, will not be ignored unless the
|
||||||
|
// type of the field itself is also passed to IgnoreUnexported.
|
||||||
|
func IgnoreUnexported(typs ...interface{}) cmp.Option {
|
||||||
|
ux := newUnexportedFilter(typs...)
|
||||||
|
return cmp.FilterPath(ux.filter, cmp.Ignore())
|
||||||
|
}
|
||||||
|
|
||||||
|
type unexportedFilter struct{ m map[reflect.Type]bool }
|
||||||
|
|
||||||
|
func newUnexportedFilter(typs ...interface{}) unexportedFilter {
|
||||||
|
ux := unexportedFilter{m: make(map[reflect.Type]bool)}
|
||||||
|
for _, typ := range typs {
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("invalid struct type: %T", typ))
|
||||||
|
}
|
||||||
|
ux.m[t] = true
|
||||||
|
}
|
||||||
|
return ux
|
||||||
|
}
|
||||||
|
func (xf unexportedFilter) filter(p cmp.Path) bool {
|
||||||
|
sf, ok := p.Index(-1).(cmp.StructField)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return xf.m[p.Index(-2).Type()] && !isExported(sf.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExported reports whether the identifier is exported.
|
||||||
|
func isExported(id string) bool {
|
||||||
|
r, _ := utf8.DecodeRuneInString(id)
|
||||||
|
return unicode.IsUpper(r)
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/internal/function"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortSlices returns a Transformer option that sorts all []V.
|
||||||
|
// The less function must be of the form "func(T, T) bool" which is used to
|
||||||
|
// sort any slice with element type V that is assignable to T.
|
||||||
|
//
|
||||||
|
// The less function must be:
|
||||||
|
// • Deterministic: less(x, y) == less(x, y)
|
||||||
|
// • Irreflexive: !less(x, x)
|
||||||
|
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||||
|
//
|
||||||
|
// The less function does not have to be "total". That is, if !less(x, y) and
|
||||||
|
// !less(y, x) for two elements x and y, their relative order is maintained.
|
||||||
|
//
|
||||||
|
// SortSlices can be used in conjunction with EquateEmpty.
|
||||||
|
func SortSlices(less interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(less)
|
||||||
|
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid less function: %T", less))
|
||||||
|
}
|
||||||
|
ss := sliceSorter{vf.Type().In(0), vf}
|
||||||
|
return cmp.FilterValues(ss.filter, cmp.Transformer("Sort", ss.sort))
|
||||||
|
}
|
||||||
|
|
||||||
|
type sliceSorter struct {
|
||||||
|
in reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss sliceSorter) filter(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
if !(x != nil && y != nil && vx.Type() == vy.Type()) ||
|
||||||
|
!(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) ||
|
||||||
|
(vx.Len() <= 1 && vy.Len() <= 1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Check whether the slices are already sorted to avoid an infinite
|
||||||
|
// recursion cycle applying the same transform to itself.
|
||||||
|
ok1 := sliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) })
|
||||||
|
ok2 := sliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) })
|
||||||
|
return !ok1 || !ok2
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) sort(x interface{}) interface{} {
|
||||||
|
src := reflect.ValueOf(x)
|
||||||
|
dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len())
|
||||||
|
for i := 0; i < src.Len(); i++ {
|
||||||
|
dst.Index(i).Set(src.Index(i))
|
||||||
|
}
|
||||||
|
sortSliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) })
|
||||||
|
ss.checkSort(dst)
|
||||||
|
return dst.Interface()
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) checkSort(v reflect.Value) {
|
||||||
|
start := -1 // Start of a sequence of equal elements.
|
||||||
|
for i := 1; i < v.Len(); i++ {
|
||||||
|
if ss.less(v, i-1, i) {
|
||||||
|
// Check that first and last elements in v[start:i] are equal.
|
||||||
|
if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) {
|
||||||
|
panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i)))
|
||||||
|
}
|
||||||
|
start = -1
|
||||||
|
} else if start == -1 {
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
|
||||||
|
vx, vy := v.Index(i), v.Index(j)
|
||||||
|
return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortMaps returns a Transformer option that flattens map[K]V types to be a
|
||||||
|
// sorted []struct{K, V}. The less function must be of the form
|
||||||
|
// "func(T, T) bool" which is used to sort any map with key K that is
|
||||||
|
// assignable to T.
|
||||||
|
//
|
||||||
|
// Flattening the map into a slice has the property that cmp.Equal is able to
|
||||||
|
// use Comparers on K or the K.Equal method if it exists.
|
||||||
|
//
|
||||||
|
// The less function must be:
|
||||||
|
// • Deterministic: less(x, y) == less(x, y)
|
||||||
|
// • Irreflexive: !less(x, x)
|
||||||
|
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||||
|
// • Total: if x != y, then either less(x, y) or less(y, x)
|
||||||
|
//
|
||||||
|
// SortMaps can be used in conjunction with EquateEmpty.
|
||||||
|
func SortMaps(less interface{}) cmp.Option {
|
||||||
|
vf := reflect.ValueOf(less)
|
||||||
|
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid less function: %T", less))
|
||||||
|
}
|
||||||
|
ms := mapSorter{vf.Type().In(0), vf}
|
||||||
|
return cmp.FilterValues(ms.filter, cmp.Transformer("Sort", ms.sort))
|
||||||
|
}
|
||||||
|
|
||||||
|
type mapSorter struct {
|
||||||
|
in reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms mapSorter) filter(x, y interface{}) bool {
|
||||||
|
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||||
|
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||||
|
(vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) &&
|
||||||
|
(vx.Len() != 0 || vy.Len() != 0)
|
||||||
|
}
|
||||||
|
func (ms mapSorter) sort(x interface{}) interface{} {
|
||||||
|
src := reflect.ValueOf(x)
|
||||||
|
outType := mapEntryType(src.Type())
|
||||||
|
dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len())
|
||||||
|
for i, k := range src.MapKeys() {
|
||||||
|
v := reflect.New(outType).Elem()
|
||||||
|
v.Field(0).Set(k)
|
||||||
|
v.Field(1).Set(src.MapIndex(k))
|
||||||
|
dst.Index(i).Set(v)
|
||||||
|
}
|
||||||
|
sortSlice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) })
|
||||||
|
ms.checkSort(dst)
|
||||||
|
return dst.Interface()
|
||||||
|
}
|
||||||
|
func (ms mapSorter) checkSort(v reflect.Value) {
|
||||||
|
for i := 1; i < v.Len(); i++ {
|
||||||
|
if !ms.less(v, i-1, i) {
|
||||||
|
panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (ms mapSorter) less(v reflect.Value, i, j int) bool {
|
||||||
|
vx, vy := v.Index(i).Field(0), v.Index(j).Field(0)
|
||||||
|
if !hasReflectStructOf {
|
||||||
|
vx, vy = vx.Elem(), vy.Elem()
|
||||||
|
}
|
||||||
|
return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
// +build !go1.8
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasReflectStructOf = false
|
||||||
|
|
||||||
|
func mapEntryType(reflect.Type) reflect.Type {
|
||||||
|
return reflect.TypeOf(struct{ K, V interface{} }{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool {
|
||||||
|
return sort.IsSorted(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||||
|
}
|
||||||
|
func sortSlice(slice interface{}, less func(i, j int) bool) {
|
||||||
|
sort.Sort(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||||
|
}
|
||||||
|
func sortSliceStable(slice interface{}, less func(i, j int) bool) {
|
||||||
|
sort.Stable(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||||
|
}
|
||||||
|
|
||||||
|
type reflectSliceSorter struct {
|
||||||
|
slice reflect.Value
|
||||||
|
less func(i, j int) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss reflectSliceSorter) Len() int {
|
||||||
|
return ss.slice.Len()
|
||||||
|
}
|
||||||
|
func (ss reflectSliceSorter) Less(i, j int) bool {
|
||||||
|
return ss.less(i, j)
|
||||||
|
}
|
||||||
|
func (ss reflectSliceSorter) Swap(i, j int) {
|
||||||
|
vi := ss.slice.Index(i).Interface()
|
||||||
|
vj := ss.slice.Index(j).Interface()
|
||||||
|
ss.slice.Index(i).Set(reflect.ValueOf(vj))
|
||||||
|
ss.slice.Index(j).Set(reflect.ValueOf(vi))
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasReflectStructOf = true
|
||||||
|
|
||||||
|
func mapEntryType(t reflect.Type) reflect.Type {
|
||||||
|
return reflect.StructOf([]reflect.StructField{
|
||||||
|
{Name: "K", Type: t.Key()},
|
||||||
|
{Name: "V", Type: t.Elem()},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool {
|
||||||
|
return sort.SliceIsSorted(slice, less)
|
||||||
|
}
|
||||||
|
func sortSlice(slice interface{}, less func(i, j int) bool) {
|
||||||
|
sort.Slice(slice, less)
|
||||||
|
}
|
||||||
|
func sortSliceStable(slice interface{}, less func(i, j int) bool) {
|
||||||
|
sort.SliceStable(slice, less)
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Copyright 2017, The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE.md file.
|
||||||
|
|
||||||
|
package cmpopts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// filterField returns a new Option where opt is only evaluated on paths that
|
||||||
|
// include a specific exported field on a single struct type.
|
||||||
|
// The struct type is specified by passing in a value of that type.
|
||||||
|
//
|
||||||
|
// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a
|
||||||
|
// specific sub-field that is embedded or nested within the parent struct.
|
||||||
|
func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option {
|
||||||
|
// TODO: This is currently unexported over concerns of how helper filters
|
||||||
|
// can be composed together easily.
|
||||||
|
// TODO: Add tests for FilterField.
|
||||||
|
|
||||||
|
sf := newStructFilter(typ, name)
|
||||||
|
return cmp.FilterPath(sf.filter, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
type structFilter struct {
|
||||||
|
t reflect.Type // The root struct type to match on
|
||||||
|
ft fieldTree // Tree of fields to match on
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStructFilter(typ interface{}, names ...string) structFilter {
|
||||||
|
// TODO: Perhaps allow * as a special identifier to allow ignoring any
|
||||||
|
// number of path steps until the next field match?
|
||||||
|
// This could be useful when a concrete struct gets transformed into
|
||||||
|
// an anonymous struct where it is not possible to specify that by type,
|
||||||
|
// but the transformer happens to provide guarantees about the names of
|
||||||
|
// the transformed fields.
|
||||||
|
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("%T must be a struct", typ))
|
||||||
|
}
|
||||||
|
var ft fieldTree
|
||||||
|
for _, name := range names {
|
||||||
|
cname, err := canonicalName(t, name)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err))
|
||||||
|
}
|
||||||
|
ft.insert(cname)
|
||||||
|
}
|
||||||
|
return structFilter{t, ft}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sf structFilter) filter(p cmp.Path) bool {
|
||||||
|
for i, ps := range p {
|
||||||
|
if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldTree represents a set of dot-separated identifiers.
|
||||||
|
//
|
||||||
|
// For example, inserting the following selectors:
|
||||||
|
// Foo
|
||||||
|
// Foo.Bar.Baz
|
||||||
|
// Foo.Buzz
|
||||||
|
// Nuka.Cola.Quantum
|
||||||
|
//
|
||||||
|
// Results in a tree of the form:
|
||||||
|
// {sub: {
|
||||||
|
// "Foo": {ok: true, sub: {
|
||||||
|
// "Bar": {sub: {
|
||||||
|
// "Baz": {ok: true},
|
||||||
|
// }},
|
||||||
|
// "Buzz": {ok: true},
|
||||||
|
// }},
|
||||||
|
// "Nuka": {sub: {
|
||||||
|
// "Cola": {sub: {
|
||||||
|
// "Quantum": {ok: true},
|
||||||
|
// }},
|
||||||
|
// }},
|
||||||
|
// }}
|
||||||
|
type fieldTree struct {
|
||||||
|
ok bool // Whether this is a specified node
|
||||||
|
sub map[string]fieldTree // The sub-tree of fields under this node
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert inserts a sequence of field accesses into the tree.
|
||||||
|
func (ft *fieldTree) insert(cname []string) {
|
||||||
|
if ft.sub == nil {
|
||||||
|
ft.sub = make(map[string]fieldTree)
|
||||||
|
}
|
||||||
|
if len(cname) == 0 {
|
||||||
|
ft.ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sub := ft.sub[cname[0]]
|
||||||
|
sub.insert(cname[1:])
|
||||||
|
ft.sub[cname[0]] = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchPrefix reports whether any selector in the fieldTree matches
|
||||||
|
// the start of path p.
|
||||||
|
func (ft fieldTree) matchPrefix(p cmp.Path) bool {
|
||||||
|
for _, ps := range p {
|
||||||
|
switch ps := ps.(type) {
|
||||||
|
case cmp.StructField:
|
||||||
|
ft = ft.sub[ps.Name()]
|
||||||
|
if ft.ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(ft.sub) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case cmp.Indirect:
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// canonicalName returns a list of identifiers where any struct field access
|
||||||
|
// through an embedded field is expanded to include the names of the embedded
|
||||||
|
// types themselves.
|
||||||
|
//
|
||||||
|
// For example, suppose field "Foo" is not directly in the parent struct,
|
||||||
|
// but actually from an embedded struct of type "Bar". Then, the canonical name
|
||||||
|
// of "Foo" is actually "Bar.Foo".
|
||||||
|
//
|
||||||
|
// Suppose field "Foo" is not directly in the parent struct, but actually
|
||||||
|
// a field in two different embedded structs of types "Bar" and "Baz".
|
||||||
|
// Then the selector "Foo" causes a panic since it is ambiguous which one it
|
||||||
|
// refers to. The user must specify either "Bar.Foo" or "Baz.Foo".
|
||||||
|
func canonicalName(t reflect.Type, sel string) ([]string, error) {
|
||||||
|
var name string
|
||||||
|
sel = strings.TrimPrefix(sel, ".")
|
||||||
|
if sel == "" {
|
||||||
|
return nil, fmt.Errorf("name must not be empty")
|
||||||
|
}
|
||||||
|
if i := strings.IndexByte(sel, '.'); i < 0 {
|
||||||
|
name, sel = sel, ""
|
||||||
|
} else {
|
||||||
|
name, sel = sel[:i], sel[i:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type must be a struct or pointer to struct.
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("%v must be a struct", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the canonical name for this current field name.
|
||||||
|
// If the field exists in an embedded struct, then it will be expanded.
|
||||||
|
if !isExported(name) {
|
||||||
|
// Disallow unexported fields:
|
||||||
|
// * To discourage people from actually touching unexported fields
|
||||||
|
// * FieldByName is buggy (https://golang.org/issue/4876)
|
||||||
|
return []string{name}, fmt.Errorf("name must be exported")
|
||||||
|
}
|
||||||
|
sf, ok := t.FieldByName(name)
|
||||||
|
if !ok {
|
||||||
|
return []string{name}, fmt.Errorf("does not exist")
|
||||||
|
}
|
||||||
|
var ss []string
|
||||||
|
for i := range sf.Index {
|
||||||
|
ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name)
|
||||||
|
}
|
||||||
|
if sel == "" {
|
||||||
|
return ss, nil
|
||||||
|
}
|
||||||
|
ssPost, err := canonicalName(sf.Type, sel)
|
||||||
|
return append(ss, ssPost...), err
|
||||||
|
}
|
Loading…
Reference in New Issue