forked from TrueCloudLab/restic
996 lines
33 KiB
Go
996 lines
33 KiB
Go
// 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
)
|
|
|
|
type (
|
|
MyInt int
|
|
MyFloat float32
|
|
MyTime struct{ time.Time }
|
|
MyStruct struct {
|
|
A, B []int
|
|
C, D map[time.Time]string
|
|
}
|
|
|
|
Foo1 struct{ Alpha, Bravo, Charlie int }
|
|
Foo2 struct{ *Foo1 }
|
|
Foo3 struct{ *Foo2 }
|
|
Bar1 struct{ Foo3 }
|
|
Bar2 struct {
|
|
Bar1
|
|
*Foo3
|
|
Bravo float32
|
|
}
|
|
Bar3 struct {
|
|
Bar1
|
|
Bravo *Bar2
|
|
Delta struct{ Echo Foo1 }
|
|
*Foo3
|
|
Alpha string
|
|
}
|
|
|
|
privateStruct struct{ Public, private int }
|
|
PublicStruct struct{ Public, private int }
|
|
ParentStruct struct {
|
|
*privateStruct
|
|
*PublicStruct
|
|
Public int
|
|
private int
|
|
}
|
|
|
|
Everything struct {
|
|
MyInt
|
|
MyFloat
|
|
MyTime
|
|
MyStruct
|
|
Bar3
|
|
ParentStruct
|
|
}
|
|
|
|
EmptyInterface interface{}
|
|
)
|
|
|
|
func TestOptions(t *testing.T) {
|
|
createBar3X := func() *Bar3 {
|
|
return &Bar3{
|
|
Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}},
|
|
Bravo: &Bar2{
|
|
Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}},
|
|
Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}},
|
|
Bravo: 4,
|
|
},
|
|
Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}},
|
|
Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}},
|
|
Alpha: "alpha",
|
|
}
|
|
}
|
|
createBar3Y := func() *Bar3 {
|
|
return &Bar3{
|
|
Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}},
|
|
Bravo: &Bar2{
|
|
Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}},
|
|
Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}},
|
|
Bravo: 5,
|
|
},
|
|
Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}},
|
|
Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}},
|
|
Alpha: "ALPHA",
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
label string // Test name
|
|
x, y interface{} // Input values to compare
|
|
opts []cmp.Option // Input options
|
|
wantEqual bool // Whether the inputs are equal
|
|
wantPanic bool // Whether Equal should panic
|
|
reason string // The reason for the expected outcome
|
|
}{{
|
|
label: "EquateEmpty",
|
|
x: []int{},
|
|
y: []int(nil),
|
|
wantEqual: false,
|
|
reason: "not equal because empty non-nil and nil slice differ",
|
|
}, {
|
|
label: "EquateEmpty",
|
|
x: []int{},
|
|
y: []int(nil),
|
|
opts: []cmp.Option{EquateEmpty()},
|
|
wantEqual: true,
|
|
reason: "equal because EquateEmpty equates empty slices",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
|
y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
|
wantEqual: false,
|
|
reason: "not equal because element order differs",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
|
y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
|
opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })},
|
|
wantEqual: true,
|
|
reason: "equal because SortSlices sorts the slices",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
|
y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
|
opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })},
|
|
wantEqual: false,
|
|
reason: "not equal because MyInt is not the same type as int",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []float64{0, 1, 1, 2, 2, 2},
|
|
y: []float64{2, 0, 2, 1, 2, 1},
|
|
opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })},
|
|
wantEqual: true,
|
|
reason: "equal even when sorted with duplicate elements",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4},
|
|
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2},
|
|
opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })},
|
|
wantPanic: true,
|
|
reason: "panics because SortSlices used with non-transitive less function",
|
|
}, {
|
|
label: "SortSlices",
|
|
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4},
|
|
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2},
|
|
opts: []cmp.Option{SortSlices(func(x, y float64) bool {
|
|
return (!math.IsNaN(x) && math.IsNaN(y)) || x < y
|
|
})},
|
|
wantEqual: false,
|
|
reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN",
|
|
}, {
|
|
label: "SortSlices+EquateNaNs",
|
|
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4},
|
|
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2},
|
|
opts: []cmp.Option{
|
|
EquateNaNs(),
|
|
SortSlices(func(x, y float64) bool {
|
|
return (!math.IsNaN(x) && math.IsNaN(y)) || x < y
|
|
}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
|
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday",
|
|
},
|
|
y: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
|
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday",
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal because timezones differ",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
|
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday",
|
|
},
|
|
y: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
|
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday",
|
|
},
|
|
opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })},
|
|
wantEqual: true,
|
|
reason: "equal because SortMaps flattens to a slice where Time.Equal can be used",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[MyTime]string{
|
|
{time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday",
|
|
{time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday",
|
|
{time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday",
|
|
},
|
|
y: map[MyTime]string{
|
|
{time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday",
|
|
{time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday",
|
|
{time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday",
|
|
},
|
|
opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })},
|
|
wantEqual: false,
|
|
reason: "not equal because MyTime is not assignable to time.Time",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
|
// => {0, 1, 2, 3, -1, -2, -3},
|
|
y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""},
|
|
// => {0, 1, 2, 3, 100, 200, 300},
|
|
opts: []cmp.Option{SortMaps(func(a, b int) bool {
|
|
if -10 < a && a <= 0 {
|
|
a *= -100
|
|
}
|
|
if -10 < b && b <= 0 {
|
|
b *= -100
|
|
}
|
|
return a < b
|
|
})},
|
|
wantEqual: false,
|
|
reason: "not equal because values differ even though SortMap provides valid ordering",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
|
// => {0, 1, 2, 3, -1, -2, -3},
|
|
y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""},
|
|
// => {0, 1, 2, 3, 100, 200, 300},
|
|
opts: []cmp.Option{
|
|
SortMaps(func(x, y int) bool {
|
|
if -10 < x && x <= 0 {
|
|
x *= -100
|
|
}
|
|
if -10 < y && y <= 0 {
|
|
y *= -100
|
|
}
|
|
return x < y
|
|
}),
|
|
cmp.Comparer(func(x, y int) bool {
|
|
if -10 < x && x <= 0 {
|
|
x *= -100
|
|
}
|
|
if -10 < y && y <= 0 {
|
|
y *= -100
|
|
}
|
|
return x == y
|
|
}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because Comparer used to equate differences",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
|
y: map[int]string{},
|
|
opts: []cmp.Option{SortMaps(func(x, y int) bool {
|
|
return x < y && x >= 0 && y >= 0
|
|
})},
|
|
wantPanic: true,
|
|
reason: "panics because SortMaps used with non-transitive less function",
|
|
}, {
|
|
label: "SortMaps",
|
|
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
|
y: map[int]string{},
|
|
opts: []cmp.Option{SortMaps(func(x, y int) bool {
|
|
return math.Abs(float64(x)) < math.Abs(float64(y))
|
|
})},
|
|
wantPanic: true,
|
|
reason: "panics because SortMaps used with partial less function",
|
|
}, {
|
|
label: "EquateEmpty+SortSlices+SortMaps",
|
|
x: MyStruct{
|
|
A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
|
C: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
|
},
|
|
D: map[time.Time]string{},
|
|
},
|
|
y: MyStruct{
|
|
A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
|
B: []int{},
|
|
C: map[time.Time]string{
|
|
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
|
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
|
},
|
|
},
|
|
opts: []cmp.Option{
|
|
EquateEmpty(),
|
|
SortSlices(func(x, y int) bool { return x < y }),
|
|
SortMaps(func(x, y time.Time) bool { return x.Before(y) }),
|
|
},
|
|
wantEqual: true,
|
|
reason: "no panics because EquateEmpty should compose with the sort options",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
wantEqual: false,
|
|
reason: "not equal because floats do not exactly matches",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
opts: []cmp.Option{EquateApprox(0, 0)},
|
|
wantEqual: false,
|
|
reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
opts: []cmp.Option{EquateApprox(0.003, 0.009)},
|
|
wantEqual: false,
|
|
reason: "not equal because EquateApprox is too strict",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
opts: []cmp.Option{EquateApprox(0, 0.011)},
|
|
wantEqual: true,
|
|
reason: "equal because margin is loose enough to match",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
|
wantEqual: true,
|
|
reason: "equal because fraction is loose enough to match",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: 3.09,
|
|
y: 3.10,
|
|
opts: []cmp.Option{EquateApprox(0.004, 0.011)},
|
|
wantEqual: true,
|
|
reason: "equal because both the margin and fraction are loose enough to match",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: float32(3.09),
|
|
y: float64(3.10),
|
|
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
|
wantEqual: false,
|
|
reason: "not equal because the types differ",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: float32(3.09),
|
|
y: float32(3.10),
|
|
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
|
wantEqual: true,
|
|
reason: "equal because EquateApprox also applies on float32s",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: []float64{math.Inf(+1), math.Inf(-1)},
|
|
y: []float64{math.Inf(+1), math.Inf(-1)},
|
|
opts: []cmp.Option{EquateApprox(0, 1)},
|
|
wantEqual: true,
|
|
reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: []float64{math.Inf(+1), -1e100},
|
|
y: []float64{+1e100, math.Inf(-1)},
|
|
opts: []cmp.Option{EquateApprox(0, 1)},
|
|
wantEqual: false,
|
|
reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: float64(+1e100),
|
|
y: float64(-1e100),
|
|
opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)},
|
|
wantEqual: true,
|
|
reason: "equal because infinite fraction matches everything",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: float64(+1e100),
|
|
y: float64(-1e100),
|
|
opts: []cmp.Option{EquateApprox(0, math.Inf(+1))},
|
|
wantEqual: true,
|
|
reason: "equal because infinite margin matches everything",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: math.Pi,
|
|
y: math.Pi,
|
|
opts: []cmp.Option{EquateApprox(0, 0)},
|
|
wantEqual: true,
|
|
reason: "equal because EquateApprox(0, 0) is equivalent to ==",
|
|
}, {
|
|
label: "EquateApprox",
|
|
x: math.Pi,
|
|
y: math.Nextafter(math.Pi, math.Inf(+1)),
|
|
opts: []cmp.Option{EquateApprox(0, 0)},
|
|
wantEqual: false,
|
|
reason: "not equal because EquateApprox(0, 0) is equivalent to ==",
|
|
}, {
|
|
label: "EquateNaNs",
|
|
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
|
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
|
wantEqual: false,
|
|
reason: "not equal because NaN != NaN",
|
|
}, {
|
|
label: "EquateNaNs",
|
|
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
|
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
|
opts: []cmp.Option{EquateNaNs()},
|
|
wantEqual: true,
|
|
reason: "equal because EquateNaNs allows NaN == NaN",
|
|
}, {
|
|
label: "EquateNaNs",
|
|
x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0},
|
|
y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0},
|
|
opts: []cmp.Option{EquateNaNs()},
|
|
wantEqual: true,
|
|
reason: "equal because EquateNaNs operates on float32",
|
|
}, {
|
|
label: "EquateApprox+EquateNaNs",
|
|
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001},
|
|
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002},
|
|
opts: []cmp.Option{
|
|
EquateNaNs(),
|
|
EquateApprox(0.01, 0),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because EquateNaNs and EquateApprox compose together",
|
|
}, {
|
|
label: "EquateApprox+EquateNaNs",
|
|
x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001},
|
|
y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002},
|
|
opts: []cmp.Option{
|
|
EquateNaNs(),
|
|
EquateApprox(0.01, 0),
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type",
|
|
}, {
|
|
label: "EquateApprox+EquateNaNs+Transform",
|
|
x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001},
|
|
y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002},
|
|
opts: []cmp.Option{
|
|
cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }),
|
|
EquateNaNs(),
|
|
EquateApprox(0.01, 0),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because named type is transformed to float64",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
wantEqual: false,
|
|
reason: "not equal because values do not match in deeply embedded field",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreField ignores deeply embedded field: Alpha",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
|
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
|
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: createBar3X(),
|
|
y: createBar3Y(),
|
|
wantEqual: false,
|
|
reason: "not equal because many deeply nested or embedded fields differ",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: createBar3X(),
|
|
y: createBar3Y(),
|
|
opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreFields ignores fields at the highest levels",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: createBar3X(),
|
|
y: createBar3Y(),
|
|
opts: []cmp.Option{
|
|
IgnoreFields(Bar3{},
|
|
"Bar1.Foo3.Bravo",
|
|
"Bravo.Bar1.Foo3.Foo2.Foo1.Charlie",
|
|
"Bravo.Foo3.Foo2.Foo1.Bravo",
|
|
"Bravo.Bravo",
|
|
"Delta.Echo.Charlie",
|
|
"Foo3.Foo2.Foo1.Alpha",
|
|
"Alpha",
|
|
),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreFields ignores fields using fully-qualified field",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: createBar3X(),
|
|
y: createBar3Y(),
|
|
opts: []cmp.Option{
|
|
IgnoreFields(Bar3{},
|
|
"Bar1.Foo3.Bravo",
|
|
"Bravo.Foo3.Foo2.Foo1.Bravo",
|
|
"Bravo.Bravo",
|
|
"Delta.Echo.Charlie",
|
|
"Foo3.Foo2.Foo1.Alpha",
|
|
"Alpha",
|
|
),
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
x: createBar3X(),
|
|
y: createBar3Y(),
|
|
opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")},
|
|
wantEqual: false,
|
|
reason: "not equal because highest-level field is not ignored: Foo3",
|
|
}, {
|
|
label: "IgnoreTypes",
|
|
x: []interface{}{5, "same"},
|
|
y: []interface{}{6, "same"},
|
|
wantEqual: false,
|
|
reason: "not equal because 5 != 6",
|
|
}, {
|
|
label: "IgnoreTypes",
|
|
x: []interface{}{5, "same"},
|
|
y: []interface{}{6, "same"},
|
|
opts: []cmp.Option{IgnoreTypes(0)},
|
|
wantEqual: true,
|
|
reason: "equal because ints are ignored",
|
|
}, {
|
|
label: "IgnoreTypes+IgnoreInterfaces",
|
|
x: []interface{}{5, "same", new(bytes.Buffer)},
|
|
y: []interface{}{6, "same", new(bytes.Buffer)},
|
|
opts: []cmp.Option{IgnoreTypes(0)},
|
|
wantPanic: true,
|
|
reason: "panics because bytes.Buffer has unexported fields",
|
|
}, {
|
|
label: "IgnoreTypes+IgnoreInterfaces",
|
|
x: []interface{}{5, "same", new(bytes.Buffer)},
|
|
y: []interface{}{6, "diff", new(bytes.Buffer)},
|
|
opts: []cmp.Option{
|
|
IgnoreTypes(0, ""),
|
|
IgnoreInterfaces(struct{ io.Reader }{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because bytes.Buffer is ignored by match on interface type",
|
|
}, {
|
|
label: "IgnoreTypes+IgnoreInterfaces",
|
|
x: []interface{}{5, "same", new(bytes.Buffer)},
|
|
y: []interface{}{6, "same", new(bytes.Buffer)},
|
|
opts: []cmp.Option{
|
|
IgnoreTypes(0, ""),
|
|
IgnoreInterfaces(struct {
|
|
io.Reader
|
|
io.Writer
|
|
fmt.Stringer
|
|
}{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because bytes.Buffer is ignored by match on multiple interface types",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
x: struct{ mu sync.Mutex }{},
|
|
y: struct{ mu sync.Mutex }{},
|
|
wantPanic: true,
|
|
reason: "panics because sync.Mutex has unexported fields",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
x: struct{ mu sync.Mutex }{},
|
|
y: struct{ mu sync.Mutex }{},
|
|
opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
x: struct{ mu *sync.Mutex }{},
|
|
y: struct{ mu *sync.Mutex }{},
|
|
opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreInterfaces applies on pointers",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2},
|
|
y: ParentStruct{Public: 1, private: -2},
|
|
opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})},
|
|
wantEqual: false,
|
|
reason: "not equal because ParentStruct.private differs with AllowUnexported",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2},
|
|
y: ParentStruct{Public: 1, private: -2},
|
|
opts: []cmp.Option{IgnoreUnexported(ParentStruct{})},
|
|
wantEqual: true,
|
|
reason: "equal because IgnoreUnexported ignored ParentStruct.private",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(PublicStruct{}),
|
|
IgnoreUnexported(ParentStruct{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because ParentStruct.private is ignored",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(PublicStruct{}),
|
|
IgnoreUnexported(ParentStruct{}),
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}},
|
|
opts: []cmp.Option{
|
|
IgnoreUnexported(ParentStruct{}, PublicStruct{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}),
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal since ParentStruct.privateStruct differs",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(privateStruct{}, PublicStruct{}),
|
|
IgnoreUnexported(ParentStruct{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(PublicStruct{}, ParentStruct{}),
|
|
IgnoreUnexported(privateStruct{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
|
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
|
opts: []cmp.Option{
|
|
cmp.AllowUnexported(PublicStruct{}, ParentStruct{}),
|
|
IgnoreUnexported(privateStruct{}),
|
|
},
|
|
wantEqual: false,
|
|
reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})",
|
|
}, {
|
|
label: "IgnoreFields+IgnoreTypes+IgnoreUnexported",
|
|
x: &Everything{
|
|
MyInt: 5,
|
|
MyFloat: 3.3,
|
|
MyTime: MyTime{time.Now()},
|
|
Bar3: *createBar3X(),
|
|
ParentStruct: ParentStruct{
|
|
Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4},
|
|
},
|
|
},
|
|
y: &Everything{
|
|
MyInt: -5,
|
|
MyFloat: 3.3,
|
|
MyTime: MyTime{time.Now()},
|
|
Bar3: *createBar3Y(),
|
|
ParentStruct: ParentStruct{
|
|
Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4},
|
|
},
|
|
},
|
|
opts: []cmp.Option{
|
|
IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"),
|
|
IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"),
|
|
IgnoreTypes(MyInt(0), PublicStruct{}),
|
|
IgnoreUnexported(ParentStruct{}),
|
|
},
|
|
wantEqual: true,
|
|
reason: "equal because all Ignore options can be composed together",
|
|
}}
|
|
|
|
for _, tt := range tests {
|
|
tRun(t, tt.label, func(t *testing.T) {
|
|
var gotEqual bool
|
|
var gotPanic string
|
|
func() {
|
|
defer func() {
|
|
if ex := recover(); ex != nil {
|
|
gotPanic = fmt.Sprint(ex)
|
|
}
|
|
}()
|
|
gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...)
|
|
}()
|
|
switch {
|
|
case gotPanic == "" && tt.wantPanic:
|
|
t.Errorf("expected Equal panic\nreason: %s", tt.reason)
|
|
case gotPanic != "" && !tt.wantPanic:
|
|
t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason)
|
|
case gotEqual != tt.wantEqual:
|
|
t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPanic(t *testing.T) {
|
|
args := func(x ...interface{}) []interface{} { return x }
|
|
tests := []struct {
|
|
label string // Test name
|
|
fnc interface{} // Option function to call
|
|
args []interface{} // Arguments to pass in
|
|
wantPanic string // Expected panic message
|
|
reason string // The reason for the expected outcome
|
|
}{{
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(0.0, 0.0),
|
|
reason: "zero margin and fraction is equivalent to exact equality",
|
|
}, {
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(-0.1, 0.0),
|
|
wantPanic: "margin or fraction must be a non-negative number",
|
|
reason: "negative inputs are invalid",
|
|
}, {
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(0.0, -0.1),
|
|
wantPanic: "margin or fraction must be a non-negative number",
|
|
reason: "negative inputs are invalid",
|
|
}, {
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(math.NaN(), 0.0),
|
|
wantPanic: "margin or fraction must be a non-negative number",
|
|
reason: "NaN inputs are invalid",
|
|
}, {
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(1.0, 0.0),
|
|
reason: "fraction of 1.0 or greater is valid",
|
|
}, {
|
|
label: "EquateApprox",
|
|
fnc: EquateApprox,
|
|
args: args(0.0, math.Inf(+1)),
|
|
reason: "margin of infinity is valid",
|
|
}, {
|
|
label: "SortSlices",
|
|
fnc: SortSlices,
|
|
args: args(strings.Compare),
|
|
wantPanic: "invalid less function",
|
|
reason: "func(x, y string) int is wrong signature for less",
|
|
}, {
|
|
label: "SortSlices",
|
|
fnc: SortSlices,
|
|
args: args((func(_, _ int) bool)(nil)),
|
|
wantPanic: "invalid less function",
|
|
reason: "nil value is not valid",
|
|
}, {
|
|
label: "SortMaps",
|
|
fnc: SortMaps,
|
|
args: args(strings.Compare),
|
|
wantPanic: "invalid less function",
|
|
reason: "func(x, y string) int is wrong signature for less",
|
|
}, {
|
|
label: "SortMaps",
|
|
fnc: SortMaps,
|
|
args: args((func(_, _ int) bool)(nil)),
|
|
wantPanic: "invalid less function",
|
|
reason: "nil value is not valid",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, ""),
|
|
wantPanic: "name must not be empty",
|
|
reason: "empty selector is invalid",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "."),
|
|
wantPanic: "name must not be empty",
|
|
reason: "single dot selector is invalid",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, ".Alpha"),
|
|
reason: "dot-prefix is okay since Foo1.Alpha reads naturally",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "Alpha."),
|
|
wantPanic: "name must not be empty",
|
|
reason: "dot-suffix is invalid",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "Alpha "),
|
|
wantPanic: "does not exist",
|
|
reason: "identifiers must not have spaces",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "Zulu"),
|
|
wantPanic: "does not exist",
|
|
reason: "name of non-existent field is invalid",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "Alpha.NoExist"),
|
|
wantPanic: "must be a struct",
|
|
reason: "cannot select into a non-struct",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(&Foo1{}, "Alpha"),
|
|
wantPanic: "must be a struct",
|
|
reason: "the type must be a struct (not pointer to a struct)",
|
|
}, {
|
|
label: "IgnoreFields",
|
|
fnc: IgnoreFields,
|
|
args: args(Foo1{}, "unexported"),
|
|
wantPanic: "name must be exported",
|
|
reason: "unexported fields must not be specified",
|
|
}, {
|
|
label: "IgnoreTypes",
|
|
fnc: IgnoreTypes,
|
|
reason: "empty input is valid",
|
|
}, {
|
|
label: "IgnoreTypes",
|
|
fnc: IgnoreTypes,
|
|
args: args(nil),
|
|
wantPanic: "cannot determine type",
|
|
reason: "input must not be nil value",
|
|
}, {
|
|
label: "IgnoreTypes",
|
|
fnc: IgnoreTypes,
|
|
args: args(0, 0, 0),
|
|
reason: "duplicate inputs of the same type is valid",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(nil),
|
|
wantPanic: "input must be an anonymous struct",
|
|
reason: "input must not be nil value",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(Foo1{}),
|
|
wantPanic: "input must be an anonymous struct",
|
|
reason: "input must not be a named struct type",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(struct{ _ io.Reader }{}),
|
|
wantPanic: "struct cannot have named fields",
|
|
reason: "input must not have named fields",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(struct{ Foo1 }{}),
|
|
wantPanic: "embedded field must be an interface type",
|
|
reason: "field types must be interfaces",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(struct{ EmptyInterface }{}),
|
|
wantPanic: "cannot ignore empty interface",
|
|
reason: "field types must not be the empty interface",
|
|
}, {
|
|
label: "IgnoreInterfaces",
|
|
fnc: IgnoreInterfaces,
|
|
args: args(struct {
|
|
io.Reader
|
|
io.Writer
|
|
io.Closer
|
|
io.ReadWriteCloser
|
|
}{}),
|
|
reason: "multiple interfaces may be specified, even if they overlap",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
fnc: IgnoreUnexported,
|
|
reason: "empty input is valid",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
fnc: IgnoreUnexported,
|
|
args: args(nil),
|
|
wantPanic: "invalid struct type",
|
|
reason: "input must not be nil value",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
fnc: IgnoreUnexported,
|
|
args: args(&Foo1{}),
|
|
wantPanic: "invalid struct type",
|
|
reason: "input must be a struct type (not a pointer to a struct)",
|
|
}, {
|
|
label: "IgnoreUnexported",
|
|
fnc: IgnoreUnexported,
|
|
args: args(Foo1{}, struct{ x, X int }{}),
|
|
reason: "input may be named or unnamed structs",
|
|
}}
|
|
|
|
for _, tt := range tests {
|
|
tRun(t, tt.label, func(t *testing.T) {
|
|
// Prepare function arguments.
|
|
vf := reflect.ValueOf(tt.fnc)
|
|
var vargs []reflect.Value
|
|
for i, arg := range tt.args {
|
|
if arg == nil {
|
|
tf := vf.Type()
|
|
if i == tf.NumIn()-1 && tf.IsVariadic() {
|
|
vargs = append(vargs, reflect.Zero(tf.In(i).Elem()))
|
|
} else {
|
|
vargs = append(vargs, reflect.Zero(tf.In(i)))
|
|
}
|
|
} else {
|
|
vargs = append(vargs, reflect.ValueOf(arg))
|
|
}
|
|
}
|
|
|
|
// Call the function and capture any panics.
|
|
var gotPanic string
|
|
func() {
|
|
defer func() {
|
|
if ex := recover(); ex != nil {
|
|
if s, ok := ex.(string); ok {
|
|
gotPanic = s
|
|
} else {
|
|
panic(ex)
|
|
}
|
|
}
|
|
}()
|
|
vf.Call(vargs)
|
|
}()
|
|
|
|
switch {
|
|
case tt.wantPanic == "" && gotPanic != "":
|
|
t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason)
|
|
case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic):
|
|
t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: Delete this hack when we drop Go1.6 support.
|
|
func tRun(t *testing.T, name string, f func(t *testing.T)) {
|
|
type runner interface {
|
|
Run(string, func(t *testing.T)) bool
|
|
}
|
|
var ti interface{} = t
|
|
if r, ok := ti.(runner); ok {
|
|
r.Run(name, f)
|
|
} else {
|
|
t.Logf("Test: %s", name)
|
|
f(t)
|
|
}
|
|
}
|