github.com/integration-system/go-cmp@v0.0.0-20190131081942-ac5582987a2f/cmp/cmpopts/util_test.go (about) 1 // Copyright 2017, The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE.md file. 4 5 package cmpopts 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "math" 12 "reflect" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/integration-system/go-cmp/cmp" 19 ) 20 21 type ( 22 MyInt int 23 MyFloat float32 24 MyTime struct{ time.Time } 25 MyStruct struct { 26 A, B []int 27 C, D map[time.Time]string 28 } 29 30 Foo1 struct{ Alpha, Bravo, Charlie int } 31 Foo2 struct{ *Foo1 } 32 Foo3 struct{ *Foo2 } 33 Bar1 struct{ Foo3 } 34 Bar2 struct { 35 Bar1 36 *Foo3 37 Bravo float32 38 } 39 Bar3 struct { 40 Bar1 41 Bravo *Bar2 42 Delta struct{ Echo Foo1 } 43 *Foo3 44 Alpha string 45 } 46 47 privateStruct struct{ Public, private int } 48 PublicStruct struct{ Public, private int } 49 ParentStruct struct { 50 *privateStruct 51 *PublicStruct 52 Public int 53 private int 54 } 55 56 Everything struct { 57 MyInt 58 MyFloat 59 MyTime 60 MyStruct 61 Bar3 62 ParentStruct 63 } 64 65 EmptyInterface interface{} 66 ) 67 68 func TestOptions(t *testing.T) { 69 createBar3X := func() *Bar3 { 70 return &Bar3{ 71 Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}}, 72 Bravo: &Bar2{ 73 Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}}, 74 Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}}, 75 Bravo: 4, 76 }, 77 Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}}, 78 Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}}, 79 Alpha: "alpha", 80 } 81 } 82 createBar3Y := func() *Bar3 { 83 return &Bar3{ 84 Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}}, 85 Bravo: &Bar2{ 86 Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}}, 87 Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}}, 88 Bravo: 5, 89 }, 90 Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}}, 91 Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}}, 92 Alpha: "ALPHA", 93 } 94 } 95 96 tests := []struct { 97 label string // Test name 98 x, y interface{} // Input values to compare 99 opts []cmp.Option // Input options 100 wantEqual bool // Whether the inputs are equal 101 wantPanic bool // Whether Equal should panic 102 reason string // The reason for the expected outcome 103 }{{ 104 label: "EquateEmpty", 105 x: []int{}, 106 y: []int(nil), 107 wantEqual: false, 108 reason: "not equal because empty non-nil and nil slice differ", 109 }, { 110 label: "EquateEmpty", 111 x: []int{}, 112 y: []int(nil), 113 opts: []cmp.Option{EquateEmpty()}, 114 wantEqual: true, 115 reason: "equal because EquateEmpty equates empty slices", 116 }, { 117 label: "SortSlices", 118 x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 119 y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, 120 wantEqual: false, 121 reason: "not equal because element order differs", 122 }, { 123 label: "SortSlices", 124 x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 125 y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, 126 opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, 127 wantEqual: true, 128 reason: "equal because SortSlices sorts the slices", 129 }, { 130 label: "SortSlices", 131 x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 132 y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, 133 opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, 134 wantEqual: false, 135 reason: "not equal because MyInt is not the same type as int", 136 }, { 137 label: "SortSlices", 138 x: []float64{0, 1, 1, 2, 2, 2}, 139 y: []float64{2, 0, 2, 1, 2, 1}, 140 opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, 141 wantEqual: true, 142 reason: "equal even when sorted with duplicate elements", 143 }, { 144 label: "SortSlices", 145 x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, 146 y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, 147 opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, 148 wantPanic: true, 149 reason: "panics because SortSlices used with non-transitive less function", 150 }, { 151 label: "SortSlices", 152 x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, 153 y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, 154 opts: []cmp.Option{SortSlices(func(x, y float64) bool { 155 return (!math.IsNaN(x) && math.IsNaN(y)) || x < y 156 })}, 157 wantEqual: false, 158 reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN", 159 }, { 160 label: "SortSlices+EquateNaNs", 161 x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4}, 162 y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2}, 163 opts: []cmp.Option{ 164 EquateNaNs(), 165 SortSlices(func(x, y float64) bool { 166 return (!math.IsNaN(x) && math.IsNaN(y)) || x < y 167 }), 168 }, 169 wantEqual: true, 170 reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used", 171 }, { 172 label: "SortMaps", 173 x: map[time.Time]string{ 174 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", 175 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", 176 time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", 177 }, 178 y: map[time.Time]string{ 179 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", 180 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", 181 time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", 182 }, 183 wantEqual: false, 184 reason: "not equal because timezones differ", 185 }, { 186 label: "SortMaps", 187 x: map[time.Time]string{ 188 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", 189 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", 190 time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", 191 }, 192 y: map[time.Time]string{ 193 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", 194 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", 195 time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", 196 }, 197 opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, 198 wantEqual: true, 199 reason: "equal because SortMaps flattens to a slice where Time.Equal can be used", 200 }, { 201 label: "SortMaps", 202 x: map[MyTime]string{ 203 {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday", 204 {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday", 205 {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday", 206 }, 207 y: map[MyTime]string{ 208 {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday", 209 {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday", 210 {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday", 211 }, 212 opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, 213 wantEqual: false, 214 reason: "not equal because MyTime is not assignable to time.Time", 215 }, { 216 label: "SortMaps", 217 x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, 218 // => {0, 1, 2, 3, -1, -2, -3}, 219 y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, 220 // => {0, 1, 2, 3, 100, 200, 300}, 221 opts: []cmp.Option{SortMaps(func(a, b int) bool { 222 if -10 < a && a <= 0 { 223 a *= -100 224 } 225 if -10 < b && b <= 0 { 226 b *= -100 227 } 228 return a < b 229 })}, 230 wantEqual: false, 231 reason: "not equal because values differ even though SortMap provides valid ordering", 232 }, { 233 label: "SortMaps", 234 x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, 235 // => {0, 1, 2, 3, -1, -2, -3}, 236 y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, 237 // => {0, 1, 2, 3, 100, 200, 300}, 238 opts: []cmp.Option{ 239 SortMaps(func(x, y int) bool { 240 if -10 < x && x <= 0 { 241 x *= -100 242 } 243 if -10 < y && y <= 0 { 244 y *= -100 245 } 246 return x < y 247 }), 248 cmp.Comparer(func(x, y int) bool { 249 if -10 < x && x <= 0 { 250 x *= -100 251 } 252 if -10 < y && y <= 0 { 253 y *= -100 254 } 255 return x == y 256 }), 257 }, 258 wantEqual: true, 259 reason: "equal because Comparer used to equate differences", 260 }, { 261 label: "SortMaps", 262 x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, 263 y: map[int]string{}, 264 opts: []cmp.Option{SortMaps(func(x, y int) bool { 265 return x < y && x >= 0 && y >= 0 266 })}, 267 wantPanic: true, 268 reason: "panics because SortMaps used with non-transitive less function", 269 }, { 270 label: "SortMaps", 271 x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, 272 y: map[int]string{}, 273 opts: []cmp.Option{SortMaps(func(x, y int) bool { 274 return math.Abs(float64(x)) < math.Abs(float64(y)) 275 })}, 276 wantPanic: true, 277 reason: "panics because SortMaps used with partial less function", 278 }, { 279 label: "EquateEmpty+SortSlices+SortMaps", 280 x: MyStruct{ 281 A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 282 C: map[time.Time]string{ 283 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", 284 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", 285 }, 286 D: map[time.Time]string{}, 287 }, 288 y: MyStruct{ 289 A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, 290 B: []int{}, 291 C: map[time.Time]string{ 292 time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", 293 time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", 294 }, 295 }, 296 opts: []cmp.Option{ 297 EquateEmpty(), 298 SortSlices(func(x, y int) bool { return x < y }), 299 SortMaps(func(x, y time.Time) bool { return x.Before(y) }), 300 }, 301 wantEqual: true, 302 reason: "no panics because EquateEmpty should compose with the sort options", 303 }, { 304 label: "EquateApprox", 305 x: 3.09, 306 y: 3.10, 307 wantEqual: false, 308 reason: "not equal because floats do not exactly matches", 309 }, { 310 label: "EquateApprox", 311 x: 3.09, 312 y: 3.10, 313 opts: []cmp.Option{EquateApprox(0, 0)}, 314 wantEqual: false, 315 reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==", 316 }, { 317 label: "EquateApprox", 318 x: 3.09, 319 y: 3.10, 320 opts: []cmp.Option{EquateApprox(0.003, 0.009)}, 321 wantEqual: false, 322 reason: "not equal because EquateApprox is too strict", 323 }, { 324 label: "EquateApprox", 325 x: 3.09, 326 y: 3.10, 327 opts: []cmp.Option{EquateApprox(0, 0.011)}, 328 wantEqual: true, 329 reason: "equal because margin is loose enough to match", 330 }, { 331 label: "EquateApprox", 332 x: 3.09, 333 y: 3.10, 334 opts: []cmp.Option{EquateApprox(0.004, 0)}, 335 wantEqual: true, 336 reason: "equal because fraction is loose enough to match", 337 }, { 338 label: "EquateApprox", 339 x: 3.09, 340 y: 3.10, 341 opts: []cmp.Option{EquateApprox(0.004, 0.011)}, 342 wantEqual: true, 343 reason: "equal because both the margin and fraction are loose enough to match", 344 }, { 345 label: "EquateApprox", 346 x: float32(3.09), 347 y: float64(3.10), 348 opts: []cmp.Option{EquateApprox(0.004, 0)}, 349 wantEqual: false, 350 reason: "not equal because the types differ", 351 }, { 352 label: "EquateApprox", 353 x: float32(3.09), 354 y: float32(3.10), 355 opts: []cmp.Option{EquateApprox(0.004, 0)}, 356 wantEqual: true, 357 reason: "equal because EquateApprox also applies on float32s", 358 }, { 359 label: "EquateApprox", 360 x: []float64{math.Inf(+1), math.Inf(-1)}, 361 y: []float64{math.Inf(+1), math.Inf(-1)}, 362 opts: []cmp.Option{EquateApprox(0, 1)}, 363 wantEqual: true, 364 reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ", 365 }, { 366 label: "EquateApprox", 367 x: []float64{math.Inf(+1), -1e100}, 368 y: []float64{+1e100, math.Inf(-1)}, 369 opts: []cmp.Option{EquateApprox(0, 1)}, 370 wantEqual: false, 371 reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)", 372 }, { 373 label: "EquateApprox", 374 x: float64(+1e100), 375 y: float64(-1e100), 376 opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)}, 377 wantEqual: true, 378 reason: "equal because infinite fraction matches everything", 379 }, { 380 label: "EquateApprox", 381 x: float64(+1e100), 382 y: float64(-1e100), 383 opts: []cmp.Option{EquateApprox(0, math.Inf(+1))}, 384 wantEqual: true, 385 reason: "equal because infinite margin matches everything", 386 }, { 387 label: "EquateApprox", 388 x: math.Pi, 389 y: math.Pi, 390 opts: []cmp.Option{EquateApprox(0, 0)}, 391 wantEqual: true, 392 reason: "equal because EquateApprox(0, 0) is equivalent to ==", 393 }, { 394 label: "EquateApprox", 395 x: math.Pi, 396 y: math.Nextafter(math.Pi, math.Inf(+1)), 397 opts: []cmp.Option{EquateApprox(0, 0)}, 398 wantEqual: false, 399 reason: "not equal because EquateApprox(0, 0) is equivalent to ==", 400 }, { 401 label: "EquateNaNs", 402 x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, 403 y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, 404 wantEqual: false, 405 reason: "not equal because NaN != NaN", 406 }, { 407 label: "EquateNaNs", 408 x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, 409 y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, 410 opts: []cmp.Option{EquateNaNs()}, 411 wantEqual: true, 412 reason: "equal because EquateNaNs allows NaN == NaN", 413 }, { 414 label: "EquateNaNs", 415 x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, 416 y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, 417 opts: []cmp.Option{EquateNaNs()}, 418 wantEqual: true, 419 reason: "equal because EquateNaNs operates on float32", 420 }, { 421 label: "EquateApprox+EquateNaNs", 422 x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001}, 423 y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002}, 424 opts: []cmp.Option{ 425 EquateNaNs(), 426 EquateApprox(0.01, 0), 427 }, 428 wantEqual: true, 429 reason: "equal because EquateNaNs and EquateApprox compose together", 430 }, { 431 label: "EquateApprox+EquateNaNs", 432 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}, 433 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}, 434 opts: []cmp.Option{ 435 EquateNaNs(), 436 EquateApprox(0.01, 0), 437 }, 438 wantEqual: false, 439 reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type", 440 }, { 441 label: "EquateApprox+EquateNaNs+Transform", 442 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}, 443 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}, 444 opts: []cmp.Option{ 445 cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }), 446 EquateNaNs(), 447 EquateApprox(0.01, 0), 448 }, 449 wantEqual: true, 450 reason: "equal because named type is transformed to float64", 451 }, { 452 label: "IgnoreFields", 453 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 454 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 455 wantEqual: false, 456 reason: "not equal because values do not match in deeply embedded field", 457 }, { 458 label: "IgnoreFields", 459 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 460 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 461 opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")}, 462 wantEqual: true, 463 reason: "equal because IgnoreField ignores deeply embedded field: Alpha", 464 }, { 465 label: "IgnoreFields", 466 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 467 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 468 opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")}, 469 wantEqual: true, 470 reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha", 471 }, { 472 label: "IgnoreFields", 473 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 474 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 475 opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")}, 476 wantEqual: true, 477 reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha", 478 }, { 479 label: "IgnoreFields", 480 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 481 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 482 opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")}, 483 wantEqual: true, 484 reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha", 485 }, { 486 label: "IgnoreFields", 487 x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, 488 y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, 489 opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")}, 490 wantEqual: true, 491 reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha", 492 }, { 493 label: "IgnoreFields", 494 x: createBar3X(), 495 y: createBar3Y(), 496 wantEqual: false, 497 reason: "not equal because many deeply nested or embedded fields differ", 498 }, { 499 label: "IgnoreFields", 500 x: createBar3X(), 501 y: createBar3Y(), 502 opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")}, 503 wantEqual: true, 504 reason: "equal because IgnoreFields ignores fields at the highest levels", 505 }, { 506 label: "IgnoreFields", 507 x: createBar3X(), 508 y: createBar3Y(), 509 opts: []cmp.Option{ 510 IgnoreFields(Bar3{}, 511 "Bar1.Foo3.Bravo", 512 "Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", 513 "Bravo.Foo3.Foo2.Foo1.Bravo", 514 "Bravo.Bravo", 515 "Delta.Echo.Charlie", 516 "Foo3.Foo2.Foo1.Alpha", 517 "Alpha", 518 ), 519 }, 520 wantEqual: true, 521 reason: "equal because IgnoreFields ignores fields using fully-qualified field", 522 }, { 523 label: "IgnoreFields", 524 x: createBar3X(), 525 y: createBar3Y(), 526 opts: []cmp.Option{ 527 IgnoreFields(Bar3{}, 528 "Bar1.Foo3.Bravo", 529 "Bravo.Foo3.Foo2.Foo1.Bravo", 530 "Bravo.Bravo", 531 "Delta.Echo.Charlie", 532 "Foo3.Foo2.Foo1.Alpha", 533 "Alpha", 534 ), 535 }, 536 wantEqual: false, 537 reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", 538 }, { 539 label: "IgnoreFields", 540 x: createBar3X(), 541 y: createBar3Y(), 542 opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")}, 543 wantEqual: false, 544 reason: "not equal because highest-level field is not ignored: Foo3", 545 }, { 546 label: "IgnoreTypes", 547 x: []interface{}{5, "same"}, 548 y: []interface{}{6, "same"}, 549 wantEqual: false, 550 reason: "not equal because 5 != 6", 551 }, { 552 label: "IgnoreTypes", 553 x: []interface{}{5, "same"}, 554 y: []interface{}{6, "same"}, 555 opts: []cmp.Option{IgnoreTypes(0)}, 556 wantEqual: true, 557 reason: "equal because ints are ignored", 558 }, { 559 label: "IgnoreTypes+IgnoreInterfaces", 560 x: []interface{}{5, "same", new(bytes.Buffer)}, 561 y: []interface{}{6, "same", new(bytes.Buffer)}, 562 opts: []cmp.Option{IgnoreTypes(0)}, 563 wantPanic: true, 564 reason: "panics because bytes.Buffer has unexported fields", 565 }, { 566 label: "IgnoreTypes+IgnoreInterfaces", 567 x: []interface{}{5, "same", new(bytes.Buffer)}, 568 y: []interface{}{6, "diff", new(bytes.Buffer)}, 569 opts: []cmp.Option{ 570 IgnoreTypes(0, ""), 571 IgnoreInterfaces(struct{ io.Reader }{}), 572 }, 573 wantEqual: true, 574 reason: "equal because bytes.Buffer is ignored by match on interface type", 575 }, { 576 label: "IgnoreTypes+IgnoreInterfaces", 577 x: []interface{}{5, "same", new(bytes.Buffer)}, 578 y: []interface{}{6, "same", new(bytes.Buffer)}, 579 opts: []cmp.Option{ 580 IgnoreTypes(0, ""), 581 IgnoreInterfaces(struct { 582 io.Reader 583 io.Writer 584 fmt.Stringer 585 }{}), 586 }, 587 wantEqual: true, 588 reason: "equal because bytes.Buffer is ignored by match on multiple interface types", 589 }, { 590 label: "IgnoreInterfaces", 591 x: struct{ mu sync.Mutex }{}, 592 y: struct{ mu sync.Mutex }{}, 593 wantPanic: true, 594 reason: "panics because sync.Mutex has unexported fields", 595 }, { 596 label: "IgnoreInterfaces", 597 x: struct{ mu sync.Mutex }{}, 598 y: struct{ mu sync.Mutex }{}, 599 opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, 600 wantEqual: true, 601 reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)", 602 }, { 603 label: "IgnoreInterfaces", 604 x: struct{ mu *sync.Mutex }{}, 605 y: struct{ mu *sync.Mutex }{}, 606 opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, 607 wantEqual: true, 608 reason: "equal because IgnoreInterfaces applies on pointers", 609 }, { 610 label: "IgnoreUnexported", 611 x: ParentStruct{Public: 1, private: 2}, 612 y: ParentStruct{Public: 1, private: -2}, 613 opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})}, 614 wantEqual: false, 615 reason: "not equal because ParentStruct.private differs with AllowUnexported", 616 }, { 617 label: "IgnoreUnexported", 618 x: ParentStruct{Public: 1, private: 2}, 619 y: ParentStruct{Public: 1, private: -2}, 620 opts: []cmp.Option{IgnoreUnexported(ParentStruct{})}, 621 wantEqual: true, 622 reason: "equal because IgnoreUnexported ignored ParentStruct.private", 623 }, { 624 label: "IgnoreUnexported", 625 x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, 626 y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, 627 opts: []cmp.Option{ 628 cmp.AllowUnexported(PublicStruct{}), 629 IgnoreUnexported(ParentStruct{}), 630 }, 631 wantEqual: true, 632 reason: "equal because ParentStruct.private is ignored", 633 }, { 634 label: "IgnoreUnexported", 635 x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, 636 y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, 637 opts: []cmp.Option{ 638 cmp.AllowUnexported(PublicStruct{}), 639 IgnoreUnexported(ParentStruct{}), 640 }, 641 wantEqual: false, 642 reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})", 643 }, { 644 label: "IgnoreUnexported", 645 x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, 646 y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, 647 opts: []cmp.Option{ 648 IgnoreUnexported(ParentStruct{}, PublicStruct{}), 649 }, 650 wantEqual: true, 651 reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored", 652 }, { 653 label: "IgnoreUnexported", 654 x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, 655 y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, 656 opts: []cmp.Option{ 657 cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}), 658 }, 659 wantEqual: false, 660 reason: "not equal since ParentStruct.privateStruct differs", 661 }, { 662 label: "IgnoreUnexported", 663 x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, 664 y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, 665 opts: []cmp.Option{ 666 cmp.AllowUnexported(privateStruct{}, PublicStruct{}), 667 IgnoreUnexported(ParentStruct{}), 668 }, 669 wantEqual: true, 670 reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})", 671 }, { 672 label: "IgnoreUnexported", 673 x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, 674 y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}}, 675 opts: []cmp.Option{ 676 cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), 677 IgnoreUnexported(privateStruct{}), 678 }, 679 wantEqual: true, 680 reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})", 681 }, { 682 label: "IgnoreUnexported", 683 x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, 684 y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, 685 opts: []cmp.Option{ 686 cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), 687 IgnoreUnexported(privateStruct{}), 688 }, 689 wantEqual: false, 690 reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})", 691 }, { 692 label: "IgnoreFields+IgnoreTypes+IgnoreUnexported", 693 x: &Everything{ 694 MyInt: 5, 695 MyFloat: 3.3, 696 MyTime: MyTime{time.Now()}, 697 Bar3: *createBar3X(), 698 ParentStruct: ParentStruct{ 699 Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}, 700 }, 701 }, 702 y: &Everything{ 703 MyInt: -5, 704 MyFloat: 3.3, 705 MyTime: MyTime{time.Now()}, 706 Bar3: *createBar3Y(), 707 ParentStruct: ParentStruct{ 708 Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4}, 709 }, 710 }, 711 opts: []cmp.Option{ 712 IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"), 713 IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"), 714 IgnoreTypes(MyInt(0), PublicStruct{}), 715 IgnoreUnexported(ParentStruct{}), 716 }, 717 wantEqual: true, 718 reason: "equal because all Ignore options can be composed together", 719 }, { 720 label: "AcyclicTransformer", 721 x: "a\nb\nc\nd", 722 y: "a\nb\nd\nd", 723 opts: []cmp.Option{ 724 AcyclicTransformer("", func(s string) []string { return strings.Split(s, "\n") }), 725 }, 726 wantEqual: false, 727 reason: "not equal because 3rd line differs, but should not recurse infinitely", 728 }, { 729 label: "AcyclicTransformer", 730 x: []string{"foo", "Bar", "BAZ"}, 731 y: []string{"Foo", "BAR", "baz"}, 732 opts: []cmp.Option{ 733 AcyclicTransformer("", func(s string) string { return strings.ToUpper(s) }), 734 }, 735 wantEqual: true, 736 reason: "equal because of strings.ToUpper; AcyclicTransformer unnecessary, but check this still works", 737 }, { 738 label: "AcyclicTransformer", 739 x: "this is a sentence", 740 y: "this is a sentence", 741 opts: []cmp.Option{ 742 AcyclicTransformer("", func(s string) []string { return strings.Fields(s) }), 743 }, 744 wantEqual: true, 745 reason: "equal because acyclic transformer splits on any contiguous whitespace", 746 }} 747 748 for _, tt := range tests { 749 t.Run(tt.label, func(t *testing.T) { 750 var gotEqual bool 751 var gotPanic string 752 func() { 753 defer func() { 754 if ex := recover(); ex != nil { 755 gotPanic = fmt.Sprint(ex) 756 } 757 }() 758 gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...) 759 }() 760 switch { 761 case tt.reason == "": 762 t.Errorf("reason must be provided") 763 case gotPanic == "" && tt.wantPanic: 764 t.Errorf("expected Equal panic\nreason: %s", tt.reason) 765 case gotPanic != "" && !tt.wantPanic: 766 t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason) 767 case gotEqual != tt.wantEqual: 768 t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) 769 } 770 }) 771 } 772 } 773 774 func TestPanic(t *testing.T) { 775 args := func(x ...interface{}) []interface{} { return x } 776 tests := []struct { 777 label string // Test name 778 fnc interface{} // Option function to call 779 args []interface{} // Arguments to pass in 780 wantPanic string // Expected panic message 781 reason string // The reason for the expected outcome 782 }{{ 783 label: "EquateApprox", 784 fnc: EquateApprox, 785 args: args(0.0, 0.0), 786 reason: "zero margin and fraction is equivalent to exact equality", 787 }, { 788 label: "EquateApprox", 789 fnc: EquateApprox, 790 args: args(-0.1, 0.0), 791 wantPanic: "margin or fraction must be a non-negative number", 792 reason: "negative inputs are invalid", 793 }, { 794 label: "EquateApprox", 795 fnc: EquateApprox, 796 args: args(0.0, -0.1), 797 wantPanic: "margin or fraction must be a non-negative number", 798 reason: "negative inputs are invalid", 799 }, { 800 label: "EquateApprox", 801 fnc: EquateApprox, 802 args: args(math.NaN(), 0.0), 803 wantPanic: "margin or fraction must be a non-negative number", 804 reason: "NaN inputs are invalid", 805 }, { 806 label: "EquateApprox", 807 fnc: EquateApprox, 808 args: args(1.0, 0.0), 809 reason: "fraction of 1.0 or greater is valid", 810 }, { 811 label: "EquateApprox", 812 fnc: EquateApprox, 813 args: args(0.0, math.Inf(+1)), 814 reason: "margin of infinity is valid", 815 }, { 816 label: "SortSlices", 817 fnc: SortSlices, 818 args: args(strings.Compare), 819 wantPanic: "invalid less function", 820 reason: "func(x, y string) int is wrong signature for less", 821 }, { 822 label: "SortSlices", 823 fnc: SortSlices, 824 args: args((func(_, _ int) bool)(nil)), 825 wantPanic: "invalid less function", 826 reason: "nil value is not valid", 827 }, { 828 label: "SortMaps", 829 fnc: SortMaps, 830 args: args(strings.Compare), 831 wantPanic: "invalid less function", 832 reason: "func(x, y string) int is wrong signature for less", 833 }, { 834 label: "SortMaps", 835 fnc: SortMaps, 836 args: args((func(_, _ int) bool)(nil)), 837 wantPanic: "invalid less function", 838 reason: "nil value is not valid", 839 }, { 840 label: "IgnoreFields", 841 fnc: IgnoreFields, 842 args: args(Foo1{}, ""), 843 wantPanic: "name must not be empty", 844 reason: "empty selector is invalid", 845 }, { 846 label: "IgnoreFields", 847 fnc: IgnoreFields, 848 args: args(Foo1{}, "."), 849 wantPanic: "name must not be empty", 850 reason: "single dot selector is invalid", 851 }, { 852 label: "IgnoreFields", 853 fnc: IgnoreFields, 854 args: args(Foo1{}, ".Alpha"), 855 reason: "dot-prefix is okay since Foo1.Alpha reads naturally", 856 }, { 857 label: "IgnoreFields", 858 fnc: IgnoreFields, 859 args: args(Foo1{}, "Alpha."), 860 wantPanic: "name must not be empty", 861 reason: "dot-suffix is invalid", 862 }, { 863 label: "IgnoreFields", 864 fnc: IgnoreFields, 865 args: args(Foo1{}, "Alpha "), 866 wantPanic: "does not exist", 867 reason: "identifiers must not have spaces", 868 }, { 869 label: "IgnoreFields", 870 fnc: IgnoreFields, 871 args: args(Foo1{}, "Zulu"), 872 wantPanic: "does not exist", 873 reason: "name of non-existent field is invalid", 874 }, { 875 label: "IgnoreFields", 876 fnc: IgnoreFields, 877 args: args(Foo1{}, "Alpha.NoExist"), 878 wantPanic: "must be a struct", 879 reason: "cannot select into a non-struct", 880 }, { 881 label: "IgnoreFields", 882 fnc: IgnoreFields, 883 args: args(&Foo1{}, "Alpha"), 884 wantPanic: "must be a struct", 885 reason: "the type must be a struct (not pointer to a struct)", 886 }, { 887 label: "IgnoreFields", 888 fnc: IgnoreFields, 889 args: args(Foo1{}, "unexported"), 890 wantPanic: "name must be exported", 891 reason: "unexported fields must not be specified", 892 }, { 893 label: "IgnoreTypes", 894 fnc: IgnoreTypes, 895 reason: "empty input is valid", 896 }, { 897 label: "IgnoreTypes", 898 fnc: IgnoreTypes, 899 args: args(nil), 900 wantPanic: "cannot determine type", 901 reason: "input must not be nil value", 902 }, { 903 label: "IgnoreTypes", 904 fnc: IgnoreTypes, 905 args: args(0, 0, 0), 906 reason: "duplicate inputs of the same type is valid", 907 }, { 908 label: "IgnoreInterfaces", 909 fnc: IgnoreInterfaces, 910 args: args(nil), 911 wantPanic: "input must be an anonymous struct", 912 reason: "input must not be nil value", 913 }, { 914 label: "IgnoreInterfaces", 915 fnc: IgnoreInterfaces, 916 args: args(Foo1{}), 917 wantPanic: "input must be an anonymous struct", 918 reason: "input must not be a named struct type", 919 }, { 920 label: "IgnoreInterfaces", 921 fnc: IgnoreInterfaces, 922 args: args(struct{ _ io.Reader }{}), 923 wantPanic: "struct cannot have named fields", 924 reason: "input must not have named fields", 925 }, { 926 label: "IgnoreInterfaces", 927 fnc: IgnoreInterfaces, 928 args: args(struct{ Foo1 }{}), 929 wantPanic: "embedded field must be an interface type", 930 reason: "field types must be interfaces", 931 }, { 932 label: "IgnoreInterfaces", 933 fnc: IgnoreInterfaces, 934 args: args(struct{ EmptyInterface }{}), 935 wantPanic: "cannot ignore empty interface", 936 reason: "field types must not be the empty interface", 937 }, { 938 label: "IgnoreInterfaces", 939 fnc: IgnoreInterfaces, 940 args: args(struct { 941 io.Reader 942 io.Writer 943 io.Closer 944 io.ReadWriteCloser 945 }{}), 946 reason: "multiple interfaces may be specified, even if they overlap", 947 }, { 948 label: "IgnoreUnexported", 949 fnc: IgnoreUnexported, 950 reason: "empty input is valid", 951 }, { 952 label: "IgnoreUnexported", 953 fnc: IgnoreUnexported, 954 args: args(nil), 955 wantPanic: "invalid struct type", 956 reason: "input must not be nil value", 957 }, { 958 label: "IgnoreUnexported", 959 fnc: IgnoreUnexported, 960 args: args(&Foo1{}), 961 wantPanic: "invalid struct type", 962 reason: "input must be a struct type (not a pointer to a struct)", 963 }, { 964 label: "IgnoreUnexported", 965 fnc: IgnoreUnexported, 966 args: args(Foo1{}, struct{ x, X int }{}), 967 reason: "input may be named or unnamed structs", 968 }, { 969 label: "AcyclicTransformer", 970 fnc: AcyclicTransformer, 971 args: args("", "not a func"), 972 wantPanic: "invalid transformer function", 973 reason: "AcyclicTransformer has same input requirements as Transformer", 974 }} 975 976 for _, tt := range tests { 977 t.Run(tt.label, func(t *testing.T) { 978 // Prepare function arguments. 979 vf := reflect.ValueOf(tt.fnc) 980 var vargs []reflect.Value 981 for i, arg := range tt.args { 982 if arg == nil { 983 tf := vf.Type() 984 if i == tf.NumIn()-1 && tf.IsVariadic() { 985 vargs = append(vargs, reflect.Zero(tf.In(i).Elem())) 986 } else { 987 vargs = append(vargs, reflect.Zero(tf.In(i))) 988 } 989 } else { 990 vargs = append(vargs, reflect.ValueOf(arg)) 991 } 992 } 993 994 // Call the function and capture any panics. 995 var gotPanic string 996 func() { 997 defer func() { 998 if ex := recover(); ex != nil { 999 if s, ok := ex.(string); ok { 1000 gotPanic = s 1001 } else { 1002 panic(ex) 1003 } 1004 } 1005 }() 1006 vf.Call(vargs) 1007 }() 1008 1009 switch { 1010 case tt.reason == "": 1011 t.Errorf("reason must be provided") 1012 case tt.wantPanic == "" && gotPanic != "": 1013 t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason) 1014 case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic): 1015 t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason) 1016 } 1017 }) 1018 } 1019 }