// 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" "errors" "fmt" "io" "math" "reflect" "strings" "sync" "testing" "time" "github.com/google/go-cmp/cmp" "golang.org/x/xerrors" ) 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: xerrors.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: xerrors.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: xerrors.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: xerrors.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 }{xerrors.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 }{xerrors.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: "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) } }) } }