blob: 6a7c30085247e047a592bfaa2198c42d492daf0c [file] [log] [blame]
// 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 file.
package cmpopts
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"net/netip"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
type (
MyInt int
MyInts []int
MyFloat float32
MyString string
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: "EquateApproxTime",
x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(0)},
wantEqual: true,
reason: "equal because times are identical",
}, {
label: "EquateApproxTime",
x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: true,
reason: "equal because time is exactly at the allowed margin",
}, {
label: "EquateApproxTime",
x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC),
y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: true,
reason: "equal because time is exactly at the allowed margin (negative)",
}, {
label: "EquateApproxTime",
x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC),
y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)},
wantEqual: false,
reason: "not equal because time is outside allowed margin",
}, {
label: "EquateApproxTime",
x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC),
y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)},
wantEqual: false,
reason: "not equal because time is outside allowed margin (negative)",
}, {
label: "EquateApproxTime",
x: time.Time{},
y: time.Time{},
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: true,
reason: "equal because both times are zero",
}, {
label: "EquateApproxTime",
x: time.Time{},
y: time.Time{}.Add(1),
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: false,
reason: "not equal because zero time is always not equal not non-zero",
}, {
label: "EquateApproxTime",
x: time.Time{}.Add(1),
y: time.Time{},
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: false,
reason: "not equal because zero time is always not equal not non-zero",
}, {
label: "EquateApproxTime",
x: time.Date(2409, 11, 10, 23, 0, 0, 0, time.UTC),
y: time.Date(2000, 11, 10, 23, 0, 3, 0, time.UTC),
opts: []cmp.Option{EquateApproxTime(3 * time.Second)},
wantEqual: false,
reason: "time difference overflows time.Duration",
}, {
label: "EquateErrors",
x: nil,
y: nil,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "nil values are equal",
}, {
label: "EquateErrors",
x: errors.New("EOF"),
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "user-defined EOF is not exactly equal",
}, {
label: "EquateErrors",
x: fmt.Errorf("wrapped: %w", io.EOF),
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "wrapped io.EOF is equal according to errors.Is",
}, {
label: "EquateErrors",
x: fmt.Errorf("wrapped: %w", io.EOF),
y: io.EOF,
wantEqual: false,
reason: "wrapped io.EOF is not equal without EquateErrors option",
}, {
label: "EquateErrors",
x: io.EOF,
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "sentinel errors are equal",
}, {
label: "EquateErrors",
x: io.EOF,
y: AnyError,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "AnyError is equal to any non-nil error",
}, {
label: "EquateErrors",
x: io.EOF,
y: AnyError,
wantEqual: false,
reason: "AnyError is not equal to any non-nil error without EquateErrors option",
}, {
label: "EquateErrors",
x: nil,
y: AnyError,
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "AnyError is not equal to nil value",
}, {
label: "EquateErrors",
x: nil,
y: nil,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "nil values are equal",
}, {
label: "EquateErrors",
x: errors.New("EOF"),
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "user-defined EOF is not exactly equal",
}, {
label: "EquateErrors",
x: fmt.Errorf("wrapped: %w", io.EOF),
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "wrapped io.EOF is equal according to errors.Is",
}, {
label: "EquateErrors",
x: fmt.Errorf("wrapped: %w", io.EOF),
y: io.EOF,
wantEqual: false,
reason: "wrapped io.EOF is not equal without EquateErrors option",
}, {
label: "EquateErrors",
x: io.EOF,
y: io.EOF,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "sentinel errors are equal",
}, {
label: "EquateErrors",
x: io.EOF,
y: AnyError,
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "AnyError is equal to any non-nil error",
}, {
label: "EquateErrors",
x: io.EOF,
y: AnyError,
wantEqual: false,
reason: "AnyError is not equal to any non-nil error without EquateErrors option",
}, {
label: "EquateErrors",
x: nil,
y: AnyError,
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "AnyError is not equal to nil value",
}, {
label: "EquateErrors",
x: struct{ E error }{nil},
y: struct{ E error }{nil},
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "nil values are equal",
}, {
label: "EquateErrors",
x: struct{ E error }{errors.New("EOF")},
y: struct{ E error }{io.EOF},
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "user-defined EOF is not exactly equal",
}, {
label: "EquateErrors",
x: struct{ E error }{fmt.Errorf("wrapped: %w", io.EOF)},
y: struct{ E error }{io.EOF},
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "wrapped io.EOF is equal according to errors.Is",
}, {
label: "EquateErrors",
x: struct{ E error }{fmt.Errorf("wrapped: %w", io.EOF)},
y: struct{ E error }{io.EOF},
wantEqual: false,
reason: "wrapped io.EOF is not equal without EquateErrors option",
}, {
label: "EquateErrors",
x: struct{ E error }{io.EOF},
y: struct{ E error }{io.EOF},
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "sentinel errors are equal",
}, {
label: "EquateErrors",
x: struct{ E error }{io.EOF},
y: struct{ E error }{AnyError},
opts: []cmp.Option{EquateErrors()},
wantEqual: true,
reason: "AnyError is equal to any non-nil error",
}, {
label: "EquateErrors",
x: struct{ E error }{io.EOF},
y: struct{ E error }{AnyError},
wantEqual: false,
reason: "AnyError is not equal to any non-nil error without EquateErrors option",
}, {
label: "EquateErrors",
x: struct{ E error }{nil},
y: struct{ E error }{AnyError},
opts: []cmp.Option{EquateErrors()},
wantEqual: false,
reason: "AnyError is not equal to nil value",
}, {
label: "EquateComparable",
x: []struct{ P netip.Addr }{
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
},
y: []struct{ P netip.Addr }{
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
},
opts: []cmp.Option{EquateComparable(netip.Addr{})},
wantEqual: true,
reason: "equal because all IP addresses are the same",
}, {
label: "EquateComparable",
x: []struct{ P netip.Addr }{
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
{netip.AddrFrom4([4]byte{1, 2, 3, 5})},
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
},
y: []struct{ P netip.Addr }{
{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
{netip.AddrFrom4([4]byte{1, 2, 3, 7})},
{netip.AddrFrom4([4]byte{1, 2, 3, 6})},
},
opts: []cmp.Option{EquateComparable(netip.Addr{})},
wantEqual: false,
reason: "not equal because second IP address is different",
}, {
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: "IgnoreFields",
x: ParentStruct{
privateStruct: &privateStruct{private: 1},
PublicStruct: &PublicStruct{private: 2},
private: 3,
},
y: ParentStruct{
privateStruct: &privateStruct{private: 10},
PublicStruct: &PublicStruct{private: 20},
private: 30,
},
opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{})},
wantEqual: false,
reason: "not equal because unexported fields mismatch",
}, {
label: "IgnoreFields",
x: ParentStruct{
privateStruct: &privateStruct{private: 1},
PublicStruct: &PublicStruct{private: 2},
private: 3,
},
y: ParentStruct{
privateStruct: &privateStruct{private: 10},
PublicStruct: &PublicStruct{private: 20},
private: 30,
},
opts: []cmp.Option{
cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{}),
IgnoreFields(ParentStruct{}, "PublicStruct.private", "privateStruct.private", "private"),
},
wantEqual: true,
reason: "equal because mismatching unexported fields are ignored",
}, {
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",
}, {
label: "IgnoreSliceElements",
x: []int{1, 0, 2, 3, 0, 4, 0, 0},
y: []int{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: true,
reason: "equal because zero elements are ignored",
}, {
label: "IgnoreSliceElements",
x: []MyInt{1, 0, 2, 3, 0, 4, 0, 0},
y: []MyInt{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: false,
reason: "not equal because MyInt is not assignable to int",
}, {
label: "IgnoreSliceElements",
x: MyInts{1, 0, 2, 3, 0, 4, 0, 0},
y: MyInts{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: true,
reason: "equal because the element type of MyInts is assignable to int",
}, {
label: "IgnoreSliceElements+EquateEmpty",
x: []MyInt{},
y: []MyInt{0, 0, 0, 0},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
EquateEmpty(),
},
wantEqual: false,
reason: "not equal because ignored elements does not imply empty slice",
}, {
label: "IgnoreMapEntries",
x: map[string]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[string]int{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: true,
reason: "equal because uppercase keys are ignored",
}, {
label: "IgnoreMapEntries",
x: map[MyString]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[MyString]int{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: false,
reason: "not equal because MyString is not assignable to string",
}, {
label: "IgnoreMapEntries",
x: map[string]MyInt{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[string]MyInt{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: false,
reason: "not equal because MyInt is not assignable to int",
}, {
label: "IgnoreMapEntries+EquateEmpty",
x: map[string]MyInt{"ONE": 1, "TWO": 2, "THREE": 3},
y: nil,
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
EquateEmpty(),
},
wantEqual: false,
reason: "not equal because ignored entries does not imply empty map",
}, {
label: "AcyclicTransformer",
x: "a\nb\nc\nd",
y: "a\nb\nd\nd",
opts: []cmp.Option{
AcyclicTransformer("", func(s string) []string { return strings.Split(s, "\n") }),
},
wantEqual: false,
reason: "not equal because 3rd line differs, but should not recurse infinitely",
}, {
label: "AcyclicTransformer",
x: []string{"foo", "Bar", "BAZ"},
y: []string{"Foo", "BAR", "baz"},
opts: []cmp.Option{
AcyclicTransformer("", strings.ToUpper),
},
wantEqual: true,
reason: "equal because of strings.ToUpper; AcyclicTransformer unnecessary, but check this still works",
}, {
label: "AcyclicTransformer",
x: "this is a sentence",
y: "this is a sentence",
opts: []cmp.Option{
AcyclicTransformer("", strings.Fields),
},
wantEqual: true,
reason: "equal because acyclic transformer splits on any contiguous whitespace",
}}
for _, tt := range tests {
t.Run(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 tt.reason == "":
t.Errorf("reason must be provided")
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: "EquateApproxTime",
fnc: EquateApproxTime,
args: args(time.Duration(-1)),
wantPanic: "margin must be a non-negative number",
reason: "negative duration is invalid",
}, {
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 non-pointer struct",
reason: "the type must be a struct (not pointer to a struct)",
}, {
label: "IgnoreFields",
fnc: IgnoreFields,
args: args(struct{ privateStruct }{}, "privateStruct"),
reason: "privateStruct field permitted since it is the default name of the embedded type",
}, {
label: "IgnoreFields",
fnc: IgnoreFields,
args: args(struct{ privateStruct }{}, "Public"),
reason: "Public field permitted since it is a forwarded field that is exported",
}, {
label: "IgnoreFields",
fnc: IgnoreFields,
args: args(struct{ privateStruct }{}, "private"),
wantPanic: "does not exist",
reason: "private field not permitted since it is a forwarded field that is unexported",
}, {
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: "must be a non-pointer struct",
reason: "input must not be nil value",
}, {
label: "IgnoreUnexported",
fnc: IgnoreUnexported,
args: args(&Foo1{}),
wantPanic: "must be a non-pointer struct",
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",
}, {
label: "AcyclicTransformer",
fnc: AcyclicTransformer,
args: args("", "not a func"),
wantPanic: "invalid transformer function",
reason: "AcyclicTransformer has same input requirements as Transformer",
}}
for _, tt := range tests {
t.Run(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.reason == "":
t.Errorf("reason must be provided")
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)
}
})
}
}