diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b49573d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +on: [push, pull_request] +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + - name: Test + run: go test -v -race ./... + - name: Format + if: matrix.go-version == '1.19.x' + run: diff -u <(echo -n) <(gofmt -d .) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 93ed6a8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -sudo: false -language: go -matrix: - include: - - go: 1.8.x - script: - - go test -v -race ./... - - go: 1.9.x - script: - - go test -v -race ./... - - go: 1.10.x - script: - - go test -v -race ./... - - go: 1.11.x - script: - - diff -u <(echo -n) <(gofmt -d .) - - go test -v -race ./... - - go: master - script: - - go test -v -race ./... - allow_failures: - - go: master - fast_finish: true diff --git a/README.md b/README.md index 61c9c4c..e592e3a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Package for equality of Go values -[![GoDoc](https://godoc.org/github.com/google/go-cmp/cmp?status.svg)][godoc] -[![Build Status](https://travis-ci.org/google/go-cmp.svg?branch=master)][travis] +[![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)][godev] +[![Build Status](https://github.com/google/go-cmp/actions/workflows/test.yml/badge.svg?branch=master)][actions] This package is intended to be a more powerful and safer alternative to `reflect.DeepEqual` for comparing whether two values are semantically equal. @@ -24,12 +24,12 @@ The primary features of `cmp` are: by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly compared using the `AllowUnexported` option. -See the [GoDoc documentation][godoc] for more information. +See the [documentation][godev] for more information. This is not an official Google product. -[godoc]: https://godoc.org/github.com/google/go-cmp/cmp -[travis]: https://travis-ci.org/google/go-cmp +[godev]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[actions]: https://github.com/google/go-cmp/actions ## Install diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 41bbddc..e54a76c 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -1,13 +1,15 @@ // 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. +// license that can be found in the LICENSE file. // Package cmpopts provides common options for the cmp package. package cmpopts import ( + "errors" "math" "reflect" + "time" "github.com/google/go-cmp/cmp" ) @@ -40,6 +42,7 @@ func isEmpty(x, y interface{}) bool { // The fraction and margin must be non-negative. // // The mathematical expression used is equivalent to: +// // |x-y| ≤ max(fraction*min(|x|, |y|), margin) // // EquateApprox can be used in conjunction with EquateNaNs. @@ -87,3 +90,67 @@ func areNaNsF64s(x, y float64) bool { func areNaNsF32s(x, y float32) bool { return areNaNsF64s(float64(x), float64(y)) } + +// EquateApproxTime returns a Comparer option that determines two non-zero +// time.Time values to be equal if they are within some margin of one another. +// If both times have a monotonic clock reading, then the monotonic time +// difference will be used. The margin must be non-negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("margin must be a non-negative number") + } + a := timeApproximator{margin} + return cmp.FilterValues(areNonZeroTimes, cmp.Comparer(a.compare)) +} + +func areNonZeroTimes(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() +} + +type timeApproximator struct { + margin time.Duration +} + +func (a timeApproximator) compare(x, y time.Time) bool { + // Avoid subtracting times to avoid overflow when the + // difference is larger than the largest representable duration. + if x.After(y) { + // Ensure x is always before y + x, y = y, x + } + // We're within the margin if x+margin >= y. + // Note: time.Time doesn't have AfterOrEqual method hence the negation. + return !x.Add(a.margin).Before(y) +} + +// AnyError is an error that matches any non-nil error. +var AnyError anyError + +type anyError struct{} + +func (anyError) Error() string { return "any error" } +func (anyError) Is(err error) bool { return err != nil } + +// EquateErrors returns a Comparer option that determines errors to be equal +// if errors.Is reports them to match. The AnyError error can be used to +// match any non-nil error. +func EquateErrors() cmp.Option { + return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors)) +} + +// areConcreteErrors reports whether x and y are types that implement error. +// The input types are deliberately of the interface{} type rather than the +// error type so that we can handle situations where the current type is an +// interface{}, but the underlying concrete types both happen to implement +// the error interface. +func areConcreteErrors(x, y interface{}) bool { + _, ok1 := x.(error) + _, ok2 := y.(error) + return ok1 && ok2 +} + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + return errors.Is(xe, ye) || errors.Is(ye, xe) +} diff --git a/cmp/cmpopts/example_test.go b/cmp/cmpopts/example_test.go new file mode 100644 index 0000000..4b9a8ab --- /dev/null +++ b/cmp/cmpopts/example_test.go @@ -0,0 +1,130 @@ +// Copyright 2020, 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_test + +import ( + "fmt" + "net" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-cmp/cmp/internal/flags" +) + +func init() { + flags.Deterministic = true +} + +// Use IgnoreFields to ignore fields on a struct type when comparing +// by providing a value of the type and the field names to ignore. +// Typically, a zero value of the type is used (e.g., foo.MyStruct{}). +func ExampleIgnoreFields_testing() { + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() + + // While the specified fields will be semantically ignored for the comparison, + // the fields may be printed in the diff when displaying entire values + // that are already determined to be different. + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(Client{}, "IPAddress")); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) + } + + // Output: + // MakeGatewayInfo() mismatch (-want +got): + // cmpopts_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: s"ffff0000", + // Clients: []cmpopts_test.Client{ + // ... // 3 identical elements + // {Hostname: "espresso", ...}, + // {Hostname: "latte", LastSeen: s"2009-11-10 23:00:23 +0000 UTC", ...}, + // + { + // + Hostname: "americano", + // + IPAddress: s"192.168.0.188", + // + LastSeen: s"2009-11-10 23:03:05 +0000 UTC", + // + }, + // }, + // } +} + +type ( + Gateway struct { + SSID string + IPAddress net.IP + NetMask net.IPMask + Clients []Client + } + Client struct { + Hostname string + IPAddress net.IP + LastSeen time.Time + } +) + +func MakeGatewayInfo() (x, y Gateway) { + x = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 1), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 219), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }, { + Hostname: "americano", + IPAddress: net.IPv4(192, 168, 0, 188), + LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC), + }}, + } + y = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 2), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 221), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }}, + } + return x, y +} + +var t fakeT + +type fakeT struct{} + +func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index e86554b..80c6061 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmpopts @@ -11,16 +11,16 @@ import ( "unicode/utf8" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" ) -// IgnoreFields returns an Option that ignores exported fields of the -// given names on a single struct type. +// IgnoreFields returns an Option that ignores fields of the +// given names on a single struct type. It respects the names of exported fields +// that are forwarded due to struct embedding. // The struct type is specified by passing in a value of that type. // // The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a // specific sub-field that is embedded or nested within the parent struct. -// -// This does not handle unexported fields; use IgnoreUnexported instead. func IgnoreFields(typ interface{}, names ...string) cmp.Option { sf := newStructFilter(typ, names...) return cmp.FilterPath(sf.filter, cmp.Ignore()) @@ -112,6 +112,10 @@ func (tf ifaceFilter) filter(p cmp.Path) bool { // In particular, unexported fields within the struct's exported fields // of struct types, including anonymous fields, will not be ignored unless the // type of the field itself is also passed to IgnoreUnexported. +// +// Avoid ignoring unexported fields of a type which you do not control (i.e. a +// type from another repository), as changes to the implementation of such types +// may change how the comparison behaves. Prefer a custom Comparer instead. func IgnoreUnexported(typs ...interface{}) cmp.Option { ux := newUnexportedFilter(typs...) return cmp.FilterPath(ux.filter, cmp.Ignore()) @@ -124,7 +128,7 @@ func newUnexportedFilter(typs ...interface{}) unexportedFilter { for _, typ := range typs { t := reflect.TypeOf(typ) if t == nil || t.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid struct type: %T", typ)) + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) } ux.m[t] = true } @@ -143,3 +147,60 @@ func isExported(id string) bool { r, _ := utf8.DecodeRuneInString(id) return unicode.IsUpper(r) } + +// IgnoreSliceElements returns an Option that ignores elements of []V. +// The discard function must be of the form "func(T) bool" which is used to +// ignore slice elements of type V, where V is assignable to T. +// Elements are ignored if the function reports true. +func IgnoreSliceElements(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + si, ok := p.Index(-1).(cmp.SliceIndex) + if !ok { + return false + } + if !si.Type().AssignableTo(vf.Type().In(0)) { + return false + } + vx, vy := si.Values() + if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} + +// IgnoreMapEntries returns an Option that ignores entries of map[K]V. +// The discard function must be of the form "func(T, R) bool" which is used to +// ignore map entries of type K and V, where K and V are assignable to T and R. +// Entries are ignored if the function reports true. +func IgnoreMapEntries(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + mi, ok := p.Index(-1).(cmp.MapIndex) + if !ok { + return false + } + if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) { + return false + } + k := mi.Key() + vx, vy := mi.Values() + if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} diff --git a/cmp/cmpopts/sort.go b/cmp/cmpopts/sort.go index 8b2ef83..0eb2a75 100644 --- a/cmp/cmpopts/sort.go +++ b/cmp/cmpopts/sort.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmpopts @@ -18,18 +18,18 @@ import ( // sort any slice with element type V that is assignable to T. // // The less function must be: -// • Deterministic: less(x, y) == less(x, y) -// • Irreflexive: !less(x, x) -// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) // // The less function does not have to be "total". That is, if !less(x, y) and // !less(y, x) for two elements x and y, their relative order is maintained. // // SortSlices can be used in conjunction with EquateEmpty. -func SortSlices(less interface{}) cmp.Option { - vf := reflect.ValueOf(less) +func SortSlices(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { - panic(fmt.Sprintf("invalid less function: %T", less)) + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) } ss := sliceSorter{vf.Type().In(0), vf} return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort)) @@ -91,16 +91,16 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool { // use Comparers on K or the K.Equal method if it exists. // // The less function must be: -// • Deterministic: less(x, y) == less(x, y) -// • Irreflexive: !less(x, x) -// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) -// • Total: if x != y, then either less(x, y) or less(y, x) +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Total: if x != y, then either less(x, y) or less(y, x) // // SortMaps can be used in conjunction with EquateEmpty. -func SortMaps(less interface{}) cmp.Option { - vf := reflect.ValueOf(less) +func SortMaps(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { - panic(fmt.Sprintf("invalid less function: %T", less)) + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) } ms := mapSorter{vf.Type().In(0), vf} return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index 97f7079..ca11a40 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmpopts @@ -42,7 +42,7 @@ func newStructFilter(typ interface{}, names ...string) structFilter { t := reflect.TypeOf(typ) if t == nil || t.Kind() != reflect.Struct { - panic(fmt.Sprintf("%T must be a struct", typ)) + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) } var ft fieldTree for _, name := range names { @@ -67,12 +67,14 @@ func (sf structFilter) filter(p cmp.Path) bool { // fieldTree represents a set of dot-separated identifiers. // // For example, inserting the following selectors: +// // Foo // Foo.Bar.Baz // Foo.Buzz // Nuka.Cola.Quantum // // Results in a tree of the form: +// // {sub: { // "Foo": {ok: true, sub: { // "Bar": {sub: { @@ -160,14 +162,19 @@ func canonicalName(t reflect.Type, sel string) ([]string, error) { // Find the canonical name for this current field name. // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) if !isExported(name) { - // Disallow unexported fields: - // * To discourage people from actually touching unexported fields - // * FieldByName is buggy (https://golang.org/issue/4876) - return []string{name}, fmt.Errorf("name must be exported") + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } } - sf, ok := t.FieldByName(name) - if !ok { + if sf.Name == "" { return []string{name}, fmt.Errorf("does not exist") } var ss []string diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 53b9248..7adeb9b 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -1,11 +1,12 @@ // 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. +// license that can be found in the LICENSE file. package cmpopts import ( "bytes" + "errors" "fmt" "io" "math" @@ -20,7 +21,9 @@ import ( type ( MyInt int + MyInts []int MyFloat float32 + MyString string MyTime struct{ time.Time } MyStruct struct { A, B []int @@ -448,6 +451,231 @@ func TestOptions(t *testing.T) { }, 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: "IgnoreFields", x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, @@ -542,6 +770,39 @@ func TestOptions(t *testing.T) { 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"}, @@ -716,6 +977,80 @@ func TestOptions(t *testing.T) { }, 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", @@ -730,16 +1065,16 @@ func TestOptions(t *testing.T) { x: []string{"foo", "Bar", "BAZ"}, y: []string{"Foo", "BAR", "baz"}, opts: []cmp.Option{ - AcyclicTransformer("", func(s string) string { return strings.ToUpper(s) }), + 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", + y: "this is a sentence", opts: []cmp.Option{ - AcyclicTransformer("", func(s string) []string { return strings.Fields(s) }), + AcyclicTransformer("", strings.Fields), }, wantEqual: true, reason: "equal because acyclic transformer splits on any contiguous whitespace", @@ -812,6 +1147,12 @@ func TestPanic(t *testing.T) { 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, @@ -881,14 +1222,24 @@ func TestPanic(t *testing.T) { label: "IgnoreFields", fnc: IgnoreFields, args: args(&Foo1{}, "Alpha"), - wantPanic: "must be a struct", + 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(Foo1{}, "unexported"), - wantPanic: "name must be exported", - reason: "unexported fields must not be specified", + 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, @@ -952,13 +1303,13 @@ func TestPanic(t *testing.T) { label: "IgnoreUnexported", fnc: IgnoreUnexported, args: args(nil), - wantPanic: "invalid struct type", + wantPanic: "must be a non-pointer struct", reason: "input must not be nil value", }, { label: "IgnoreUnexported", fnc: IgnoreUnexported, args: args(&Foo1{}), - wantPanic: "invalid struct type", + wantPanic: "must be a non-pointer struct", reason: "input must be a struct type (not a pointer to a struct)", }, { label: "IgnoreUnexported", diff --git a/cmp/cmpopts/xform.go b/cmp/cmpopts/xform.go index be278f1..8812443 100644 --- a/cmp/cmpopts/xform.go +++ b/cmp/cmpopts/xform.go @@ -1,6 +1,6 @@ // Copyright 2018, 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. +// license that can be found in the LICENSE file. package cmpopts @@ -23,13 +23,14 @@ func (xf xformFilter) filter(p cmp.Path) bool { // that the transformer cannot be recursively applied upon its own output. // // An example use case is a transformer that splits a string by lines: +// // AcyclicTransformer("SplitLines", func(s string) []string{ // return strings.Split(s, "\n") // }) // // Had this been an unfiltered Transformer instead, this would result in an // infinite cycle converting a string to []string to [][]string and so on. -func AcyclicTransformer(name string, f interface{}) cmp.Option { - xf := xformFilter{cmp.Transformer(name, f)} +func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option { + xf := xformFilter{cmp.Transformer(name, xformFunc)} return cmp.FilterPath(xf.filter, xf.xform) } diff --git a/cmp/compare.go b/cmp/compare.go index a7bcaab..087320d 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -1,29 +1,33 @@ // 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. +// license that can be found in the LICENSE file. // Package cmp determines equality of values. // // This package is intended to be a more powerful and safer alternative to // reflect.DeepEqual for comparing whether two values are semantically equal. +// It is intended to only be used in tests, as performance is not a goal and +// it may panic if it cannot compare the values. Its propensity towards +// panicking means that its unsuitable for production environments where a +// spurious panic may be fatal. // // The primary features of cmp are: // -// • When the default behavior of equality does not suit the needs of the test, -// custom equality functions can override the equality operation. -// For example, an equality function may report floats as equal so long as they -// are within some tolerance of each other. +// - When the default behavior of equality does not suit the test's needs, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as +// they are within some tolerance of each other. // -// • Types that have an Equal method may use that method to determine equality. -// This allows package authors to determine the equality operation for the types -// that they define. +// - Types with an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation +// for the types that they define. // -// • If no custom equality functions are used and no Equal method is defined, -// equality is determined by recursively comparing the primitive kinds on both -// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported -// fields are not compared by default; they result in panics unless suppressed -// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared -// using the AllowUnexported option. +// - If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on +// both values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, +// unexported fields are not compared by default; they result in panics +// unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported) +// or explicitly compared using the Exporter option. package cmp import ( @@ -36,76 +40,135 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) -var nothing = reflect.Value{} +// TODO(≥go1.18): Use any instead of interface{}. // Equal reports whether x and y are equal by recursively applying the // following rules in the given order to x and y and all of their sub-values: // -// • If two values are not of the same type, then they are never equal -// and the overall result is false. +// - Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is non-zero, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform +// the current values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. // -// • Let S be the set of all Ignore, Transformer, and Comparer options that -// remain after applying all path filters, value filters, and type filters. -// If at least one Ignore exists in S, then the comparison is ignored. -// If the number of Transformer and Comparer options in S is greater than one, -// then Equal panics because it is ambiguous which option to use. -// If S contains a single Transformer, then use that to transform the current -// values and recursively call Equal on the output values. -// If S contains a single Comparer, then use that to compare the current values. -// Otherwise, evaluation proceeds to the next rule. +// - If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and +// evaluation proceeds to the next rule. // -// • If the values have an Equal method of the form "(T) Equal(T) bool" or -// "(T) Equal(I) bool" where T is assignable to I, then use the result of -// x.Equal(y) even if x or y is nil. -// Otherwise, no such method exists and evaluation proceeds to the next rule. +// - Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, +// and channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. // -// • Lastly, try to compare x and y based on their basic kinds. -// Simple kinds like booleans, integers, floats, complex numbers, strings, and -// channels are compared using the equivalent of the == operator in Go. -// Functions are only equal if they are both nil, otherwise they are unequal. -// Pointers are equal if the underlying values they point to are also equal. -// Interfaces are equal if their underlying concrete values are also equal. +// Structs are equal if recursively calling Equal on all fields report equal. +// If a struct contains unexported fields, Equal panics unless an Ignore option +// (e.g., cmpopts.IgnoreUnexported) ignores that field or the Exporter option +// explicitly permits comparing the unexported field. // -// Structs are equal if all of their fields are equal. If a struct contains -// unexported fields, Equal panics unless the AllowUnexported option is used or -// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. +// Slices are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored slice or array elements report equal. +// Empty non-nil slices and nil slices are not equal; to equate empty slices, +// consider using cmpopts.EquateEmpty. // -// Arrays, slices, and maps are equal if they are both nil or both non-nil -// with the same length and the elements at each index or key are equal. -// Note that a non-nil empty slice and a nil slice are not equal. -// To equate empty slices and maps, consider using cmpopts.EquateEmpty. +// Maps are equal if they are both nil or both non-nil, where recursively +// calling Equal on all non-ignored map entries report equal. // Map keys are equal according to the == operator. // To use custom comparisons for map keys, consider using cmpopts.SortMaps. +// Empty non-nil maps and nil maps are not equal; to equate empty maps, +// consider using cmpopts.EquateEmpty. +// +// Pointers and interfaces are equal if they are both nil or both non-nil, +// where they have the same underlying concrete type and recursively +// calling Equal on the underlying values reports equal. +// +// Before recursing into a pointer, slice element, or map, the current path +// is checked to detect whether the address has already been visited. +// If there is a cycle, then the pointed at values are considered equal +// only if both addresses were previously visited in the same path step. func Equal(x, y interface{}, opts ...Option) bool { s := newState(opts) - s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) + s.compareAny(rootStep(x, y)) return s.result.Equal() } -// Diff returns a human-readable report of the differences between two values. -// It returns an empty string if and only if Equal returns true for the same -// input values and options. The output string will use the "-" symbol to -// indicate elements removed from x, and the "+" symbol to indicate elements -// added to y. +// Diff returns a human-readable report of the differences between two values: +// y - x. It returns an empty string if and only if Equal returns true for the +// same input values and options. // -// Do not depend on this output being stable. +// The output is displayed as a literal in pseudo-Go syntax. +// At the start of each line, a "-" prefix indicates an element removed from x, +// a "+" prefix to indicates an element added from y, and the lack of a prefix +// indicates an element common to both x and y. If possible, the output +// uses fmt.Stringer.String or error.Error methods to produce more humanly +// readable outputs. In such cases, the string is prefixed with either an +// 's' or 'e' character, respectively, to indicate that the method was called. +// +// Do not depend on this output being stable. If you need the ability to +// programmatically interpret the difference, consider using a custom Reporter. func Diff(x, y interface{}, opts ...Option) string { + s := newState(opts) + + // Optimization: If there are no other reporters, we can optimize for the + // common case where the result is equal (and thus no reported difference). + // This avoids the expensive construction of a difference tree. + if len(s.reporters) == 0 { + s.compareAny(rootStep(x, y)) + if s.result.Equal() { + return "" + } + s.result = diff.Result{} // Reset results + } + r := new(defaultReporter) - opts = Options{Options(opts), r} - eq := Equal(x, y, opts...) + s.reporters = append(s.reporters, reporter{r}) + s.compareAny(rootStep(x, y)) d := r.String() - if (d == "") != eq { + if (d == "") != s.result.Equal() { panic("inconsistent difference and equality results") } return d } +// rootStep constructs the first path step. If x and y have differing types, +// then they are stored within an empty interface type. +func rootStep(x, y interface{}) PathStep { + vx := reflect.ValueOf(x) + vy := reflect.ValueOf(y) + + // If the inputs are different types, auto-wrap them in an empty interface + // so that they have the same parent type. + var t reflect.Type + if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() { + t = anyType + if vx.IsValid() { + vvx := reflect.New(t).Elem() + vvx.Set(vx) + vx = vvx + } + if vy.IsValid() { + vvy := reflect.New(t).Elem() + vvy.Set(vy) + vy = vvy + } + } else { + t = vx.Type() + } + + return &pathStep{t, vx, vy} +} + type state struct { // These fields represent the "comparison state". // Calling statelessCompare must not result in observable changes to these. - result diff.Result // The current result of comparison - curPath Path // The current path in the value tree - reporter reporter // Optional reporter used for difference formatting + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + curPtrs pointerPath // The current set of visited pointers + reporters []reporter // Optional reporters // recChecker checks for infinite cycles applying the same set of // transformers upon the output of itself. @@ -116,15 +179,15 @@ type state struct { dynChecker dynChecker // These fields, once set by processOption, will not change. - exporters map[reflect.Type]bool // Set of structs with unexported field visibility - opts Options // List of all fundamental and filter options + exporters []exporter // List of exporters for structs with unexported fields + opts Options // List of all fundamental and filter options } func newState(opts []Option) *state { - s := new(state) - for _, opt := range opts { - s.processOption(opt) - } + // Always ensure a validator option exists to validate the inputs. + s := &state{opts: Options{validator{}}} + s.curPtrs.Init() + s.processOption(Options(opts)) return s } @@ -143,18 +206,10 @@ func (s *state) processOption(opt Option) { panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) } s.opts = append(s.opts, opt) - case visibleStructs: - if s.exporters == nil { - s.exporters = make(map[reflect.Type]bool) - } - for t := range opt { - s.exporters[t] = true - } + case exporter: + s.exporters = append(s.exporters, opt) case reporter: - if s.reporter != nil { - panic("difference reporter already registered") - } - s.reporter = opt + s.reporters = append(s.reporters, opt) default: panic(fmt.Sprintf("unknown option %T", opt)) } @@ -163,154 +218,96 @@ func (s *state) processOption(opt Option) { // statelessCompare compares two values and returns the result. // This function is stateless in that it does not alter the current result, // or output to any registered reporters. -func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { - // We do not save and restore the curPath because all of the compareX - // methods should properly push and pop from the path. - // It is an implementation bug if the contents of curPath differs from +func (s *state) statelessCompare(step PathStep) diff.Result { + // We do not save and restore curPath and curPtrs because all of the + // compareX methods should properly push and pop from them. + // It is an implementation bug if the contents of the paths differ from // when calling this function to when returning from it. - oldResult, oldReporter := s.result, s.reporter + oldResult, oldReporters := s.result, s.reporters s.result = diff.Result{} // Reset result - s.reporter = nil // Remove reporter to avoid spurious printouts - s.compareAny(vx, vy) + s.reporters = nil // Remove reporters to avoid spurious printouts + s.compareAny(step) res := s.result - s.result, s.reporter = oldResult, oldReporter + s.result, s.reporters = oldResult, oldReporters return res } -func (s *state) compareAny(vx, vy reflect.Value) { - // TODO: Support cyclic data structures. +func (s *state) compareAny(step PathStep) { + // Update the path stack. + s.curPath.push(step) + defer s.curPath.pop() + for _, r := range s.reporters { + r.PushStep(step) + defer r.PopStep() + } s.recChecker.Check(s.curPath) - // Rule 0: Differing types are never equal. - if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid(), vx, vy) - return - } - if vx.Type() != vy.Type() { - s.report(false, vx, vy) // Possible for path to be empty - return - } - t := vx.Type() - if len(s.curPath) == 0 { - s.curPath.push(&pathStep{typ: t}) - defer s.curPath.pop() + // Cycle-detection for slice elements (see NOTE in compareSlice). + t := step.Type() + vx, vy := step.Values() + if si, ok := step.(SliceIndex); ok && si.isSlice && vx.IsValid() && vy.IsValid() { + px, py := vx.Addr(), vy.Addr() + if eq, visited := s.curPtrs.Push(px, py); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(px, py) } - vx, vy = s.tryExporting(vx, vy) // Rule 1: Check whether an option applies on this node in the value tree. - if s.tryOptions(vx, vy, t) { + if s.tryOptions(t, vx, vy) { return } // Rule 2: Check whether the type has a valid Equal method. - if s.tryMethod(vx, vy, t) { + if s.tryMethod(t, vx, vy) { return } - // Rule 3: Recursively descend into each value's underlying kind. + // Rule 3: Compare based on the underlying kind. switch t.Kind() { case reflect.Bool: - s.report(vx.Bool() == vy.Bool(), vx, vy) - return + s.report(vx.Bool() == vy.Bool(), 0) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s.report(vx.Int() == vy.Int(), vx, vy) - return + s.report(vx.Int() == vy.Int(), 0) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s.report(vx.Uint() == vy.Uint(), vx, vy) - return + s.report(vx.Uint() == vy.Uint(), 0) case reflect.Float32, reflect.Float64: - s.report(vx.Float() == vy.Float(), vx, vy) - return + s.report(vx.Float() == vy.Float(), 0) case reflect.Complex64, reflect.Complex128: - s.report(vx.Complex() == vy.Complex(), vx, vy) - return + s.report(vx.Complex() == vy.Complex(), 0) case reflect.String: - s.report(vx.String() == vy.String(), vx, vy) - return + s.report(vx.String() == vy.String(), 0) case reflect.Chan, reflect.UnsafePointer: - s.report(vx.Pointer() == vy.Pointer(), vx, vy) - return + s.report(vx.Pointer() == vy.Pointer(), 0) case reflect.Func: - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return + s.report(vx.IsNil() && vy.IsNil(), 0) case reflect.Struct: - s.compareStruct(vx, vy, t) - return - case reflect.Slice: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - fallthrough - case reflect.Array: - s.compareSlice(vx, vy, t) - return + s.compareStruct(t, vx, vy) + case reflect.Slice, reflect.Array: + s.compareSlice(t, vx, vy) case reflect.Map: - s.compareMap(vx, vy, t) - return + s.compareMap(t, vx, vy) case reflect.Ptr: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - s.curPath.push(&indirect{pathStep{t.Elem()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) - return + s.comparePtr(t, vx, vy) case reflect.Interface: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) - return - } - if vx.Elem().Type() != vy.Elem().Type() { - s.report(false, vx.Elem(), vy.Elem()) - return - } - s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) - return + s.compareInterface(t, vx, vy) default: panic(fmt.Sprintf("%v kind not handled", t.Kind())) } } -func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { - if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { - if sf.force { - // Use unsafe pointer arithmetic to get read-write access to an - // unexported field in the struct. - vx = unsafeRetrieveField(sf.pvx, sf.field) - vy = unsafeRetrieveField(sf.pvy, sf.field) - } else { - // We are not allowed to export the value, so invalidate them - // so that tryOptions can panic later if not explicitly ignored. - vx = nothing - vy = nothing - } - } - return vx, vy -} - -func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { - // If there were no FilterValues, we will not detect invalid inputs, - // so manually check for them and append invalid if necessary. - // We still evaluate the options since an ignore can override invalid. - opts := s.opts - if !vx.IsValid() || !vy.IsValid() { - opts = Options{opts, invalid{}} - } - +func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { // Evaluate all filters and apply the remaining options. - if opt := opts.filter(s, vx, vy, t); opt != nil { + if opt := s.opts.filter(s, t, vx, vy); opt != nil { opt.apply(s, vx, vy) return true } return false } -func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { +func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { // Check if this type even has an Equal method. m, ok := t.MethodByName("Equal") if !ok || !function.IsType(m.Type, function.EqualAssignable) { @@ -318,12 +315,11 @@ func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { } eq := s.callTTBFunc(m.Func, vx, vy) - s.report(eq, vx, vy) + s.report(eq, reportByMethod) return true } -func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { - v = sanitizeValue(v, f.Type().In(0)) +func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { if !s.dynChecker.Next() { return f.Call([]reflect.Value{v})[0] } @@ -333,11 +329,12 @@ func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { // unsafe mutations to the input. c := make(chan reflect.Value) go detectRaces(c, f, v) + got := <-c want := f.Call([]reflect.Value{v})[0] - if got := <-c; !s.statelessCompare(got, want).Equal() { + if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() { // To avoid false-positives with non-reflexive equality operations, // we sanity check whether a value is equal to itself. - if !s.statelessCompare(want, want).Equal() { + if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { return want } panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) @@ -346,8 +343,6 @@ func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { } func (s *state) callTTBFunc(f, x, y reflect.Value) bool { - x = sanitizeValue(x, f.Type().In(0)) - y = sanitizeValue(y, f.Type().In(1)) if !s.dynChecker.Next() { return f.Call([]reflect.Value{x, y})[0].Bool() } @@ -358,8 +353,9 @@ func (s *state) callTTBFunc(f, x, y reflect.Value) bool { // unsafe mutations to the input. c := make(chan reflect.Value) go detectRaces(c, f, y, x) + got := <-c want := f.Call([]reflect.Value{x, y})[0].Bool() - if got := <-c; !got.IsValid() || got.Bool() != want { + if !got.IsValid() || got.Bool() != want { panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) } return want @@ -374,29 +370,16 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { ret = f.Call(vs)[0] } -// sanitizeValue converts nil interfaces of type T to those of type R, -// assuming that T is assignable to R. -// Otherwise, it returns the input value as is. -func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { - // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143). - // The upstream fix landed in Go1.10, so we can remove this when drop support - // for Go1.9 and below. - if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { - return reflect.New(t).Elem() - } - return v -} - -func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { +func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { + var addr bool var vax, vay reflect.Value // Addressable versions of vx and vy - step := &structField{} - s.curPath.push(step) - defer s.curPath.pop() + var mayForce, mayForceInit bool + step := StructField{&structField{}} for i := 0; i < t.NumField(); i++ { - vvx := vx.Field(i) - vvy := vy.Field(i) step.typ = t.Field(i).Type + step.vx = vx.Field(i) + step.vy = vy.Field(i) step.name = t.Field(i).Name step.idx = i step.unexported = !isExported(step.name) @@ -407,97 +390,143 @@ func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { // Defer checking of unexported fields until later to give an // Ignore a chance to ignore the field. if !vax.IsValid() || !vay.IsValid() { - // For unsafeRetrieveField to work, the parent struct must + // For retrieveUnexportedField to work, the parent struct must // be addressable. Create a new copy of the values if // necessary to make them addressable. + addr = vx.CanAddr() || vy.CanAddr() vax = makeAddressable(vx) vay = makeAddressable(vy) } - step.force = s.exporters[t] + if !mayForceInit { + for _, xf := range s.exporters { + mayForce = mayForce || xf(t) + } + mayForceInit = true + } + step.mayForce = mayForce + step.paddr = addr step.pvx = vax step.pvy = vay step.field = t.Field(i) } - s.compareAny(vvx, vvy) + s.compareAny(step) } } -func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { - step := &sliceIndex{pathStep{t.Elem()}, 0, 0} - s.curPath.push(step) - - // Compute an edit-script for slices vx and vy. - es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { - step.xkey, step.ykey = ix, iy - return s.statelessCompare(vx.Index(ix), vy.Index(iy)) - }) - - // Report the entire slice as is if the arrays are of primitive kind, - // and the arrays are different enough. - isPrimitive := false - switch t.Elem().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - isPrimitive = true - } - if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { - s.curPath.pop() // Pop first since we are reporting the whole slice - s.report(false, vx, vy) +func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { + isSlice := t.Kind() == reflect.Slice + if isSlice && (vx.IsNil() || vy.IsNil()) { + s.report(vx.IsNil() && vy.IsNil(), 0) return } - // Replay the edit-script. + // NOTE: It is incorrect to call curPtrs.Push on the slice header pointer + // since slices represents a list of pointers, rather than a single pointer. + // The pointer checking logic must be handled on a per-element basis + // in compareAny. + // + // A slice header (see reflect.SliceHeader) in Go is a tuple of a starting + // pointer P, a length N, and a capacity C. Supposing each slice element has + // a memory size of M, then the slice is equivalent to the list of pointers: + // [P+i*M for i in range(N)] + // + // For example, v[:0] and v[:1] are slices with the same starting pointer, + // but they are clearly different values. Using the slice pointer alone + // violates the assumption that equal pointers implies equal values. + + step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}, isSlice: isSlice}} + withIndexes := func(ix, iy int) SliceIndex { + if ix >= 0 { + step.vx, step.xkey = vx.Index(ix), ix + } else { + step.vx, step.xkey = reflect.Value{}, -1 + } + if iy >= 0 { + step.vy, step.ykey = vy.Index(iy), iy + } else { + step.vy, step.ykey = reflect.Value{}, -1 + } + return step + } + + // Ignore options are able to ignore missing elements in a slice. + // However, detecting these reliably requires an optimal differencing + // algorithm, for which diff.Difference is not. + // + // Instead, we first iterate through both slices to detect which elements + // would be ignored if standing alone. The index of non-discarded elements + // are stored in a separate slice, which diffing is then performed on. + var indexesX, indexesY []int + var ignoredX, ignoredY []bool + for ix := 0; ix < vx.Len(); ix++ { + ignored := s.statelessCompare(withIndexes(ix, -1)).NumDiff == 0 + if !ignored { + indexesX = append(indexesX, ix) + } + ignoredX = append(ignoredX, ignored) + } + for iy := 0; iy < vy.Len(); iy++ { + ignored := s.statelessCompare(withIndexes(-1, iy)).NumDiff == 0 + if !ignored { + indexesY = append(indexesY, iy) + } + ignoredY = append(ignoredY, ignored) + } + + // Compute an edit-script for slices vx and vy (excluding ignored elements). + edits := diff.Difference(len(indexesX), len(indexesY), func(ix, iy int) diff.Result { + return s.statelessCompare(withIndexes(indexesX[ix], indexesY[iy])) + }) + + // Replay the ignore-scripts and the edit-script. var ix, iy int - for _, e := range es { + for ix < vx.Len() || iy < vy.Len() { + var e diff.EditType + switch { + case ix < len(ignoredX) && ignoredX[ix]: + e = diff.UniqueX + case iy < len(ignoredY) && ignoredY[iy]: + e = diff.UniqueY + default: + e, edits = edits[0], edits[1:] + } switch e { case diff.UniqueX: - step.xkey, step.ykey = ix, -1 - s.report(false, vx.Index(ix), nothing) + s.compareAny(withIndexes(ix, -1)) ix++ case diff.UniqueY: - step.xkey, step.ykey = -1, iy - s.report(false, nothing, vy.Index(iy)) + s.compareAny(withIndexes(-1, iy)) iy++ default: - step.xkey, step.ykey = ix, iy - if e == diff.Identity { - s.report(true, vx.Index(ix), vy.Index(iy)) - } else { - s.compareAny(vx.Index(ix), vy.Index(iy)) - } + s.compareAny(withIndexes(ix, iy)) ix++ iy++ } } - s.curPath.pop() - return } -func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { +func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil(), 0) return } + // Cycle-detection for maps. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + // We combine and sort the two map keys so that we can perform the // comparisons in a deterministic order. - step := &mapIndex{pathStep: pathStep{t.Elem()}} - s.curPath.push(step) - defer s.curPath.pop() + step := MapIndex{&mapIndex{pathStep: pathStep{typ: t.Elem()}}} for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.vx = vx.MapIndex(k) + step.vy = vy.MapIndex(k) step.key = k - vvx := vx.MapIndex(k) - vvy := vy.MapIndex(k) - switch { - case vvx.IsValid() && vvy.IsValid(): - s.compareAny(vvx, vvy) - case vvx.IsValid() && !vvy.IsValid(): - s.report(false, vvx, nothing) - case !vvx.IsValid() && vvy.IsValid(): - s.report(false, nothing, vvy) - default: - // It is possible for both vvx and vvy to be invalid if the + if !step.vx.IsValid() && !step.vy.IsValid() { + // It is possible for both vx and vy to be invalid if the // key contained a NaN value in it. // // Even with the ability to retrieve NaN keys in Go 1.12, @@ -514,19 +543,52 @@ func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { const help = "consider providing a Comparer to compare the map" panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) } + s.compareAny(step) } } -// report records the result of a single comparison. -// It also calls Report if any reporter is registered. -func (s *state) report(eq bool, vx, vy reflect.Value) { - if eq { - s.result.NSame++ - } else { - s.result.NDiff++ +func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return } - if s.reporter != nil { - s.reporter.Report(vx, vy, eq, s.curPath) + + // Cycle-detection for pointers. + if eq, visited := s.curPtrs.Push(vx, vy); visited { + s.report(eq, reportByCycle) + return + } + defer s.curPtrs.Pop(vx, vy) + + vx, vy = vx.Elem(), vy.Elem() + s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) +} + +func (s *state) compareInterface(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + vx, vy = vx.Elem(), vy.Elem() + if vx.Type() != vy.Type() { + s.report(false, 0) + return + } + s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}}) +} + +func (s *state) report(eq bool, rf resultFlags) { + if rf&reportByIgnore == 0 { + if eq { + s.result.NumSame++ + rf |= reportEqual + } else { + s.result.NumDiff++ + rf |= reportUnequal + } + } + for _, r := range s.reporters { + r.Report(Result{flags: rf}) } } @@ -577,7 +639,9 @@ type dynChecker struct{ curr, next int } // Next increments the state and reports whether a check should be performed. // // Checks occur every Nth function call, where N is a triangular number: +// // 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// // See https://en.wikipedia.org/wiki/Triangular_number // // This sequence ensures that the cost of checks drops significantly as diff --git a/cmp/compare_test.go b/cmp/compare_test.go index c98b088..88b7d45 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1,20 +1,24 @@ // 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. +// license that can be found in the LICENSE file. package cmp_test import ( "bytes" - "crypto/md5" + "crypto/sha256" "encoding/json" + "errors" + "flag" "fmt" "io" + "io/ioutil" "math" "math/rand" "reflect" "regexp" "sort" + "strconv" "strings" "sync" "testing" @@ -22,37 +26,123 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-cmp/cmp/internal/flags" + pb "github.com/google/go-cmp/cmp/internal/testprotos" ts "github.com/google/go-cmp/cmp/internal/teststructs" + foo1 "github.com/google/go-cmp/cmp/internal/teststructs/foo1" + foo2 "github.com/google/go-cmp/cmp/internal/teststructs/foo2" ) -var now = time.Now() +func init() { + flags.Deterministic = true +} + +var update = flag.Bool("update", false, "update golden test files") -func intPtr(n int) *int { return &n } +const goldenHeaderPrefix = "<<< " +const goldenFooterPrefix = ">>> " + +// mustParseGolden parses a file as a set of key-value pairs. +// +// The syntax is simple and looks something like: +// +// <<< Key1 +// value1a +// value1b +// >>> Key1 +// <<< Key2 +// value2 +// >>> Key2 +// +// It is the user's responsibility to choose a sufficiently unique key name +// such that it never appears in the body of the value itself. +func mustParseGolden(path string) map[string]string { + b, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + s := string(b) + + out := map[string]string{} + for len(s) > 0 { + // Identify the next header. + i := strings.Index(s, "\n") + len("\n") + header := s[:i] + if !strings.HasPrefix(header, goldenHeaderPrefix) { + panic(fmt.Sprintf("invalid header: %q", header)) + } + + // Locate the next footer. + footer := goldenFooterPrefix + header[len(goldenHeaderPrefix):] + j := strings.Index(s, footer) + if j < 0 { + panic(fmt.Sprintf("missing footer: %q", footer)) + } + + // Store the name and data. + name := header[len(goldenHeaderPrefix) : len(header)-len("\n")] + if _, ok := out[name]; ok { + panic(fmt.Sprintf("duplicate name: %q", name)) + } + out[name] = s[len(header):j] + s = s[j+len(footer):] + } + return out +} +func mustFormatGolden(path string, in []struct{ Name, Data string }) { + var b []byte + for _, v := range in { + b = append(b, goldenHeaderPrefix+v.Name+"\n"...) + b = append(b, v.Data...) + b = append(b, goldenFooterPrefix+v.Name+"\n"...) + } + if err := ioutil.WriteFile(path, b, 0664); err != nil { + panic(err) + } +} + +var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC) + +// TODO(≥go1.18): Define a generic function that boxes a value on the heap. +func newInt(n int) *int { return &n } + +type Stringer string + +func newStringer(s string) fmt.Stringer { return (*Stringer)(&s) } +func (s Stringer) String() string { return string(s) } type test struct { - label string // Test description + label string // Test name x, y interface{} // Input values to compare opts []cmp.Option // Input options - wantDiff string // The exact difference string + wantEqual bool // Whether any difference is expected wantPanic string // Sub-string of an expected panic message + reason string // The reason for the expected outcome } func TestDiff(t *testing.T) { var tests []test tests = append(tests, comparerTests()...) tests = append(tests, transformerTests()...) + tests = append(tests, reporterTests()...) tests = append(tests, embeddedTests()...) tests = append(tests, methodTests()...) + tests = append(tests, cycleTests()...) tests = append(tests, project1Tests()...) tests = append(tests, project2Tests()...) tests = append(tests, project3Tests()...) tests = append(tests, project4Tests()...) + const goldenFile = "testdata/diffs" + gotDiffs := []struct{ Name, Data string }{} + wantDiffs := mustParseGolden(goldenFile) for _, tt := range tests { tt := tt t.Run(tt.label, func(t *testing.T) { - t.Parallel() + if !*update { + t.Parallel() + } var gotDiff, gotPanic string func() { defer func() { @@ -66,20 +156,41 @@ func TestDiff(t *testing.T) { }() gotDiff = cmp.Diff(tt.x, tt.y, tt.opts...) }() - if tt.wantPanic == "" { + + switch { + case strings.Contains(t.Name(), "#"): + panic("unique test name must be provided") + case tt.reason == "": + panic("reason must be provided") + case tt.wantPanic == "": if gotPanic != "" { - t.Fatalf("unexpected panic message: %s", gotPanic) + t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) + } + if *update { + if gotDiff != "" { + gotDiffs = append(gotDiffs, struct{ Name, Data string }{t.Name(), gotDiff}) + } + } else { + wantDiff := wantDiffs[t.Name()] + if diff := cmp.Diff(wantDiff, gotDiff); diff != "" { + t.Fatalf("Diff:\ngot:\n%s\nwant:\n%s\ndiff: (-want +got)\n%s\nreason: %v", gotDiff, wantDiff, diff, tt.reason) + } } - if got, want := strings.TrimSpace(gotDiff), strings.TrimSpace(tt.wantDiff); got != want { - t.Fatalf("difference message:\ngot:\n%s\n\nwant:\n%s", got, want) + gotEqual := gotDiff == "" + if gotEqual != tt.wantEqual { + t.Fatalf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) } - } else { + default: if !strings.Contains(gotPanic, tt.wantPanic) { - t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + t.Fatalf("panic message:\ngot: %s\nwant: %s\nreason: %v", gotPanic, tt.wantPanic, tt.reason) } } }) } + + if *update { + mustFormatGolden(goldenFile, gotDiffs) + } } func comparerTests() []test { @@ -110,6 +221,10 @@ func comparerTests() []test { Xattrs map[string]string } + type namedWithUnexported struct { + unexported string + } + makeTarHeaders := func(tf byte) (hs []tarHeader) { for i := 0; i < 5; i++ { hs = append(hs, tarHeader{ @@ -124,29 +239,40 @@ func comparerTests() []test { } return []test{{ - label: label, - x: 1, - y: 1, + label: label + "/Nil", + x: nil, + y: nil, + wantEqual: true, + reason: "nils are equal", }, { - label: label, + label: label + "/Integer", + x: 1, + y: 1, + wantEqual: true, + reason: "identical integers are equal", + }, { + label: label + "/UnfilteredIgnore", x: 1, y: 1, opts: []cmp.Option{cmp.Ignore()}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/UnfilteredCompare", x: 1, y: 1, opts: []cmp.Option{cmp.Comparer(func(_, _ interface{}) bool { return true })}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/UnfilteredTransform", x: 1, y: 1, opts: []cmp.Option{cmp.Transformer("λ", func(x interface{}) interface{} { return x })}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/AmbiguousOptions", x: 1, y: 1, opts: []cmp.Option{ @@ -154,8 +280,9 @@ func comparerTests() []test { cmp.Transformer("λ", func(x int) float64 { return float64(x) }), }, wantPanic: "ambiguous set of applicable options", + reason: "both options apply on int, leading to ambiguity", }, { - label: label, + label: label + "/IgnorePrecedence", x: 1, y: 1, opts: []cmp.Option{ @@ -165,91 +292,115 @@ func comparerTests() []test { cmp.Comparer(func(x, y int) bool { return true }), cmp.Transformer("λ", func(x int) float64 { return float64(x) }), }, + wantEqual: true, + reason: "ignore takes precedence over other options", }, { - label: label, + label: label + "/UnknownOption", opts: []cmp.Option{struct{ cmp.Option }{}}, wantPanic: "unknown option", + reason: "use of unknown option should panic", }, { - label: label, - x: struct{ A, B, C int }{1, 2, 3}, - y: struct{ A, B, C int }{1, 2, 3}, + label: label + "/StructEqual", + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 3}, + wantEqual: true, + reason: "struct comparison with all equal fields", }, { - label: label, - x: struct{ A, B, C int }{1, 2, 3}, - y: struct{ A, B, C int }{1, 2, 4}, - wantDiff: "root.C:\n\t-: 3\n\t+: 4\n", + label: label + "/StructInequal", + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 4}, + wantEqual: false, + reason: "struct comparison with inequal C field", }, { - label: label, + label: label + "/StructUnexported", x: struct{ a, b, c int }{1, 2, 3}, y: struct{ a, b, c int }{1, 2, 4}, wantPanic: "cannot handle unexported field", + reason: "unexported fields result in a panic by default", }, { - label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(4)}, + label: label + "/PointerStructEqual", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(4)}, + wantEqual: true, + reason: "comparison of pointer to struct with equal A field", }, { - label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, - wantDiff: "*root.A:\n\t-: 4\n\t+: 5\n", + label: label + "/PointerStructInequal", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, + wantEqual: false, + reason: "comparison of pointer to struct with inequal A field", }, { - label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, + label: label + "/PointerStructTrueComparer", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y int) bool { return true }), }, + wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with always equal comparer", }, { - label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, + label: label + "/PointerStructNonNilComparer", + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), }, + wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with comparer checking pointers for nilness", }, { - label: label, - x: &struct{ R *bytes.Buffer }{}, - y: &struct{ R *bytes.Buffer }{}, + label: label + "/StructNestedPointerEqual", + x: &struct{ R *bytes.Buffer }{}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: true, + reason: "equal since both pointers in R field are nil", }, { - label: label, - x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, - y: &struct{ R *bytes.Buffer }{}, - wantDiff: "root.R:\n\t-: s\"\"\n\t+: \n", + label: label + "/StructNestedPointerInequal", + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: false, + reason: "inequal since R field is inequal", }, { - label: label, + label: label + "/StructNestedPointerTrueComparer", x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, y: &struct{ R *bytes.Buffer }{}, opts: []cmp.Option{ cmp.Comparer(func(x, y io.Reader) bool { return true }), }, + wantEqual: true, + reason: "equal despite inequal R field values since the comparer always reports true", }, { - label: label, + label: label + "/StructNestedValueUnexportedPanic1", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer contains unexported fields", }, { - label: label, + label: label + "/StructNestedValueUnexportedPanic2", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, opts: []cmp.Option{ cmp.Comparer(func(x, y io.Reader) bool { return true }), }, wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer value does not implement io.Reader", }, { - label: label, + label: label + "/StructNestedValueEqual", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, opts: []cmp.Option{ cmp.Transformer("Ref", func(x bytes.Buffer) *bytes.Buffer { return &x }), cmp.Comparer(func(x, y io.Reader) bool { return true }), }, + wantEqual: true, + reason: "bytes.Buffer pointer due to shallow copy does implement io.Reader", }, { - label: label, + label: label + "/RegexpUnexportedPanic", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, wantPanic: "cannot handle unexported field", + reason: "regexp.Regexp contains unexported fields", }, { - label: label, + label: label + "/RegexpEqual", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { @@ -258,8 +409,10 @@ func comparerTests() []test { } return x.String() == y.String() })}, + wantEqual: true, + reason: "comparer for *regexp.Regexp applied with equal regexp strings", }, { - label: label, + label: label + "/RegexpInequal", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*d*")}, opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { @@ -268,12 +421,10 @@ func comparerTests() []test { } return x.String() == y.String() })}, - wantDiff: ` -{[]*regexp.Regexp}[1]: - -: s"a*b*c*" - +: s"a*b*d*"`, + wantEqual: false, + reason: "comparer for *regexp.Regexp applied with inequal regexp strings", }, { - label: label, + label: label + "/TriplePointerEqual", x: func() ***int { a := 0 b := &a @@ -286,8 +437,10 @@ func comparerTests() []test { c := &b return &c }(), + wantEqual: true, + reason: "three layers of pointers to the same value", }, { - label: label, + label: label + "/TriplePointerInequal", x: func() ***int { a := 0 b := &a @@ -300,66 +453,48 @@ func comparerTests() []test { c := &b return &c }(), - wantDiff: ` -***{***int}: - -: 0 - +: 1`, - }, { - label: label, - x: []int{1, 2, 3, 4, 5}[:3], - y: []int{1, 2, 3}, - }, { - label: label, - x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, - y: struct{ fmt.Stringer }{regexp.MustCompile("hello")}, - opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, - }, { - label: label, - x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, - y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, - opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, - wantDiff: ` -root: - -: s"hello" - +: s"hello2"`, - }, { - label: label, - x: md5.Sum([]byte{'a'}), - y: md5.Sum([]byte{'b'}), - wantDiff: ` -{[16]uint8}: - -: [16]uint8{0x0c, 0xc1, 0x75, 0xb9, 0xc0, 0xf1, 0xb6, 0xa8, 0x31, 0xc3, 0x99, 0xe2, 0x69, 0x77, 0x26, 0x61} - +: [16]uint8{0x92, 0xeb, 0x5f, 0xfe, 0xe6, 0xae, 0x2f, 0xec, 0x3a, 0xd7, 0x1c, 0x77, 0x75, 0x31, 0x57, 0x8f}`, - }, { - label: label, - x: new(fmt.Stringer), - y: nil, - wantDiff: ` -: - -: & - +: `, - }, { - label: label, - x: makeTarHeaders('0'), - y: makeTarHeaders('\x00'), - wantDiff: ` -{[]cmp_test.tarHeader}[0].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[1].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[2].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[3].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[4].Typeflag: - -: 0x30 - +: 0x00`, - }, { - label: label, + wantEqual: false, + reason: "three layers of pointers to different values", + }, { + label: label + "/SliceWithDifferingCapacity", + x: []int{1, 2, 3, 4, 5}[:3], + y: []int{1, 2, 3}, + wantEqual: true, + reason: "elements past the slice length are not compared", + }, { + label: label + "/StringerEqual", + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: true, + reason: "comparer for fmt.Stringer used to compare differing types with same string", + }, { + label: label + "/StringerInequal", + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: false, + reason: "comparer for fmt.Stringer used to compare differing types with different strings", + }, { + label: label + "/DifferingHash", + x: sha256.Sum256([]byte{'a'}), + y: sha256.Sum256([]byte{'b'}), + wantEqual: false, + reason: "hash differs", + }, { + label: label + "/NilStringer", + x: new(fmt.Stringer), + y: nil, + wantEqual: false, + reason: "by default differing types are always inequal", + }, { + label: label + "/TarHeaders", + x: makeTarHeaders('0'), + y: makeTarHeaders('\x00'), + wantEqual: false, + reason: "type flag differs between the headers", + }, { + label: label + "/NonDeterministicComparer", x: make([]int, 1000), y: make([]int, 1000), opts: []cmp.Option{ @@ -368,8 +503,9 @@ root: }), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic comparer", }, { - label: label, + label: label + "/NonDeterministicFilter", x: make([]int, 1000), y: make([]int, 1000), opts: []cmp.Option{ @@ -378,8 +514,9 @@ root: }, cmp.Ignore()), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic filter", }, { - label: label, + label: label + "/AsymmetricComparer", x: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, y: []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, opts: []cmp.Option{ @@ -388,8 +525,9 @@ root: }), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "asymmetric comparer", }, { - label: label, + label: label + "/NonDeterministicTransformer", x: make([]string, 1000), y: make([]string, 1000), opts: []cmp.Option{ @@ -398,10 +536,9 @@ root: }), }, wantPanic: "non-deterministic function detected", + reason: "non-deterministic transformer", }, { - // Make sure the dynamic checks don't raise a false positive for - // non-reflexive comparisons. - label: label, + label: label + "/IrreflexiveComparison", x: make([]int, 10), y: make([]int, 10), opts: []cmp.Option{ @@ -409,26 +546,21 @@ root: return math.NaN() }), }, - wantDiff: ` -{[]int}: - -: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - +: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}`, - }, { - // Ensure reasonable Stringer formatting of map keys. - label: label, - x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, - y: map[*pb.Stringer]*pb.Stringer(nil), - wantDiff: ` -{map[*testprotos.Stringer]*testprotos.Stringer}: - -: map[*testprotos.Stringer]*testprotos.Stringer{s"hello": s"world"} - +: map[*testprotos.Stringer]*testprotos.Stringer(nil)`, - }, { - // Ensure Stringer avoids double-quote escaping if possible. - label: label, - x: []*pb.Stringer{{`multi\nline\nline\nline`}}, - wantDiff: ":\n\t-: []*testprotos.Stringer{s`multi\\nline\\nline\\nline`}\n\t+: ", - }, { - label: label, + wantEqual: false, + reason: "dynamic checks should not panic for non-reflexive comparisons", + }, { + label: label + "/StringerMapKey", + x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, + y: map[*pb.Stringer]*pb.Stringer(nil), + wantEqual: false, + reason: "stringer should be used to format the map key", + }, { + label: label + "/StringerBacktick", + x: []*pb.Stringer{{`multi\nline\nline\nline`}}, + wantEqual: false, + reason: "stringer should use backtick quoting if more readable", + }, { + label: label + "/AvoidPanicAssignableConverter", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -436,8 +568,10 @@ root: return x == nil && y == nil }), }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", }, { - label: label, + label: label + "/AvoidPanicAssignableTransformer", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -445,8 +579,10 @@ root: return v == nil }), }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", }, { - label: label, + label: label + "/AvoidPanicAssignableFilter", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -454,21 +590,126 @@ root: return x == nil && y == nil }, cmp.Ignore()), }, + wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", + }, { + label: label + "/DynamicMap", + x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}}, + y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}}, + wantEqual: false, + reason: "dynamic map with differing types (but semantically equivalent values) should be inequal", + }, { + label: label + "/MapKeyPointer", + x: map[*int]string{ + new(int): "hello", + }, + y: map[*int]string{ + new(int): "world", + }, + wantEqual: false, + reason: "map keys should use shallow (rather than deep) pointer comparison", }, { - label: label, - x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}}, - y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}}, - wantDiff: ` -root[0]["hr"]: - -: int(65) - +: float64(65) -root[1]["hr"]: - -: int(63) - +: float64(63)`, - }, { - label: label, - x: struct{ _ string }{}, - y: struct{ _ string }{}, + label: label + "/IgnoreSliceElements", + x: [2][]int{ + {0, 0, 0, 1, 2, 3, 0, 0, 4, 5, 6, 7, 8, 0, 9, 0, 0}, + {0, 1, 0, 0, 0, 20}, + }, + y: [2][]int{ + {1, 2, 3, 0, 4, 5, 6, 7, 0, 8, 9, 0, 0, 0}, + {0, 0, 1, 2, 0, 0, 0}, + }, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + vx, vy := p.Last().Values() + if vx.IsValid() && vx.Kind() == reflect.Int && vx.Int() == 0 { + return true + } + if vy.IsValid() && vy.Kind() == reflect.Int && vy.Int() == 0 { + return true + } + return false + }, cmp.Ignore()), + }, + wantEqual: false, + reason: "all zero slice elements are ignored (even if missing)", + }, { + label: label + "/IgnoreMapEntries", + x: [2]map[string]int{ + {"ignore1": 0, "ignore2": 0, "keep1": 1, "keep2": 2, "KEEP3": 3, "IGNORE3": 0}, + {"keep1": 1, "ignore1": 0}, + }, + y: [2]map[string]int{ + {"ignore1": 0, "ignore3": 0, "ignore4": 0, "keep1": 1, "keep2": 2, "KEEP3": 3}, + {"keep1": 1, "keep2": 2, "ignore2": 0}, + }, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + vx, vy := p.Last().Values() + if vx.IsValid() && vx.Kind() == reflect.Int && vx.Int() == 0 { + return true + } + if vy.IsValid() && vy.Kind() == reflect.Int && vy.Int() == 0 { + return true + } + return false + }, cmp.Ignore()), + }, + wantEqual: false, + reason: "all zero map entries are ignored (even if missing)", + }, { + label: label + "/PanicUnexportedNamed", + x: namedWithUnexported{unexported: "x"}, + y: namedWithUnexported{unexported: "y"}, + wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".namedWithUnexported", + reason: "panic on named struct type with unexported field", + }, { + label: label + "/PanicUnexportedUnnamed", + x: struct{ a int }{}, + y: struct{ a int }{}, + wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".(struct { a int })", + reason: "panic on unnamed struct type with unexported field", + }, { + label: label + "/UnaddressableStruct", + x: struct{ s fmt.Stringer }{new(bytes.Buffer)}, + y: struct{ s fmt.Stringer }{nil}, + opts: []cmp.Option{ + cmp.AllowUnexported(struct{ s fmt.Stringer }{}), + cmp.FilterPath(func(p cmp.Path) bool { + if _, ok := p.Last().(cmp.StructField); !ok { + return false + } + + t := p.Index(-1).Type() + vx, vy := p.Index(-1).Values() + pvx, pvy := p.Index(-2).Values() + switch { + case vx.Type() != t: + panic(fmt.Sprintf("inconsistent type: %v != %v", vx.Type(), t)) + case vy.Type() != t: + panic(fmt.Sprintf("inconsistent type: %v != %v", vy.Type(), t)) + case vx.CanAddr() != pvx.CanAddr(): + panic(fmt.Sprintf("inconsistent addressability: %v != %v", vx.CanAddr(), pvx.CanAddr())) + case vy.CanAddr() != pvy.CanAddr(): + panic(fmt.Sprintf("inconsistent addressability: %v != %v", vy.CanAddr(), pvy.CanAddr())) + } + return true + }, cmp.Ignore()), + }, + wantEqual: true, + reason: "verify that exporter does not leak implementation details", + }, { + label: label + "/ErrorPanic", + x: io.EOF, + y: io.EOF, + wantPanic: "consider using cmpopts.EquateErrors", + reason: "suggest cmpopts.EquateErrors when accessing unexported fields of error types", + }, { + label: label + "/ErrorEqual", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{cmpopts.EquateErrors()}, + wantEqual: true, + reason: "cmpopts.EquateErrors should equate these two errors as sentinel values", }} } @@ -493,7 +734,7 @@ func transformerTests() []test { } return []test{{ - label: label, + label: label + "/Uints", x: uint8(0), y: uint8(1), opts: []cmp.Option{ @@ -501,12 +742,10 @@ func transformerTests() []test { cmp.Transformer("λ", func(in uint16) uint32 { return uint32(in) }), cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), }, - wantDiff: ` -λ(λ(λ({uint8}))): - -: 0x00 - +: 0x01`, + wantEqual: false, + reason: "transform uint8 -> uint16 -> uint32 -> uint64", }, { - label: label, + label: label + "/Ambiguous", x: 0, y: 1, opts: []cmp.Option{ @@ -514,8 +753,9 @@ func transformerTests() []test { cmp.Transformer("λ", func(in int) int { return in }), }, wantPanic: "ambiguous set of applicable options", + reason: "both transformers apply on int", }, { - label: label, + label: label + "/Filtered", x: []int{0, -5, 0, -1}, y: []int{1, 3, 0, -5}, opts: []cmp.Option{ @@ -528,31 +768,24 @@ func transformerTests() []test { cmp.Transformer("λ", func(in int) int64 { return int64(in) }), ), }, - wantDiff: ` -λ({[]int}[1]): - -: -5 - +: 3 -λ({[]int}[3]): - -: -1 - +: -5`, + wantEqual: false, + reason: "disjoint transformers filtered based on the values", }, { - label: label, + label: label + "/DisjointOutput", x: 0, y: 1, opts: []cmp.Option{ cmp.Transformer("λ", func(in int) interface{} { if in == 0 { - return "string" + return "zero" } return float64(in) }), }, - wantDiff: ` -λ({int}): - -: "string" - +: 1`, + wantEqual: false, + reason: "output type differs based on input value", }, { - label: label, + label: label + "/JSON", x: `{ "firstName": "John", "lastName": "Smith", @@ -589,55 +822,609 @@ func transformerTests() []test { return m }), }, - wantDiff: ` -ParseJSON({string})["address"]["city"]: - -: "Los Angeles" - +: "New York" -ParseJSON({string})["address"]["state"]: - -: "CA" - +: "NY" -ParseJSON({string})["phoneNumbers"][0]["number"]: - -: "212 555-4321" - +: "212 555-1234" -ParseJSON({string})["spouse"]: - -: - +: interface {}(nil)`, - }, { - label: label, + wantEqual: false, + reason: "transformer used to parse JSON input", + }, { + label: label + "/AcyclicString", x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")}, y: StringBytes{String: "some\nmulti\nline\nstring", Bytes: []byte("some\nmulti\nline\nBytes")}, opts: []cmp.Option{ transformOnce("SplitString", func(s string) []string { return strings.Split(s, "\n") }), transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }), }, - wantDiff: ` -SplitString({cmp_test.StringBytes}.String)[2]: - -: "Line" - +: "line" -SplitBytes({cmp_test.StringBytes}.Bytes)[3][0]: - -: 0x62 - +: 0x42`, + wantEqual: false, + reason: "string -> []string and []byte -> [][]byte transformer only applied once", }, { - x: "a\nb\nc\n", - y: "a\nb\nc\n", + label: label + "/CyclicString", + x: "a\nb\nc\n", + y: "a\nb\nc\n", opts: []cmp.Option{ cmp.Transformer("SplitLines", func(s string) []string { return strings.Split(s, "\n") }), }, wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from string -> []string -> string", }, { - x: complex64(0), - y: complex64(0), + label: label + "/CyclicComplex", + x: complex64(0), + y: complex64(0), opts: []cmp.Option{ cmp.Transformer("T1", func(x complex64) complex128 { return complex128(x) }), cmp.Transformer("T2", func(x complex128) [2]float64 { return [2]float64{real(x), imag(x)} }), cmp.Transformer("T3", func(x float64) complex64 { return complex64(complex(x, 0)) }), }, wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from complex64 -> complex128 -> [2]float64 -> complex64", + }} +} + +func reporterTests() []test { + const label = "Reporter" + + type ( + MyString string + MyByte byte + MyBytes []byte + MyInt int8 + MyInts []int8 + MyUint int16 + MyUints []int16 + MyFloat float32 + MyFloats []float32 + MyComposite struct { + StringA string + StringB MyString + BytesA []byte + BytesB []MyByte + BytesC MyBytes + IntsA []int8 + IntsB []MyInt + IntsC MyInts + UintsA []uint16 + UintsB []MyUint + UintsC MyUints + FloatsA []float32 + FloatsB []MyFloat + FloatsC MyFloats + } + PointerString *string + ) + + return []test{{ + label: label + "/PanicStringer", + x: struct{ X fmt.Stringer }{struct{ fmt.Stringer }{nil}}, + y: struct{ X fmt.Stringer }{bytes.NewBuffer(nil)}, + wantEqual: false, + reason: "panic from fmt.Stringer should not crash the reporter", + }, { + label: label + "/PanicError", + x: struct{ X error }{struct{ error }{nil}}, + y: struct{ X error }{errors.New("")}, + wantEqual: false, + reason: "panic from error should not crash the reporter", + }, { + label: label + "/AmbiguousType", + x: foo1.Bar{}, + y: foo2.Bar{}, + wantEqual: false, + reason: "reporter should display the qualified type name to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointer", + x: newInt(0), + y: newInt(0), + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointerStruct", + x: struct{ I *int }{newInt(0)}, + y: struct{ I *int }{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousPointerSlice", + x: []*int{newInt(0)}, + y: []*int{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousPointerMap", + x: map[string]*int{"zero": newInt(0)}, + y: map[string]*int{"zero": newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two map values", + }, { + label: label + "/AmbiguousStringer", + x: Stringer("hello"), + y: newStringer("hello"), + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two values", + }, { + label: label + "/AmbiguousStringerStruct", + x: struct{ S fmt.Stringer }{Stringer("hello")}, + y: struct{ S fmt.Stringer }{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousStringerSlice", + x: []fmt.Stringer{Stringer("hello")}, + y: []fmt.Stringer{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousStringerMap", + x: map[string]fmt.Stringer{"zero": Stringer("hello")}, + y: map[string]fmt.Stringer{"zero": newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map values", + }, { + label: label + "/AmbiguousSliceHeader", + x: make([]int, 0, 5), + y: make([]int, 0, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(x, y []int) bool { return cap(x) == cap(y) }), + }, + wantEqual: false, + reason: "reporter should display the slice header to disambiguate between the two slice values", + }, { + label: label + "/AmbiguousStringerMapKey", + x: map[interface{}]string{ + nil: "nil", + Stringer("hello"): "goodbye", + foo1.Bar{"fizz"}: "buzz", + }, + y: map[interface{}]string{ + newStringer("hello"): "goodbye", + foo2.Bar{"fizz"}: "buzz", + }, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map keys", + }, { + label: label + "/NonAmbiguousStringerMapKey", + x: map[interface{}]string{Stringer("hello"): "goodbye"}, + y: map[interface{}]string{newStringer("fizz"): "buzz"}, + wantEqual: false, + reason: "reporter should call String as there is no ambiguity between the two map keys", + }, { + label: label + "/InvalidUTF8", + x: MyString("\xed\xa0\x80"), + wantEqual: false, + reason: "invalid UTF-8 should format as quoted string", + }, { + label: label + "/UnbatchedSlice", + x: MyComposite{IntsA: []int8{11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{IntsA: []int8{10, 11, 21, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + wantEqual: false, + reason: "unbatched diffing desired since few elements differ", + }, { + label: label + "/BatchedSlice", + x: MyComposite{IntsA: []int8{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{IntsA: []int8{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, + wantEqual: false, + reason: "batched diffing desired since many elements differ", + }, { + label: label + "/BatchedWithComparer", + x: MyComposite{BytesA: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{BytesA: []byte{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, + wantEqual: false, + opts: []cmp.Option{ + cmp.Comparer(bytes.Equal), + }, + reason: "batched diffing desired since many elements differ", + }, { + label: label + "/BatchedLong", + x: MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127}}, + wantEqual: false, + reason: "batched output desired for a single slice of primitives unique to one of the inputs", + }, { + label: label + "/BatchedNamedAndUnnamed", + x: MyComposite{ + BytesA: []byte{1, 2, 3}, + BytesB: []MyByte{4, 5, 6}, + BytesC: MyBytes{7, 8, 9}, + IntsA: []int8{-1, -2, -3}, + IntsB: []MyInt{-4, -5, -6}, + IntsC: MyInts{-7, -8, -9}, + UintsA: []uint16{1000, 2000, 3000}, + UintsB: []MyUint{4000, 5000, 6000}, + UintsC: MyUints{7000, 8000, 9000}, + FloatsA: []float32{1.5, 2.5, 3.5}, + FloatsB: []MyFloat{4.5, 5.5, 6.5}, + FloatsC: MyFloats{7.5, 8.5, 9.5}, + }, + y: MyComposite{ + BytesA: []byte{3, 2, 1}, + BytesB: []MyByte{6, 5, 4}, + BytesC: MyBytes{9, 8, 7}, + IntsA: []int8{-3, -2, -1}, + IntsB: []MyInt{-6, -5, -4}, + IntsC: MyInts{-9, -8, -7}, + UintsA: []uint16{3000, 2000, 1000}, + UintsB: []MyUint{6000, 5000, 4000}, + UintsC: MyUints{9000, 8000, 7000}, + FloatsA: []float32{3.5, 2.5, 1.5}, + FloatsB: []MyFloat{6.5, 5.5, 4.5}, + FloatsC: MyFloats{9.5, 8.5, 7.5}, + }, + wantEqual: false, + reason: "batched diffing available for both named and unnamed slices", + }, { + label: label + "/BinaryHexdump", + x: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeX\x95A\xfd$fX\x8byT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1U~{\xf6\xb3~\x1dWi \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, + y: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1u-[]]\xf6\xb3haha~\x1dWI \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, + wantEqual: false, + reason: "binary diff in hexdump form since data is binary data", + }, { + label: label + "/StringHexdump", + x: MyComposite{StringB: MyString("readme.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000046\x0000000000000\x00011173\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, + y: MyComposite{StringB: MyString("gopher.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000043\x0000000000000\x00011217\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, + wantEqual: false, + reason: "binary diff desired since string looks like binary data", + }, { + label: label + "/BinaryString", + x: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"314 54th Avenue","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, + y: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, + wantEqual: false, + reason: "batched textual diff desired since bytes looks like textual data", + }, { + label: label + "/TripleQuote", + x: MyComposite{StringA: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + y: MyComposite{StringA: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/TripleQuoteSlice", + x: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of strings", + }, { + label: label + "/TripleQuoteNamedTypes", + x: MyComposite{ + StringB: MyString("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + y: MyComposite{ + StringB: MyString("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + wantEqual: false, + reason: "use triple-quote syntax for named types", + }, { + label: label + "/TripleQuoteSliceNamedTypes", + x: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of named strings", + }, { + label: label + "/TripleQuoteEndlines", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n\r", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteAmbiguousQuotes", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\n\"\"\"\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous triple quotes", + }, { + label: label + "/AvoidTripleQuoteAmbiguousEllipsis", + x: "aaa\nbbb\nccc\n...\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous ellipsis", + }, { + label: label + "/AvoidTripleQuoteNonPrintable", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\no\roo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteIdenticalWhitespace", + x: "aaa\nbbb\nccc\n ddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to visual equivalence of differences", + }, { + label: label + "/TripleQuoteStringer", + x: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")), + }, + y: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")), + }, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: false, + reason: "multi-line String output should be formatted with triple quote", + }, { + label: label + "/LimitMaximumBytesDiffs", + x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="), + y: []byte("\\====|\x96\xe7SB\xa0\xab=====\xf0\xbd\xa5q\xab\x17;======\xabP\x00=======\xeb====\xa5\x14\xe6O(\xe4=====(======/c@?===========\xd9x\xed\x13=====J\xfc\x918B\x8d======a8A\xebs\x04\xae=======\aC====\x1c===========\x91\"=======uؾ====s\xec\x845\a=====;\xabS9t======\x1f\x1b=======\x80\xab/\xed+:;====\xeaI===========\xabl=======\xb9\xe9\xfdH\x93\x8e\u007f====ח\xe5=====Ig\x88m\xf5\x01V=============\xf7+4\xb0\x92E====\x9fj\xf8&\xd0h\xf9=====\xeeΨ\r\xbf============="), + wantEqual: false, + reason: "total bytes difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumStringDiffs", + x: "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", + y: "aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", + wantEqual: false, + reason: "total string difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumSliceDiffs", + x: func() (out []struct{ S string }) { + for _, s := range strings.Split("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + y: func() (out []struct{ S string }) { + for _, s := range strings.Split("aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + wantEqual: false, + reason: "total slice difference output is truncated due to excessive number of differences", + }, { + label: label + "/MultilineString", + x: MyComposite{ + StringA: strings.TrimPrefix(` +Package cmp determines equality of values. + +This package is intended to be a more powerful and safer alternative to +reflect.DeepEqual for comparing whether two values are semantically equal. + +The primary features of cmp are: + +• When the default behavior of equality does not suit the needs of the test, +custom equality functions can override the equality operation. +For example, an equality function may report floats as equal so long as they +are within some tolerance of each other. + +• Types that have an Equal method may use that method to determine equality. +This allows package authors to determine the equality operation for the types +that they define. + +• If no custom equality functions are used and no Equal method is defined, +equality is determined by recursively comparing the primitive kinds on both +values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +fields are not compared by default; they result in panics unless suppressed +by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared +using the AllowUnexported option. +`, "\n"), + }, + y: MyComposite{ + StringA: strings.TrimPrefix(` +Package cmp determines equality of value. + +This package is intended to be a more powerful and safer alternative to +reflect.DeepEqual for comparing whether two values are semantically equal. + +The primary features of cmp are: + +• When the default behavior of equality does not suit the needs of the test, +custom equality functions can override the equality operation. +For example, an equality function may report floats as equal so long as they +are within some tolerance of each other. + +• If no custom equality functions are used and no Equal method is defined, +equality is determined by recursively comparing the primitive kinds on both +values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +fields are not compared by default; they result in panics unless suppressed +by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared +using the AllowUnexported option.`, "\n"), + }, + wantEqual: false, + reason: "batched per-line diff desired since string looks like multi-line textual data", + }, { + label: label + "/Slices", + x: MyComposite{ + BytesA: []byte{1, 2, 3}, + BytesB: []MyByte{4, 5, 6}, + BytesC: MyBytes{7, 8, 9}, + IntsA: []int8{-1, -2, -3}, + IntsB: []MyInt{-4, -5, -6}, + IntsC: MyInts{-7, -8, -9}, + UintsA: []uint16{1000, 2000, 3000}, + UintsB: []MyUint{4000, 5000, 6000}, + UintsC: MyUints{7000, 8000, 9000}, + FloatsA: []float32{1.5, 2.5, 3.5}, + FloatsB: []MyFloat{4.5, 5.5, 6.5}, + FloatsC: MyFloats{7.5, 8.5, 9.5}, + }, + y: MyComposite{}, + wantEqual: false, + reason: "batched diffing for non-nil slices and nil slices", + }, { + label: label + "/EmptySlices", + x: MyComposite{ + BytesA: []byte{}, + BytesB: []MyByte{}, + BytesC: MyBytes{}, + IntsA: []int8{}, + IntsB: []MyInt{}, + IntsC: MyInts{}, + UintsA: []uint16{}, + UintsB: []MyUint{}, + UintsC: MyUints{}, + FloatsA: []float32{}, + FloatsB: []MyFloat{}, + FloatsC: MyFloats{}, + }, + y: MyComposite{}, + wantEqual: false, + reason: "batched diffing for empty slices and nil slices", + }, { + label: label + "/LargeMapKey", + x: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20) + return &b + }(): 0}, + y: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20) + return &b + }(): 0}, + reason: "printing map keys should have some verbosity limit imposed", + }, { + label: label + "/LargeStringInInterface", + x: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}, + + y: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}, + reason: "strings within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeBytesInInterface", + x: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.")}, + y: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,")}, + reason: "bytes slice within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeStandaloneString", + x: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}}, + y: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}}, + reason: "printing a large standalone string that is different should print enough context to see the difference", + }, { + label: label + "/SurroundingEqualElements", + x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,#=_value _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,#=_value _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,#=_value _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,#=_value _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c,#=_value _value=4 41\t", + y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\t", + reason: "leading/trailing equal spans should not appear in diff lines", + }, { + label: label + "/MostlyTextString", + x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,\xff=_value _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,\xff=_value _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,\xff=_value _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,\xff=_value _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1=c,\xff=_value _value=4 41\n", + y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\n", + reason: "the presence of a few invalid UTF-8 characters should not prevent printing this as text", + }, { + label: label + "/AllLinesDiffer", + x: "d5c14bdf6bac81c27afc5429500ed750\n25483503b557c606dad4f144d27ae10b\n90bdbcdbb6ea7156068e3dcfb7459244\n978f480a6e3cced51e297fbff9a506b7\n", + y: "Xd5c14bdf6bac81c27afc5429500ed750\nX25483503b557c606dad4f144d27ae10b\nX90bdbcdbb6ea7156068e3dcfb7459244\nX978f480a6e3cced51e297fbff9a506b7\n", + reason: "all lines are different, so diffing based on lines is pointless", + }, { + label: label + "/StringifiedBytes", + x: struct{ X []byte }{[]byte("hello, world!")}, + y: struct{ X []byte }{}, + reason: "[]byte should be printed as text since it is printable text", + }, { + label: label + "/NonStringifiedBytes", + x: struct{ X []byte }{[]byte("\xde\xad\xbe\xef")}, + y: struct{ X []byte }{}, + reason: "[]byte should not be printed as text since it is binary data", + }, { + label: label + "/StringifiedNamedBytes", + x: struct{ X MyBytes }{MyBytes("hello, world!")}, + y: struct{ X MyBytes }{}, + reason: "MyBytes should be printed as text since it is printable text", + }, { + label: label + "/NonStringifiedNamedBytes", + x: struct{ X MyBytes }{MyBytes("\xde\xad\xbe\xef")}, + y: struct{ X MyBytes }{}, + reason: "MyBytes should not be printed as text since it is binary data", + }, { + label: label + "/ShortJSON", + x: `{ + "id": 1, + "foo": true, + "bar": true, +}`, + y: `{ + "id": 1434180, + "foo": true, + "bar": true, +}`, + reason: "short multiline JSON should prefer triple-quoted string diff as it is more readable", + }, { + label: label + "/PointerToStringOrAny", + x: func() *string { + var v string = "hello" + return &v + }(), + y: func() *interface{} { + var v interface{} = "hello" + return &v + }(), + reason: "mismatched types between any and *any should print differently", + }, { + label: label + "/NamedPointer", + x: func() *string { + v := "hello" + return &v + }(), + y: func() PointerString { + v := "hello" + return &v + }(), + reason: "mismatched pointer types should print differently", + }, { + label: label + "/MapStringAny", + x: map[string]interface{}{"key": int(0)}, + y: map[string]interface{}{"key": uint(0)}, + reason: "mismatched underlying value within interface", + }, { + label: label + "/StructFieldAny", + x: struct{ X interface{} }{int(0)}, + y: struct{ X interface{} }{uint(0)}, + reason: "mismatched underlying value within interface", + }, { + label: label + "/SliceOfBytesText", + x: [][]byte{ + []byte("hello"), []byte("foo"), []byte("barbaz"), []byte("blahdieblah"), + }, + y: [][]byte{ + []byte("foo"), []byte("foo"), []byte("barbaz"), []byte("added"), []byte("here"), []byte("hrmph"), + }, + reason: "should print text byte slices as strings", + }, { + label: label + "/SliceOfBytesBinary", + x: [][]byte{ + []byte("\xde\xad\xbe\xef"), []byte("\xffoo"), []byte("barbaz"), []byte("blahdieblah"), + }, + y: [][]byte{ + []byte("\xffoo"), []byte("foo"), []byte("barbaz"), []byte("added"), []byte("here"), []byte("hrmph\xff"), + }, + reason: "should print text byte slices as strings except those with binary", + }, { + label: label + "/ManyEscapeCharacters", + x: `[ + {"Base32": "NA======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + {"Base32": "NBSWY3A="}, + {"Base32": "NBSWY3DP"} +]`, + y: `[ + {"Base32": "NB======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + {"Base32": "NBSWY3A="}, + {"Base32": "NBSWY3DP"} +]`, + reason: "should use line-based diffing since byte-based diffing is unreadable due to heavy amounts of escaping", }} } func embeddedTests() []test { - const label = "EmbeddedStruct/" + const label = "EmbeddedStruct" privateStruct := *new(ts.ParentStructA).PrivateStruct() @@ -730,538 +1517,502 @@ func embeddedTests() []test { } return []test{{ - label: label + "ParentStructA", + label: label + "/ParentStructA/PanicUnexported1", x: ts.ParentStructA{}, y: ts.ParentStructA{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructA has an unexported field", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Ignored", x: ts.ParentStructA{}, y: ts.ParentStructA{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructA{}), }, + wantEqual: true, + reason: "the only field (which is unexported) of ParentStructA is ignored", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/PanicUnexported2", x: createStructA(0), y: createStructA(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Equal", x: createStructA(0), y: createStructA(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructA and privateStruct are allowed", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Inequal", x: createStructA(0), y: createStructA(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, - wantDiff: ` -{teststructs.ParentStructA}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructA}.privateStruct.private: - -: 2 - +: 3`, + wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/PanicUnexported1", x: ts.ParentStructB{}, y: ts.ParentStructB{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructB{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct has an unexported field", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Ignored", x: ts.ParentStructB{}, y: ts.ParentStructB{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructB{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are ignored", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/PanicUnexported2", x: createStructB(0), y: createStructB(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Equal", x: createStructB(0), y: createStructB(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are allowed", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Inequal", x: createStructB(0), y: createStructB(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, - wantDiff: ` -{teststructs.ParentStructB}.PublicStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructB}.PublicStruct.private: - -: 2 - +: 3`, + wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/PanicUnexported1", x: ts.ParentStructC{}, y: ts.ParentStructC{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructC has unexported fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Ignored", x: ts.ParentStructC{}, y: ts.ParentStructC{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructC{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructC are ignored", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/PanicUnexported2", x: createStructC(0), y: createStructC(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Equal", x: createStructC(0), y: createStructC(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructC and privateStruct are allowed", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Inequal", x: createStructC(0), y: createStructC(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, - wantDiff: ` -{teststructs.ParentStructC}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructC}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructC}.Public: - -: 3 - +: 4 -{teststructs.ParentStructC}.private: - -: 4 - +: 5`, - }, { - label: label + "ParentStructD", + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructD/PanicUnexported1", x: ts.ParentStructD{}, y: ts.ParentStructD{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructD{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructD has unexported fields", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Ignored", x: ts.ParentStructD{}, y: ts.ParentStructD{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructD{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructD and PublicStruct are ignored", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/PanicUnexported2", x: createStructD(0), y: createStructD(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Equal", x: createStructD(0), y: createStructD(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of both ParentStructD and PublicStruct are allowed", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Inequal", x: createStructD(0), y: createStructD(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, - wantDiff: ` -{teststructs.ParentStructD}.PublicStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructD}.PublicStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructD}.Public: - -: 3 - +: 4 -{teststructs.ParentStructD}.private: - -: 4 - +: 5`, - }, { - label: label + "ParentStructE", + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructE/PanicUnexported1", x: ts.ParentStructE{}, y: ts.ParentStructE{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructE{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructE has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Ignored", x: ts.ParentStructE{}, y: ts.ParentStructE{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructE{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructE and PublicStruct are ignored", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/PanicUnexported2", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/PanicUnexported3", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Equal", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructE, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Inequal", x: createStructE(0), y: createStructE(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` -{teststructs.ParentStructE}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructE}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructE}.PublicStruct.Public: - -: 3 - +: 4 -{teststructs.ParentStructE}.PublicStruct.private: - -: 4 - +: 5`, - }, { - label: label + "ParentStructF", + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructF/PanicUnexported1", x: ts.ParentStructF{}, y: ts.ParentStructF{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructF{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructF has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Ignored", x: ts.ParentStructF{}, y: ts.ParentStructF{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructF{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructF and PublicStruct are ignored", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/PanicUnexported2", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/PanicUnexported3", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Equal", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructF, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Inequal", x: createStructF(0), y: createStructF(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` -{teststructs.ParentStructF}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructF}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructF}.PublicStruct.Public: - -: 3 - +: 4 -{teststructs.ParentStructF}.PublicStruct.private: - -: 4 - +: 5 -{teststructs.ParentStructF}.Public: - -: 5 - +: 6 -{teststructs.ParentStructF}.private: - -: 6 - +: 7`, - }, { - label: label + "ParentStructG", + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructG/PanicUnexported1", x: ts.ParentStructG{}, y: ts.ParentStructG{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructG has unexported fields", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Ignored", x: ts.ParentStructG{}, y: ts.ParentStructG{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructG{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructG are ignored", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/PanicUnexported2", x: createStructG(0), y: createStructG(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Equal", x: createStructG(0), y: createStructG(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructG and privateStruct are allowed", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Inequal", x: createStructG(0), y: createStructG(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, - wantDiff: ` -{*teststructs.ParentStructG}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructG}.privateStruct.private: - -: 2 - +: 3`, + wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructH", - x: ts.ParentStructH{}, - y: ts.ParentStructH{}, + label: label + "/ParentStructH/EqualNil", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + wantEqual: true, + reason: "PublicStruct is not compared because the pointer is nil", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/PanicUnexported1", x: createStructH(0), y: createStructH(0), wantPanic: "cannot handle unexported field", + reason: "PublicStruct has unexported fields", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Ignored", x: ts.ParentStructH{}, y: ts.ParentStructH{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructH{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructH are ignored (it has none)", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/PanicUnexported2", x: createStructH(0), y: createStructH(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Equal", x: createStructH(0), y: createStructH(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of both ParentStructH and PublicStruct are allowed", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Inequal", x: createStructH(0), y: createStructH(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, - wantDiff: ` -{*teststructs.ParentStructH}.PublicStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructH}.PublicStruct.private: - -: 2 - +: 3`, + wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported1", x: ts.ParentStructI{}, y: ts.ParentStructI{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructI has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Ignored1", x: ts.ParentStructI{}, y: ts.ParentStructI{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructI are ignored", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported2", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Ignored2", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructI and PublicStruct are ignored", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported3", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Equal", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructI, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Inequal", x: createStructI(0), y: createStructI(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` -{*teststructs.ParentStructI}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructI}.privateStruct.private: - -: 2 - +: 3 -{*teststructs.ParentStructI}.PublicStruct.Public: - -: 3 - +: 4 -{*teststructs.ParentStructI}.PublicStruct.private: - -: 4 - +: 5`, - }, { - label: label + "ParentStructJ", + wantEqual: false, + reason: "the two values differ on some fields", + }, { + label: label + "/ParentStructJ/PanicUnexported1", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructJ has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/PanicUnexported2", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructJ{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Ignored", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), }, + wantEqual: true, + reason: "unexported fields of ParentStructJ and PublicStruct are ignored", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/PanicUnexported3", x: createStructJ(0), y: createStructJ(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Equal", x: createStructJ(0), y: createStructJ(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, + reason: "unexported fields of both ParentStructJ, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Inequal", x: createStructJ(0), y: createStructJ(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` -{*teststructs.ParentStructJ}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructJ}.privateStruct.private: - -: 2 - +: 3 -{*teststructs.ParentStructJ}.PublicStruct.Public: - -: 3 - +: 4 -{*teststructs.ParentStructJ}.PublicStruct.private: - -: 4 - +: 5 -{*teststructs.ParentStructJ}.Public.Public: - -: 7 - +: 8 -{*teststructs.ParentStructJ}.Public.private: - -: 8 - +: 9 -{*teststructs.ParentStructJ}.private.Public: - -: 5 - +: 6 -{*teststructs.ParentStructJ}.private.private: - -: 6 - +: 7`, + wantEqual: false, + reason: "the two values differ on some fields", }} } func methodTests() []test { - const label = "EqualMethod/" + const label = "EqualMethod" // A common mistake that the Equal method is on a pointer receiver, // but only a non-pointer value is present in the struct. // A transform can be used to forcibly reference the value. - derefTransform := cmp.FilterPath(func(p cmp.Path) bool { + addrTransform := cmp.FilterPath(func(p cmp.Path) bool { if len(p) == 0 { return false } @@ -1275,7 +2026,7 @@ func methodTests() []test { tf.In(0).AssignableTo(tf.In(1)) && tf.Out(0) == reflect.TypeOf(true) } return false - }, cmp.Transformer("Ref", func(x interface{}) interface{} { + }, cmp.Transformer("Addr", func(x interface{}) interface{} { v := reflect.ValueOf(x) vp := reflect.New(v.Type()) vp.Elem().Set(v) @@ -1286,279 +2037,537 @@ func methodTests() []test { // returns true, while the underlying data are fundamentally different. // Since the method should be called, these are expected to be equal. return []test{{ - label: label + "StructA", - x: ts.StructA{X: "NotEqual"}, - y: ts.StructA{X: "not_equal"}, - }, { - label: label + "StructA", - x: &ts.StructA{X: "NotEqual"}, - y: &ts.StructA{X: "not_equal"}, - }, { - label: label + "StructB", - x: ts.StructB{X: "NotEqual"}, - y: ts.StructB{X: "not_equal"}, - wantDiff: ` -{teststructs.StructB}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructB", - x: ts.StructB{X: "NotEqual"}, - y: ts.StructB{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB", - x: &ts.StructB{X: "NotEqual"}, - y: &ts.StructB{X: "not_equal"}, - }, { - label: label + "StructC", - x: ts.StructC{X: "NotEqual"}, - y: ts.StructC{X: "not_equal"}, - }, { - label: label + "StructC", - x: &ts.StructC{X: "NotEqual"}, - y: &ts.StructC{X: "not_equal"}, - }, { - label: label + "StructD", - x: ts.StructD{X: "NotEqual"}, - y: ts.StructD{X: "not_equal"}, - wantDiff: ` -{teststructs.StructD}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructD", - x: ts.StructD{X: "NotEqual"}, - y: ts.StructD{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructD", - x: &ts.StructD{X: "NotEqual"}, - y: &ts.StructD{X: "not_equal"}, - }, { - label: label + "StructE", - x: ts.StructE{X: "NotEqual"}, - y: ts.StructE{X: "not_equal"}, - wantDiff: ` -{teststructs.StructE}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructE", - x: ts.StructE{X: "NotEqual"}, - y: ts.StructE{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructE", - x: &ts.StructE{X: "NotEqual"}, - y: &ts.StructE{X: "not_equal"}, - }, { - label: label + "StructF", - x: ts.StructF{X: "NotEqual"}, - y: ts.StructF{X: "not_equal"}, - wantDiff: ` -{teststructs.StructF}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructF", - x: &ts.StructF{X: "NotEqual"}, - y: &ts.StructF{X: "not_equal"}, - }, { - label: label + "StructA1", - x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, - y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA1", - x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructA1", - x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, - y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA1", - x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructB1", - x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, - y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB1", - x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: "{teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructB1", - x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, - y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB1", - x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: "{*teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructC1", - x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructC1", - x: &ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD1", - x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` -{teststructs.StructD1}.StructD.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructD1}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructD1", - x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructD1", - x: &ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE1", - x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` -{teststructs.StructE1}.StructE.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructE1}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructE1", - x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructE1", - x: &ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF1", - x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` -{teststructs.StructF1}.StructF.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructF1}.X: - -: "NotEqual" - +: "not_equal"`, - }, { - label: label + "StructF1", - x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructA2", - x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, - y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA2", - x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructA2", - x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, - y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA2", - x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructB2", - x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, - y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructB2", - x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructB2", - x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, - y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructB2", - x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "StructC2", - x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructC2", - x: &ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD2", - x: ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD2", - x: &ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE2", - x: ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE2", - x: &ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF2", - x: ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF2", - x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructNo", - x: ts.StructNo{X: "NotEqual"}, - y: ts.StructNo{X: "not_equal"}, - wantDiff: "{teststructs.StructNo}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", - }, { - label: label + "AssignA", - x: ts.AssignA(func() int { return 0 }), - y: ts.AssignA(func() int { return 1 }), - }, { - label: label + "AssignB", - x: ts.AssignB(struct{ A int }{0}), - y: ts.AssignB(struct{ A int }{1}), - }, { - label: label + "AssignC", - x: ts.AssignC(make(chan bool)), - y: ts.AssignC(make(chan bool)), - }, { - label: label + "AssignD", - x: ts.AssignD(make(chan bool)), - y: ts.AssignD(make(chan bool)), + label: label + "/StructA/ValueEqual", + x: ts.StructA{X: "NotEqual"}, + y: ts.StructA{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructA value called", + }, { + label: label + "/StructA/PointerEqual", + x: &ts.StructA{X: "NotEqual"}, + y: &ts.StructA{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called", + }, { + label: label + "/StructB/ValueInequal", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB value not called", + }, { + label: label + "/StructB/ValueAddrEqual", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform", + }, { + label: label + "/StructB/PointerEqual", + x: &ts.StructB{X: "NotEqual"}, + y: &ts.StructB{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called", + }, { + label: label + "/StructC/ValueEqual", + x: ts.StructC{X: "NotEqual"}, + y: ts.StructC{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC value called", + }, { + label: label + "/StructC/PointerEqual", + x: &ts.StructC{X: "NotEqual"}, + y: &ts.StructC{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC pointer called", + }, { + label: label + "/StructD/ValueInequal", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructD value not called", + }, { + label: label + "/StructD/ValueAddrEqual", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructD pointer called due to shallow copy transform", + }, { + label: label + "/StructD/PointerEqual", + x: &ts.StructD{X: "NotEqual"}, + y: &ts.StructD{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructD pointer called", + }, { + label: label + "/StructE/ValueInequal", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructE value not called", + }, { + label: label + "/StructE/ValueAddrEqual", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructE pointer called due to shallow copy transform", + }, { + label: label + "/StructE/PointerEqual", + x: &ts.StructE{X: "NotEqual"}, + y: &ts.StructE{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructE pointer called", + }, { + label: label + "/StructF/ValueInequal", + x: ts.StructF{X: "NotEqual"}, + y: ts.StructF{X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructF value not called", + }, { + label: label + "/StructF/PointerEqual", + x: &ts.StructF{X: "NotEqual"}, + y: &ts.StructF{X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructF pointer called", + }, { + label: label + "/StructA1/ValueEqual", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA value called with equal X field", + }, { + label: label + "/StructA1/ValueInequal", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", + }, { + label: label + "/StructA1/PointerEqual", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA value called with equal X field", + }, { + label: label + "/StructA1/PointerInequal", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", + }, { + label: label + "/StructB1/ValueEqual", + x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, + y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", + }, { + label: label + "/StructB1/ValueInequal", + x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", + }, { + label: label + "/StructB1/PointerEqual", + x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, + y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", + }, { + label: label + "/StructB1/PointerInequal", + x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", + }, { + label: label + "/StructC1/ValueEqual", + x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC1 value called", + }, { + label: label + "/StructC1/PointerEqual", + x: &ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructC1 pointer called", + }, { + label: label + "/StructD1/ValueInequal", + x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructD1 value not called", + }, { + label: label + "/StructD1/PointerAddrEqual", + x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructD1 pointer called due to shallow copy transform", + }, { + label: label + "/StructD1/PointerEqual", + x: &ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructD1 pointer called", + }, { + label: label + "/StructE1/ValueInequal", + x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructE1 value not called", + }, { + label: label + "/StructE1/ValueAddrEqual", + x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{addrTransform}, + wantEqual: true, + reason: "Equal method on StructE1 pointer called due to shallow copy transform", + }, { + label: label + "/StructE1/PointerEqual", + x: &ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructE1 pointer called", + }, { + label: label + "/StructF1/ValueInequal", + x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructF1 value not called", + }, { + label: label + "/StructF1/PointerEqual", + x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method on StructF1 pointer called", + }, { + label: label + "/StructA2/ValueEqual", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", + }, { + label: label + "/StructA2/ValueInequal", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", + }, { + label: label + "/StructA2/PointerEqual", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", + }, { + label: label + "/StructA2/PointerInequal", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", + }, { + label: label + "/StructB2/ValueEqual", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", + }, { + label: label + "/StructB2/ValueInequal", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", + }, { + label: label + "/StructB2/PointerEqual", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", + }, { + label: label + "/StructB2/PointerInequal", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", + }, { + label: label + "/StructC2/ValueEqual", + x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructC2 value due to forwarded StructC pointer", + }, { + label: label + "/StructC2/PointerEqual", + x: &ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructC2 pointer due to forwarded StructC pointer", + }, { + label: label + "/StructD2/ValueEqual", + x: ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructD2 value due to forwarded StructD pointer", + }, { + label: label + "/StructD2/PointerEqual", + x: &ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructD2 pointer due to forwarded StructD pointer", + }, { + label: label + "/StructE2/ValueEqual", + x: ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructE2 value due to forwarded StructE pointer", + }, { + label: label + "/StructE2/PointerEqual", + x: &ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructE2 pointer due to forwarded StructE pointer", + }, { + label: label + "/StructF2/ValueEqual", + x: ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructF2 value due to forwarded StructF pointer", + }, { + label: label + "/StructF2/PointerEqual", + x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, + wantEqual: true, + reason: "Equal method called on StructF2 pointer due to forwarded StructF pointer", + }, { + label: label + "/StructNo/Inequal", + x: ts.StructNo{X: "NotEqual"}, + y: ts.StructNo{X: "not_equal"}, + wantEqual: false, + reason: "Equal method not called since StructNo is not assignable to InterfaceA", + }, { + label: label + "/AssignA/Equal", + x: ts.AssignA(func() int { return 0 }), + y: ts.AssignA(func() int { return 1 }), + wantEqual: true, + reason: "Equal method called since named func is assignable to unnamed func", + }, { + label: label + "/AssignB/Equal", + x: ts.AssignB(struct{ A int }{0}), + y: ts.AssignB(struct{ A int }{1}), + wantEqual: true, + reason: "Equal method called since named struct is assignable to unnamed struct", + }, { + label: label + "/AssignC/Equal", + x: ts.AssignC(make(chan bool)), + y: ts.AssignC(make(chan bool)), + wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", + }, { + label: label + "/AssignD/Equal", + x: ts.AssignD(make(chan bool)), + y: ts.AssignD(make(chan bool)), + wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", }} } +type ( + CycleAlpha struct { + Name string + Bravos map[string]*CycleBravo + } + CycleBravo struct { + ID int + Name string + Mods int + Alphas map[string]*CycleAlpha + } +) + +func cycleTests() []test { + const label = "Cycle" + + type ( + P *P + S []S + M map[int]M + ) + + makeGraph := func() map[string]*CycleAlpha { + v := map[string]*CycleAlpha{ + "Foo": &CycleAlpha{ + Name: "Foo", + Bravos: map[string]*CycleBravo{ + "FooBravo": &CycleBravo{ + Name: "FooBravo", + ID: 101, + Mods: 100, + Alphas: map[string]*CycleAlpha{ + "Foo": nil, // cyclic reference + }, + }, + }, + }, + "Bar": &CycleAlpha{ + Name: "Bar", + Bravos: map[string]*CycleBravo{ + "BarBuzzBravo": &CycleBravo{ + Name: "BarBuzzBravo", + ID: 102, + Mods: 2, + Alphas: map[string]*CycleAlpha{ + "Bar": nil, // cyclic reference + "Buzz": nil, // cyclic reference + }, + }, + "BuzzBarBravo": &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + Mods: 0, + Alphas: map[string]*CycleAlpha{ + "Bar": nil, // cyclic reference + "Buzz": nil, // cyclic reference + }, + }, + }, + }, + "Buzz": &CycleAlpha{ + Name: "Buzz", + Bravos: map[string]*CycleBravo{ + "BarBuzzBravo": nil, // cyclic reference + "BuzzBarBravo": nil, // cyclic reference + }, + }, + } + v["Foo"].Bravos["FooBravo"].Alphas["Foo"] = v["Foo"] + v["Bar"].Bravos["BarBuzzBravo"].Alphas["Bar"] = v["Bar"] + v["Bar"].Bravos["BarBuzzBravo"].Alphas["Buzz"] = v["Buzz"] + v["Bar"].Bravos["BuzzBarBravo"].Alphas["Bar"] = v["Bar"] + v["Bar"].Bravos["BuzzBarBravo"].Alphas["Buzz"] = v["Buzz"] + v["Buzz"].Bravos["BarBuzzBravo"] = v["Bar"].Bravos["BarBuzzBravo"] + v["Buzz"].Bravos["BuzzBarBravo"] = v["Bar"].Bravos["BuzzBarBravo"] + return v + } + + var tests []test + type XY struct{ x, y interface{} } + for _, tt := range []struct { + label string + in XY + wantEqual bool + reason string + }{{ + label: "PointersEqual", + in: func() XY { + x := new(P) + *x = x + y := new(P) + *y = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node pointers", + }, { + label: "PointersInequal", + in: func() XY { + x := new(P) + *x = x + y1, y2 := new(P), new(P) + *y1 = y2 + *y2 = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double-node pointers", + }, { + label: "SlicesEqual", + in: func() XY { + x := S{nil} + x[0] = x + y := S{nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node slices", + }, { + label: "SlicesInequal", + in: func() XY { + x := S{nil} + x[0] = x + y1, y2 := S{nil}, S{nil} + y1[0] = y2 + y2[0] = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double node slices", + }, { + label: "MapsEqual", + in: func() XY { + x := M{0: nil} + x[0] = x + y := M{0: nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + reason: "equal pair of single-node maps", + }, { + label: "MapsInequal", + in: func() XY { + x := M{0: nil} + x[0] = x + y1, y2 := M{0: nil}, M{0: nil} + y1[0] = y2 + y2[0] = y1 + return XY{x, y1} + }(), + wantEqual: false, + reason: "inequal pair of single-node and double-node maps", + }, { + label: "GraphEqual", + in: XY{makeGraph(), makeGraph()}, + wantEqual: true, + reason: "graphs are equal since they have identical forms", + }, { + label: "GraphInequalZeroed", + in: func() XY { + x := makeGraph() + y := makeGraph() + y["Foo"].Bravos["FooBravo"].ID = 0 + y["Bar"].Bravos["BarBuzzBravo"].ID = 0 + y["Bar"].Bravos["BuzzBarBravo"].ID = 0 + return XY{x, y} + }(), + wantEqual: false, + reason: "graphs are inequal because the ID fields are different", + }, { + label: "GraphInequalStruct", + in: func() XY { + x := makeGraph() + y := makeGraph() + x["Buzz"].Bravos["BuzzBarBravo"] = &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + } + return XY{x, y} + }(), + wantEqual: false, + reason: "graphs are inequal because they differ on a map element", + }} { + tests = append(tests, test{ + label: label + "/" + tt.label, + x: tt.in.x, + y: tt.in.y, + wantEqual: tt.wantEqual, + reason: tt.reason, + }) + } + return tests +} + func project1Tests() []test { const label = "Project1" @@ -1585,7 +2594,7 @@ func project1Tests() []test { Target: "corporation", Immutable: &ts.GoatImmutable{ ID: "southbay", - State: (*pb.Goat_States)(intPtr(5)), + State: (*pb.Goat_States)(newInt(5)), Started: now, }, }, @@ -1613,13 +2622,13 @@ func project1Tests() []test { Immutable: &ts.EagleImmutable{ ID: "eagleID", Birthday: now, - MissingCall: (*pb.Eagle_MissingCalls)(intPtr(55)), + MissingCall: (*pb.Eagle_MissingCalls)(newInt(55)), }, } } return []test{{ - label: label, + label: label + "/PanicUnexported", x: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, @@ -1627,36 +2636,42 @@ func project1Tests() []test { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/ProtoEqual", x: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, y: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, - opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "simulated protobuf messages contain the same values", }, { - label: label, + label: label + "/ProtoInequal", x: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata2"}}, }}}, - opts: []cmp.Option{cmp.Comparer(pb.Equal)}, - wantDiff: "{teststructs.Eagle}.Slaps[4].Args:\n\t-: s\"metadata\"\n\t+: s\"metadata2\"\n", - }, { - label: label, - x: createEagle(), - y: createEagle(), - opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, - }, { - label: label, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "simulated protobuf messages contain different values", + }, { + label: label + "/Equal", + x: createEagle(), + y: createEagle(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "equal because values are the same", + }, { + label: label + "/Inequal", x: func() ts.Eagle { eg := createEagle() eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.ID = "southbay2" - eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(intPtr(6)) + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(newInt(6)) eg.Slaps[0].Immutable.MildSlap = false return eg }(), @@ -1666,23 +2681,9 @@ func project1Tests() []test { eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices = devs[:1] return eg }(), - opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, - wantDiff: ` -{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.ID: - -: "southbay2" - +: "southbay" -*{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.State: - -: testprotos.Goat_States(6) - +: testprotos.Goat_States(5) -{teststructs.Eagle}.Slaps[0].Immutable.MildSlap: - -: false - +: true -{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[1->?]: - -: "bar" - +: -{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[2->?]: - -: "baz" - +: `, + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "inequal because some values are different", }} } @@ -1742,17 +2743,20 @@ func project2Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported", x: createBatch(), y: createBatch(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, - x: createBatch(), - y: createBatch(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + label: label + "/Equal", + x: createBatch(), + y: createBatch(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + reason: "equal because identical values are compared", }, { - label: label, + label: label + "/InequalOrder", x: createBatch(), y: func() ts.GermBatch { gb := createBatch() @@ -1760,16 +2764,11 @@ func project2Tests() []test { s[0], s[1], s[2] = s[1], s[2], s[0] return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, - wantDiff: ` -{teststructs.GermBatch}.DirtyGerms[18][0->?]: - -: s"germ2" - +: -{teststructs.GermBatch}.DirtyGerms[18][?->2]: - -: - +: s"germ2"`, - }, { - label: label, + opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, + wantEqual: false, + reason: "inequal because slice contains elements in differing order", + }, { + label: label + "/EqualOrder", x: createBatch(), y: func() ts.GermBatch { gb := createBatch() @@ -1777,9 +2776,11 @@ func project2Tests() []test { s[0], s[1], s[2] = s[1], s[2], s[0] return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + reason: "equal because unordered slice is sorted using transformer", }, { - label: label, + label: label + "/Inequal", x: func() ts.GermBatch { gb := createBatch() delete(gb.DirtyGerms, 17) @@ -1792,20 +2793,9 @@ func project2Tests() []test { gb.GermStrain = 22 return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, - wantDiff: ` -{teststructs.GermBatch}.DirtyGerms[17]: - -: - +: []*testprotos.Germ{s"germ1"} -Sort({teststructs.GermBatch}.DirtyGerms[18])[2->?]: - -: s"germ4" - +: -{teststructs.GermBatch}.DishMap[1]: - -: (*teststructs.Dish)(nil) - +: &teststructs.Dish{err: &errors.errorString{s: "unexpected EOF"}} -{teststructs.GermBatch}.GermStrain: - -: 421 - +: 22`, + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: false, + reason: "inequal because some values are different", }} } @@ -1843,23 +2833,27 @@ func project3Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported1", x: createDirt(), y: createDirt(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/PanicUnexported2", x: createDirt(), y: createDirt(), opts: []cmp.Option{allowVisibility, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", }, { - label: label, - x: createDirt(), - y: createDirt(), - opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + label: label + "/Equal", + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", }, { - label: label, + label: label + "/Inequal", x: func() ts.Dirt { d := createDirt() d.SetTable(ts.CreateMockTable([]string{"a", "c"})) @@ -1874,23 +2868,9 @@ func project3Tests() []test { }) return d }(), - opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, - wantDiff: ` -{teststructs.Dirt}.table: - -: &teststructs.MockTable{state: []string{"a", "c"}} - +: &teststructs.MockTable{state: []string{"a", "b", "c"}} -{teststructs.Dirt}.Discord: - -: teststructs.DiscordState(554) - +: teststructs.DiscordState(500) -λ({teststructs.Dirt}.Proto): - -: s"blah" - +: s"proto" -{teststructs.Dirt}.wizard["albus"]: - -: s"dumbledore" - +: -{teststructs.Dirt}.wizard["harry"]: - -: s"potter" - +: s"otter"`, + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: false, + reason: "inequal because some values are different", }} } @@ -1933,23 +2913,27 @@ func project4Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported1", x: createCartel(), y: createCartel(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/PanicUnexported2", x: createCartel(), y: createCartel(), opts: []cmp.Option{allowVisibility, cmp.Comparer(pb.Equal)}, wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", }, { - label: label, - x: createCartel(), - y: createCartel(), - opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + label: label + "/Equal", + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", }, { - label: label, + label: label + "/Inequal", x: func() ts.Cartel { d := createCartel() var p1, p2 ts.Poison @@ -1967,22 +2951,58 @@ func project4Tests() []test { d.SetPublicMessage([]byte{1, 2, 4, 3, 5}) return d }(), - opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, - wantDiff: ` -{teststructs.Cartel}.Headquarter.subDivisions[0->?]: - -: "alpha" - +: -{teststructs.Cartel}.Headquarter.publicMessage[2]: - -: 0x03 - +: 0x04 -{teststructs.Cartel}.Headquarter.publicMessage[3]: - -: 0x04 - +: 0x03 -{teststructs.Cartel}.poisons[0].poisonType: - -: testprotos.PoisonType(1) - +: testprotos.PoisonType(5) -{teststructs.Cartel}.poisons[1->?]: - -: &teststructs.Poison{poisonType: testprotos.PoisonType(2), manufacturer: "acme2"} - +: `, + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: false, + reason: "inequal because some values are different", }} } + +// BenchmarkBytes benchmarks the performance of performing Equal or Diff on +// large slices of bytes. +func BenchmarkBytes(b *testing.B) { + // Create a list of PathFilters that never apply, but are evaluated. + const maxFilters = 5 + var filters cmp.Options + errorIface := reflect.TypeOf((*error)(nil)).Elem() + for i := 0; i <= maxFilters; i++ { + filters = append(filters, cmp.FilterPath(func(p cmp.Path) bool { + return p.Last().Type().AssignableTo(errorIface) // Never true + }, cmp.Ignore())) + } + + type benchSize struct { + label string + size int64 + } + for _, ts := range []benchSize{ + {"4KiB", 1 << 12}, + {"64KiB", 1 << 16}, + {"1MiB", 1 << 20}, + {"16MiB", 1 << 24}, + } { + bx := append(append(make([]byte, ts.size/2), 'x'), make([]byte, ts.size/2)...) + by := append(append(make([]byte, ts.size/2), 'y'), make([]byte, ts.size/2)...) + b.Run(ts.label, func(b *testing.B) { + // Iteratively add more filters that never apply, but are evaluated + // to measure the cost of simply evaluating each filter. + for i := 0; i <= maxFilters; i++ { + b.Run(fmt.Sprintf("EqualFilter%d", i), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(2 * ts.size) + for j := 0; j < b.N; j++ { + cmp.Equal(bx, by, filters[:i]...) + } + }) + } + for i := 0; i <= maxFilters; i++ { + b.Run(fmt.Sprintf("DiffFilter%d", i), func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(2 * ts.size) + for j := 0; j < b.N; j++ { + cmp.Diff(bx, by, filters[:i]...) + } + }) + } + }) + } +} diff --git a/cmp/example_reporter_test.go b/cmp/example_reporter_test.go new file mode 100644 index 0000000..bacba28 --- /dev/null +++ b/cmp/example_reporter_test.go @@ -0,0 +1,59 @@ +// Copyright 2019, 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 cmp_test + +import ( + "fmt" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// DiffReporter is a simple custom reporter that only records differences +// detected during comparison. +type DiffReporter struct { + path cmp.Path + diffs []string +} + +func (r *DiffReporter) PushStep(ps cmp.PathStep) { + r.path = append(r.path, ps) +} + +func (r *DiffReporter) Report(rs cmp.Result) { + if !rs.Equal() { + vx, vy := r.path.Last().Values() + r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)) + } +} + +func (r *DiffReporter) PopStep() { + r.path = r.path[:len(r.path)-1] +} + +func (r *DiffReporter) String() string { + return strings.Join(r.diffs, "\n") +} + +func ExampleReporter() { + x, y := MakeGatewayInfo() + + var r DiffReporter + cmp.Equal(x, y, cmp.Reporter(&r)) + fmt.Print(r.String()) + + // Output: + // {cmp_test.Gateway}.IPAddress: + // -: 192.168.0.1 + // +: 192.168.0.2 + // + // {cmp_test.Gateway}.Clients[4].IPAddress: + // -: 192.168.0.219 + // +: 192.168.0.221 + // + // {cmp_test.Gateway}.Clients[5->?]: + // -: {Hostname:americano IPAddress:192.168.0.188 LastSeen:2009-11-10 23:03:05 +0000 UTC} + // +: +} diff --git a/cmp/example_test.go b/cmp/example_test.go index 5507e0b..9968149 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -1,15 +1,17 @@ // 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. +// license that can be found in the LICENSE file. package cmp_test import ( "fmt" "math" + "net" "reflect" "sort" "strings" + "time" "github.com/google/go-cmp/cmp" ) @@ -18,108 +20,41 @@ import ( // fundamental options and filters and not in terms of what cool things you can // do with them since that overlaps with cmp/cmpopts. -// Use Diff for printing out human-readable errors for test cases comparing -// nested or structured data. +// Use Diff to print out a human-readable report of differences for tests +// comparing nested or structured data. func ExampleDiff_testing() { - // Code under test: - type ShipManifest struct { - Name string - Crew map[string]string - Androids int - Stolen bool - } - - // AddCrew tries to add the given crewmember to the manifest. - AddCrew := func(m *ShipManifest, name, title string) { - if m.Crew == nil { - m.Crew = make(map[string]string) - } - m.Crew[title] = name - } + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() - // Test function: - tests := []struct { - desc string - before *ShipManifest - name, title string - after *ShipManifest - }{ - { - desc: "add to empty", - before: &ShipManifest{}, - name: "Zaphod Beeblebrox", - title: "Galactic President", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - }, - { - desc: "add another", - before: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - name: "Trillian", - title: "Human", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - "Trillian": "Human", - }, - }, - }, - { - desc: "overwrite", - before: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - name: "Zaphod Beeblebrox", - title: "Just this guy, you know?", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Just this guy, you know?", - }, - }, - }, - } - - var t fakeT - for _, test := range tests { - AddCrew(test.before, test.name, test.title) - if diff := cmp.Diff(test.before, test.after); diff != "" { - t.Errorf("%s: after AddCrew, manifest differs: (-want +got)\n%s", test.desc, diff) - } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) } // Output: - // add to empty: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Galactic President"]: - // -: "Zaphod Beeblebrox" - // +: - // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: - // -: - // +: "Galactic President" - // - // add another: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Human"]: - // -: "Trillian" - // +: - // {*cmp_test.ShipManifest}.Crew["Trillian"]: - // -: - // +: "Human" - // - // overwrite: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Just this guy, you know?"]: - // -: "Zaphod Beeblebrox" - // +: - // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: - // -: "Galactic President" - // +: "Just this guy, you know?" + // MakeGatewayInfo() mismatch (-want +got): + // cmp_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: s"ffff0000", + // Clients: []cmp_test.Client{ + // ... // 2 identical elements + // {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"}, + // {Hostname: "espresso", IPAddress: s"192.168.0.121"}, + // { + // Hostname: "latte", + // - IPAddress: s"192.168.0.221", + // + IPAddress: s"192.168.0.219", + // LastSeen: s"2009-11-10 23:00:23 +0000 UTC", + // }, + // + { + // + Hostname: "americano", + // + IPAddress: s"192.168.0.188", + // + LastSeen: s"2009-11-10 23:03:05 +0000 UTC", + // + }, + // }, + // } } // Approximate equality for floats can be handled by defining a custom @@ -163,9 +98,9 @@ func ExampleOption_equalNaNs() { return (math.IsNaN(x) && math.IsNaN(y)) || x == y }) - x := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} - y := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} - z := []float64{1.0, math.NaN(), math.Pi, -0.0, +0.0} // Pi constant instead of E + x := []float64{1.0, math.NaN(), math.E, 0.0} + y := []float64{1.0, math.NaN(), math.E, 0.0} + z := []float64{1.0, math.NaN(), math.Pi, 0.0} // Pi constant instead of E fmt.Println(cmp.Equal(x, y, opt)) fmt.Println(cmp.Equal(y, z, opt)) @@ -281,10 +216,10 @@ func ExampleOption_sortedSlice() { type otherString string func (x otherString) Equal(y otherString) bool { - return strings.ToLower(string(x)) == strings.ToLower(string(y)) + return strings.EqualFold(string(x), string(y)) } -// If the Equal method defined on a type is not suitable, the type can be be +// If the Equal method defined on a type is not suitable, the type can be // dynamically transformed to be stripped of the Equal method (or any method // for that matter). func ExampleOption_avoidEqualMethod() { @@ -364,6 +299,78 @@ func ExampleOption_transformComplex() { // false } +type ( + Gateway struct { + SSID string + IPAddress net.IP + NetMask net.IPMask + Clients []Client + } + Client struct { + Hostname string + IPAddress net.IP + LastSeen time.Time + } +) + +func MakeGatewayInfo() (x, y Gateway) { + x = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 1), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 219), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }, { + Hostname: "americano", + IPAddress: net.IPv4(192, 168, 0, 188), + LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC), + }}, + } + y = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 2), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 221), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }}, + } + return x, y +} + +var t fakeT + type fakeT struct{} func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } diff --git a/cmp/export_panic.go b/cmp/export_panic.go new file mode 100644 index 0000000..ae851fe --- /dev/null +++ b/cmp/export_panic.go @@ -0,0 +1,16 @@ +// 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. + +//go:build purego +// +build purego + +package cmp + +import "reflect" + +const supportExporters = false + +func retrieveUnexportedField(reflect.Value, reflect.StructField, bool) reflect.Value { + panic("no support for forcibly accessing unexported fields") +} diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go new file mode 100644 index 0000000..e2c0f74 --- /dev/null +++ b/cmp/export_unsafe.go @@ -0,0 +1,36 @@ +// 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. + +//go:build !purego +// +build !purego + +package cmp + +import ( + "reflect" + "unsafe" +) + +const supportExporters = true + +// retrieveUnexportedField uses unsafe to forcibly retrieve any field from +// a struct such that the value has read-write permissions. +// +// The parent struct, v, must be addressable, while f must be a StructField +// describing the field to retrieve. If addr is false, +// then the returned value will be shallowed copied to be non-addressable. +func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) reflect.Value { + ve := reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() + if !addr { + // A field is addressable if and only if the struct is addressable. + // If the original parent value was not addressable, shallow copy the + // value to make it non-addressable to avoid leaking an implementation + // detail of how forcibly exporting a field works. + if ve.Kind() == reflect.Interface && ve.IsNil() { + return reflect.Zero(f.Type) + } + return reflect.ValueOf(ve.Interface()).Convert(f.Type) + } + return ve +} diff --git a/cmp/internal/diff/debug_disable.go b/cmp/internal/diff/debug_disable.go index fe98dcc..36062a6 100644 --- a/cmp/internal/diff/debug_disable.go +++ b/cmp/internal/diff/debug_disable.go @@ -1,7 +1,8 @@ // 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. +// license that can be found in the LICENSE file. +//go:build !cmp_debug // +build !cmp_debug package diff diff --git a/cmp/internal/diff/debug_enable.go b/cmp/internal/diff/debug_enable.go index 597b6ae..a3b97a1 100644 --- a/cmp/internal/diff/debug_enable.go +++ b/cmp/internal/diff/debug_enable.go @@ -1,7 +1,8 @@ // 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. +// license that can be found in the LICENSE file. +//go:build cmp_debug // +build cmp_debug package diff diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 6326465..a248e54 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. // Package diff implements an algorithm for producing edit-scripts. // The edit-script is a sequence of operations needed to transform one list @@ -12,6 +12,13 @@ // is more important than obtaining a minimal Levenshtein distance. package diff +import ( + "math/rand" + "time" + + "github.com/google/go-cmp/cmp/internal/flags" +) + // EditType represents a single operation within an edit-script. type EditType uint8 @@ -85,33 +92,44 @@ func (es EditScript) LenY() int { return len(es) - es.stats().NX } type EqualFunc func(ix int, iy int) Result // Result is the result of comparison. -// NSame is the number of sub-elements that are equal. -// NDiff is the number of sub-elements that are not equal. -type Result struct{ NSame, NDiff int } +// NumSame is the number of sub-elements that are equal. +// NumDiff is the number of sub-elements that are not equal. +type Result struct{ NumSame, NumDiff int } + +// BoolResult returns a Result that is either Equal or not Equal. +func BoolResult(b bool) Result { + if b { + return Result{NumSame: 1} // Equal, Similar + } else { + return Result{NumDiff: 2} // Not Equal, not Similar + } +} // Equal indicates whether the symbols are equal. Two symbols are equal -// if and only if NDiff == 0. If Equal, then they are also Similar. -func (r Result) Equal() bool { return r.NDiff == 0 } +// if and only if NumDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NumDiff == 0 } // Similar indicates whether two symbols are similar and may be represented // by using the Modified type. As a special case, we consider binary comparisons // (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. // -// The exact ratio of NSame to NDiff to determine similarity may change. +// The exact ratio of NumSame to NumDiff to determine similarity may change. func (r Result) Similar() bool { - // Use NSame+1 to offset NSame so that binary comparisons are similar. - return r.NSame+1 >= r.NDiff + // Use NumSame+1 to offset NumSame so that binary comparisons are similar. + return r.NumSame+1 >= r.NumDiff } +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + // Difference reports whether two lists of lengths nx and ny are equal // given the definition of equality provided as f. // // This function returns an edit-script, which is a sequence of operations // needed to convert one list into the other. The following invariants for // the edit-script are maintained: -// • eq == (es.Dist()==0) -// • nx == es.LenX() -// • ny == es.LenY() +// - eq == (es.Dist()==0) +// - nx == es.LenX() +// - ny == es.LenY() // // This algorithm is not guaranteed to be an optimal solution (i.e., one that // produces an edit-script with a minimal Levenshtein distance). This algorithm @@ -151,12 +169,13 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // A diagonal edge is equivalent to a matching symbol between both X and Y. // Invariants: - // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx - // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // - 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // - 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny // // In general: - // • fwdFrontier.X < revFrontier.X - // • fwdFrontier.Y < revFrontier.Y + // - fwdFrontier.X < revFrontier.X + // - fwdFrontier.Y < revFrontier.Y + // // Unless, it is time for the algorithm to terminate. fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} revPath := path{-1, point{nx, ny}, make(EditScript, 0)} @@ -168,37 +187,50 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // approximately the square-root of the search budget. searchBudget := 4 * (nx + ny) // O(n) + // Running the tests with the "cmp_debug" build tag prints a visualization + // of the algorithm running in real-time. This is educational for + // understanding how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + // The algorithm below is a greedy, meet-in-the-middle algorithm for // computing sub-optimal edit-scripts between two lists. // // The algorithm is approximately as follows: - // • Searching for differences switches back-and-forth between - // a search that starts at the beginning (the top-left corner), and - // a search that starts at the end (the bottom-right corner). The goal of - // the search is connect with the search from the opposite corner. - // • As we search, we build a path in a greedy manner, where the first - // match seen is added to the path (this is sub-optimal, but provides a - // decent result in practice). When matches are found, we try the next pair - // of symbols in the lists and follow all matches as far as possible. - // • When searching for matches, we search along a diagonal going through - // through the "frontier" point. If no matches are found, we advance the - // frontier towards the opposite corner. - // • This algorithm terminates when either the X coordinates or the - // Y coordinates of the forward and reverse frontier points ever intersect. - // + // - Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). + // The goal of the search is connect with the search + // from the opposite corner. + // - As we search, we build a path in a greedy manner, + // where the first match seen is added to the path (this is sub-optimal, + // but provides a decent result in practice). When matches are found, + // we try the next pair of symbols in the lists and follow all matches + // as far as possible. + // - When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, + // we advance the frontier towards the opposite corner. + // - This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. + // This algorithm is correct even if searching only in the forward direction // or in the reverse direction. We do both because it is commonly observed // that two lists commonly differ because elements were added to the front // or end of the other list. // - // Running the tests with the "cmp_debug" build tag prints a visualization - // of the algorithm running in real-time. This is educational for - // understanding how the algorithm works. See debug_enable.go. - f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) - for { + // Non-deterministically start with either the forward or reverse direction + // to introduce some deliberate instability so that we have the flexibility + // to change this algorithm in the future. + if flags.Deterministic || randBool { + goto forwardSearch + } else { + goto reverseSearch + } + +forwardSearch: + { // Forward search from the beginning. if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break + goto finishSearch } for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { // Search in a diagonal pattern for a match. @@ -233,10 +265,14 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { } else { fwdFrontier.Y++ } + goto reverseSearch + } +reverseSearch: + { // Reverse search from the end. if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break + goto finishSearch } for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { // Search in a diagonal pattern for a match. @@ -271,8 +307,10 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { } else { revFrontier.Y-- } + goto forwardSearch } +finishSearch: // Join the forward and reverse paths and then append the reverse path. fwdPath.connect(revPath.point, f) for i := len(revPath.es) - 1; i >= 0; i-- { @@ -354,6 +392,7 @@ type point struct{ X, Y int } func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } // zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// // [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] func zigzag(x int) int { if x&1 != 0 { diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go index 0752f4a..eacf072 100644 --- a/cmp/internal/diff/diff_test.go +++ b/cmp/internal/diff/diff_test.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package diff @@ -18,7 +18,7 @@ func TestDifference(t *testing.T) { // they can be used by the test author to indicate a missing symbol // in one of the lists. x, y string - want string + want string // '|' separated list of possible outputs }{{ x: "", y: "", @@ -30,7 +30,7 @@ func TestDifference(t *testing.T) { }, { x: "##", y: "# ", - want: ".X", + want: ".X|X.", }, { x: "a#", y: "A ", @@ -42,7 +42,7 @@ func TestDifference(t *testing.T) { }, { x: "# ", y: "##", - want: ".Y", + want: ".Y|Y.", }, { x: " #", y: "@#", @@ -142,7 +142,7 @@ func TestDifference(t *testing.T) { }, { x: "ABCAB BA ", y: " C BABAC", - want: "XX.X.Y..Y", + want: "XX.X.Y..Y|XX.Y.X..Y", }, { x: "# #### ###", y: "#y####yy###", @@ -158,7 +158,7 @@ func TestDifference(t *testing.T) { }, { x: "0 12z3x 456789 x x 0", y: "0y12Z3 y456789y y y0", - want: ".Y..M.XY......YXYXY.", + want: ".Y..M.XY......YXYXY.|.Y..M.XY......XYXYY.", }, { x: "0 2 4 6 8 ..................abXXcdEXF.ghXi", y: " 1 3 5 7 9..................AB CDE F.GH I", @@ -210,7 +210,7 @@ func TestDifference(t *testing.T) { }, { x: "0123456789 ", y: " 5678901234", - want: "XXXXX.....YYYYY", + want: "XXXXX.....YYYYY|YYYYY.....XXXXX", }, { x: "0123456789 ", y: " 4567890123", @@ -246,9 +246,14 @@ func TestDifference(t *testing.T) { x := strings.Replace(tt.x, " ", "", -1) y := strings.Replace(tt.y, " ", "", -1) es := testStrings(t, x, y) - if got := es.String(); got != tt.want { - t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, tt.want) + var want string + got := es.String() + for _, want = range strings.Split(tt.want, "|") { + if got == want { + return + } } + t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, want) }) } } @@ -387,9 +392,9 @@ func compareByte(x, y byte) (r Result) { } var ( - equalResult = Result{NDiff: 0} - similarResult = Result{NDiff: 1} - differentResult = Result{NDiff: 2} + equalResult = Result{NumDiff: 0} + similarResult = Result{NumDiff: 1} + differentResult = Result{NumDiff: 2} ) func TestResult(t *testing.T) { @@ -398,39 +403,39 @@ func TestResult(t *testing.T) { wantEqual bool wantSimilar bool }{ - // equalResult is equal since NDiff == 0, by definition of Equal method. + // equalResult is equal since NumDiff == 0, by definition of Equal method. {equalResult, true, true}, // similarResult is similar since it is a binary result where only one - // element was compared (i.e., Either NSame==1 or NDiff==1). + // element was compared (i.e., Either NumSame==1 or NumDiff==1). {similarResult, false, true}, // differentResult is different since there are enough differences that // it isn't even considered similar. {differentResult, false, false}, // Zero value is always equal. - {Result{NSame: 0, NDiff: 0}, true, true}, + {Result{NumSame: 0, NumDiff: 0}, true, true}, - // Binary comparisons (where NSame+NDiff == 1) are always similar. - {Result{NSame: 1, NDiff: 0}, true, true}, - {Result{NSame: 0, NDiff: 1}, false, true}, + // Binary comparisons (where NumSame+NumDiff == 1) are always similar. + {Result{NumSame: 1, NumDiff: 0}, true, true}, + {Result{NumSame: 0, NumDiff: 1}, false, true}, // More complex ratios. The exact ratio for similarity may change, // and may require updates to these test cases. - {Result{NSame: 1, NDiff: 1}, false, true}, - {Result{NSame: 1, NDiff: 2}, false, true}, - {Result{NSame: 1, NDiff: 3}, false, false}, - {Result{NSame: 2, NDiff: 1}, false, true}, - {Result{NSame: 2, NDiff: 2}, false, true}, - {Result{NSame: 2, NDiff: 3}, false, true}, - {Result{NSame: 3, NDiff: 1}, false, true}, - {Result{NSame: 3, NDiff: 2}, false, true}, - {Result{NSame: 3, NDiff: 3}, false, true}, - {Result{NSame: 1000, NDiff: 0}, true, true}, - {Result{NSame: 1000, NDiff: 1}, false, true}, - {Result{NSame: 1000, NDiff: 2}, false, true}, - {Result{NSame: 0, NDiff: 1000}, false, false}, - {Result{NSame: 1, NDiff: 1000}, false, false}, - {Result{NSame: 2, NDiff: 1000}, false, false}, + {Result{NumSame: 1, NumDiff: 1}, false, true}, + {Result{NumSame: 1, NumDiff: 2}, false, true}, + {Result{NumSame: 1, NumDiff: 3}, false, false}, + {Result{NumSame: 2, NumDiff: 1}, false, true}, + {Result{NumSame: 2, NumDiff: 2}, false, true}, + {Result{NumSame: 2, NumDiff: 3}, false, true}, + {Result{NumSame: 3, NumDiff: 1}, false, true}, + {Result{NumSame: 3, NumDiff: 2}, false, true}, + {Result{NumSame: 3, NumDiff: 3}, false, true}, + {Result{NumSame: 1000, NumDiff: 0}, true, true}, + {Result{NumSame: 1000, NumDiff: 1}, false, true}, + {Result{NumSame: 1000, NumDiff: 2}, false, true}, + {Result{NumSame: 0, NumDiff: 1000}, false, false}, + {Result{NumSame: 1, NumDiff: 1000}, false, false}, + {Result{NumSame: 2, NumDiff: 1000}, false, false}, } for _, tt := range tests { diff --git a/cmp/internal/flags/flags.go b/cmp/internal/flags/flags.go new file mode 100644 index 0000000..d8e459c --- /dev/null +++ b/cmp/internal/flags/flags.go @@ -0,0 +1,9 @@ +// Copyright 2019, 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 flags + +// Deterministic controls whether the output of Diff should be deterministic. +// This is only used for testing. +var Deterministic bool diff --git a/cmp/internal/function/func.go b/cmp/internal/function/func.go index 1b4c4c5..d127d43 100644 --- a/cmp/internal/function/func.go +++ b/cmp/internal/function/func.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. // Package function provides functionality for identifying function types. package function @@ -17,15 +17,19 @@ type funcType int const ( _ funcType = iota + tbFunc // func(T) bool ttbFunc // func(T, T) bool + trbFunc // func(T, R) bool tibFunc // func(T, I) bool trFunc // func(T) R - Equal = ttbFunc // func(T, T) bool - EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool - Transformer = trFunc // func(T) R - ValueFilter = ttbFunc // func(T, T) bool - Less = ttbFunc // func(T, T) bool + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool + ValuePredicate = tbFunc // func(T) bool + KeyValuePredicate = trbFunc // func(T, R) bool ) var boolType = reflect.TypeOf(true) @@ -37,10 +41,18 @@ func IsType(t reflect.Type, ft funcType) bool { } ni, no := t.NumIn(), t.NumOut() switch ft { + case tbFunc: // func(T) bool + if ni == 1 && no == 1 && t.Out(0) == boolType { + return true + } case ttbFunc: // func(T, T) bool if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { return true } + case trbFunc: // func(T, R) bool + if ni == 2 && no == 1 && t.Out(0) == boolType { + return true + } case tibFunc: // func(T, I) bool if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { return true diff --git a/cmp/internal/function/func_test.go b/cmp/internal/function/func_test.go index 61eeccd..f03ef45 100644 --- a/cmp/internal/function/func_test.go +++ b/cmp/internal/function/func_test.go @@ -1,6 +1,6 @@ // Copyright 2019, 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. +// license that can be found in the LICENSE file. package function diff --git a/cmp/internal/testprotos/protos.go b/cmp/internal/testprotos/protos.go index 120c8b0..81622d3 100644 --- a/cmp/internal/testprotos/protos.go +++ b/cmp/internal/testprotos/protos.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package testprotos diff --git a/cmp/internal/teststructs/foo1/foo.go b/cmp/internal/teststructs/foo1/foo.go new file mode 100644 index 0000000..c0882fb --- /dev/null +++ b/cmp/internal/teststructs/foo1/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, 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 foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/cmp/internal/teststructs/foo2/foo.go b/cmp/internal/teststructs/foo2/foo.go new file mode 100644 index 0000000..c0882fb --- /dev/null +++ b/cmp/internal/teststructs/foo2/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, 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 foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/cmp/internal/teststructs/project1.go b/cmp/internal/teststructs/project1.go index 1999e38..223d6ab 100644 --- a/cmp/internal/teststructs/project1.go +++ b/cmp/internal/teststructs/project1.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project2.go b/cmp/internal/teststructs/project2.go index 536592b..1616dd8 100644 --- a/cmp/internal/teststructs/project2.go +++ b/cmp/internal/teststructs/project2.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project3.go b/cmp/internal/teststructs/project3.go index 957d093..9e56dfa 100644 --- a/cmp/internal/teststructs/project3.go +++ b/cmp/internal/teststructs/project3.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project4.go b/cmp/internal/teststructs/project4.go index 49920f2..a09aba2 100644 --- a/cmp/internal/teststructs/project4.go +++ b/cmp/internal/teststructs/project4.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/structs.go b/cmp/internal/teststructs/structs.go index 6b4d2a7..bfd2de8 100644 --- a/cmp/internal/teststructs/structs.go +++ b/cmp/internal/teststructs/structs.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/value/format.go b/cmp/internal/value/format.go deleted file mode 100644 index bafb2d1..0000000 --- a/cmp/internal/value/format.go +++ /dev/null @@ -1,280 +0,0 @@ -// 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 value provides functionality for reflect.Value types. -package value - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "unicode" -) - -var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() - -// Format formats the value v as a string. -// -// This is similar to fmt.Sprintf("%+v", v) except this: -// * Prints the type unless it can be elided -// * Avoids printing struct fields that are zero -// * Prints a nil-slice as being nil, not empty -// * Prints map entries in deterministic order -func Format(v reflect.Value, conf FormatConfig) string { - conf.printType = true - conf.followPointers = true - conf.realPointers = true - return formatAny(v, conf, visited{}) -} - -type FormatConfig struct { - UseStringer bool // Should the String method be used if available? - printType bool // Should we print the type before the value? - PrintPrimitiveType bool // Should we print the type of primitives? - followPointers bool // Should we recursively follow pointers? - realPointers bool // Should we print the real address of pointers? -} - -func formatAny(v reflect.Value, conf FormatConfig, m visited) string { - // TODO: Should this be a multi-line printout in certain situations? - - if !v.IsValid() { - return "" - } - if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() { - if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { - return "" - } - - const stringerPrefix = "s" // Indicates that the String method was used - s := v.Interface().(fmt.Stringer).String() - return stringerPrefix + formatString(s) - } - - switch v.Kind() { - case reflect.Bool: - return formatPrimitive(v.Type(), v.Bool(), conf) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return formatPrimitive(v.Type(), v.Int(), conf) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { - // Unnamed uints are usually bytes or words, so use hexadecimal. - return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) - } - return formatPrimitive(v.Type(), v.Uint(), conf) - case reflect.Float32, reflect.Float64: - return formatPrimitive(v.Type(), v.Float(), conf) - case reflect.Complex64, reflect.Complex128: - return formatPrimitive(v.Type(), v.Complex(), conf) - case reflect.String: - return formatPrimitive(v.Type(), formatString(v.String()), conf) - case reflect.UnsafePointer, reflect.Chan, reflect.Func: - return formatPointer(v, conf) - case reflect.Ptr: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("(%v)(nil)", v.Type()) - } - return "" - } - if m.Visit(v) || !conf.followPointers { - return formatPointer(v, conf) - } - return "&" + formatAny(v.Elem(), conf, m) - case reflect.Interface: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - return formatAny(v.Elem(), conf, m) - case reflect.Slice: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - fallthrough - case reflect.Array: - var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface - for i := 0; i < v.Len(); i++ { - vi := v.Index(i) - if vi.CanAddr() { // Check for recursive elements - p := vi.Addr() - if m.Visit(p) { - subConf := conf - subConf.printType = true - ss = append(ss, "*"+formatPointer(p, subConf)) - continue - } - } - ss = append(ss, formatAny(vi, subConf, m)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Map: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - if m.Visit(v) { - return formatPointer(v, conf) - } - - var ss []string - keyConf, valConf := conf, conf - keyConf.printType = v.Type().Key().Kind() == reflect.Interface - keyConf.followPointers = false - valConf.printType = v.Type().Elem().Kind() == reflect.Interface - for _, k := range SortKeys(v.MapKeys()) { - sk := formatAny(k, keyConf, m) - sv := formatAny(v.MapIndex(k), valConf, m) - ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Struct: - var ss []string - subConf := conf - subConf.printType = true - for i := 0; i < v.NumField(); i++ { - vv := v.Field(i) - if isZero(vv) { - continue // Elide zero value fields - } - name := v.Type().Field(i).Name - subConf.UseStringer = conf.UseStringer - s := formatAny(vv, subConf, m) - ss = append(ss, fmt.Sprintf("%s: %s", name, s)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - default: - panic(fmt.Sprintf("%v kind not handled", v.Kind())) - } -} - -func formatString(s string) string { - // Use quoted string if it the same length as a raw string literal. - // Otherwise, attempt to use the raw string form. - qs := strconv.Quote(s) - if len(qs) == 1+len(s)+1 { - return qs - } - - // Disallow newlines to ensure output is a single line. - // Only allow printable runes for readability purposes. - rawInvalid := func(r rune) bool { - return r == '`' || r == '\n' || !unicode.IsPrint(r) - } - if strings.IndexFunc(s, rawInvalid) < 0 { - return "`" + s + "`" - } - return qs -} - -func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string { - if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") { - return fmt.Sprintf("%v(%v)", t, v) - } - return fmt.Sprintf("%v", v) -} - -func formatPointer(v reflect.Value, conf FormatConfig) string { - p := v.Pointer() - if !conf.realPointers { - p = 0 // For deterministic printing purposes - } - s := formatHex(uint64(p)) - if conf.printType { - return fmt.Sprintf("(%v)(%s)", v.Type(), s) - } - return s -} - -func formatHex(u uint64) string { - var f string - switch { - case u <= 0xff: - f = "0x%02x" - case u <= 0xffff: - f = "0x%04x" - case u <= 0xffffff: - f = "0x%06x" - case u <= 0xffffffff: - f = "0x%08x" - case u <= 0xffffffffff: - f = "0x%010x" - case u <= 0xffffffffffff: - f = "0x%012x" - case u <= 0xffffffffffffff: - f = "0x%014x" - case u <= 0xffffffffffffffff: - f = "0x%016x" - } - return fmt.Sprintf(f, u) -} - -// isZero reports whether v is the zero value. -// This does not rely on Interface and so can be used on unexported fields. -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Bool: - return v.Bool() == false - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Complex64, reflect.Complex128: - return v.Complex() == 0 - case reflect.String: - return v.String() == "" - case reflect.UnsafePointer: - return v.Pointer() == 0 - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: - return v.IsNil() - case reflect.Array: - for i := 0; i < v.Len(); i++ { - if !isZero(v.Index(i)) { - return false - } - } - return true - case reflect.Struct: - for i := 0; i < v.NumField(); i++ { - if !isZero(v.Field(i)) { - return false - } - } - return true - } - return false -} - -type visited map[Pointer]bool - -func (m visited) Visit(v reflect.Value) bool { - p := PointerOf(v) - visited := m[p] - m[p] = true - return visited -} diff --git a/cmp/internal/value/format_test.go b/cmp/internal/value/format_test.go deleted file mode 100644 index d676da2..0000000 --- a/cmp/internal/value/format_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 value - -import ( - "bytes" - "io" - "reflect" - "testing" -) - -func TestFormat(t *testing.T) { - type key struct { - a int - b string - c chan bool - } - - tests := []struct { - in interface{} - want string - }{{ - in: []int{}, - want: "[]int{}", - }, { - in: []int(nil), - want: "[]int(nil)", - }, { - in: []int{1, 2, 3, 4, 5}, - want: "[]int{1, 2, 3, 4, 5}", - }, { - in: []interface{}{1, true, "hello", struct{ A, B int }{1, 2}}, - want: "[]interface {}{1, true, \"hello\", struct { A int; B int }{A: 1, B: 2}}", - }, { - in: []struct{ A, B int }{{1, 2}, {0, 4}, {}}, - want: "[]struct { A int; B int }{{A: 1, B: 2}, {B: 4}, {}}", - }, { - in: map[*int]string{new(int): "hello"}, - want: "map[*int]string{0x00: \"hello\"}", - }, { - in: map[key]string{{}: "hello"}, - want: "map[value.key]string{{}: \"hello\"}", - }, { - in: map[key]string{{a: 5, b: "key", c: make(chan bool)}: "hello"}, - want: "map[value.key]string{{a: 5, b: \"key\", c: (chan bool)(0x00)}: \"hello\"}", - }, { - in: map[io.Reader]string{new(bytes.Reader): "hello"}, - want: "map[io.Reader]string{(*bytes.Reader)(0x00): \"hello\"}", - }, { - in: func() interface{} { - var a = []interface{}{nil} - a[0] = a - return a - }(), - want: "[]interface {}{[]interface {}{*(*interface {})(0x00)}}", - }, { - in: func() interface{} { - type A *A - var a A - a = &a - return a - }(), - want: "&(value.A)(0x00)", - }, { - in: func() interface{} { - type A map[*A]A - a := make(A) - a[&a] = a - return a - }(), - want: "value.A{0x00: 0x00}", - }, { - in: func() interface{} { - var a [2]interface{} - a[0] = &a - return a - }(), - want: "[2]interface {}{&[2]interface {}{(*[2]interface {})(0x00), interface {}(nil)}, interface {}(nil)}", - }} - - for i, tt := range tests { - // Intentionally retrieve the value through an unexported field to - // ensure the format logic does not depend on read-write access - // to the reflect.Value. - v := reflect.ValueOf(struct{ x interface{} }{tt.in}).Field(0) - got := formatAny(v, FormatConfig{UseStringer: true, printType: true, followPointers: true}, visited{}) - if got != tt.want { - t.Errorf("test %d, Format():\ngot %q\nwant %q", i, got, tt.want) - } - } -} diff --git a/cmp/internal/value/name.go b/cmp/internal/value/name.go new file mode 100644 index 0000000..7b498bb --- /dev/null +++ b/cmp/internal/value/name.go @@ -0,0 +1,164 @@ +// Copyright 2020, 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 value + +import ( + "reflect" + "strconv" +) + +var anyType = reflect.TypeOf((*interface{})(nil)).Elem() + +// TypeString is nearly identical to reflect.Type.String, +// but has an additional option to specify that full type names be used. +func TypeString(t reflect.Type, qualified bool) string { + return string(appendTypeName(nil, t, qualified, false)) +} + +func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { + // BUG: Go reflection provides no way to disambiguate two named types + // of the same name and within the same package, + // but declared within the namespace of different functions. + + // Use the "any" alias instead of "interface{}" for better readability. + if t == anyType { + return append(b, "any"...) + } + + // Named type. + if t.Name() != "" { + if qualified && t.PkgPath() != "" { + b = append(b, '"') + b = append(b, t.PkgPath()...) + b = append(b, '"') + b = append(b, '.') + b = append(b, t.Name()...) + } else { + b = append(b, t.String()...) + } + return b + } + + // Unnamed type. + switch k := t.Kind(); k { + case reflect.Bool, reflect.String, reflect.UnsafePointer, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + b = append(b, k.String()...) + case reflect.Chan: + if t.ChanDir() == reflect.RecvDir { + b = append(b, "<-"...) + } + b = append(b, "chan"...) + if t.ChanDir() == reflect.SendDir { + b = append(b, "<-"...) + } + b = append(b, ' ') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Func: + if !elideFunc { + b = append(b, "func"...) + } + b = append(b, '(') + for i := 0; i < t.NumIn(); i++ { + if i > 0 { + b = append(b, ", "...) + } + if i == t.NumIn()-1 && t.IsVariadic() { + b = append(b, "..."...) + b = appendTypeName(b, t.In(i).Elem(), qualified, false) + } else { + b = appendTypeName(b, t.In(i), qualified, false) + } + } + b = append(b, ')') + switch t.NumOut() { + case 0: + // Do nothing + case 1: + b = append(b, ' ') + b = appendTypeName(b, t.Out(0), qualified, false) + default: + b = append(b, " ("...) + for i := 0; i < t.NumOut(); i++ { + if i > 0 { + b = append(b, ", "...) + } + b = appendTypeName(b, t.Out(i), qualified, false) + } + b = append(b, ')') + } + case reflect.Struct: + b = append(b, "struct{ "...) + for i := 0; i < t.NumField(); i++ { + if i > 0 { + b = append(b, "; "...) + } + sf := t.Field(i) + if !sf.Anonymous { + if qualified && sf.PkgPath != "" { + b = append(b, '"') + b = append(b, sf.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, sf.Name...) + b = append(b, ' ') + } + b = appendTypeName(b, sf.Type, qualified, false) + if sf.Tag != "" { + b = append(b, ' ') + b = strconv.AppendQuote(b, string(sf.Tag)) + } + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + case reflect.Slice, reflect.Array: + b = append(b, '[') + if k == reflect.Array { + b = strconv.AppendUint(b, uint64(t.Len()), 10) + } + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Map: + b = append(b, "map["...) + b = appendTypeName(b, t.Key(), qualified, false) + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Ptr: + b = append(b, '*') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Interface: + b = append(b, "interface{ "...) + for i := 0; i < t.NumMethod(); i++ { + if i > 0 { + b = append(b, "; "...) + } + m := t.Method(i) + if qualified && m.PkgPath != "" { + b = append(b, '"') + b = append(b, m.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, m.Name...) + b = appendTypeName(b, m.Type, qualified, true) + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + default: + panic("invalid kind: " + k.String()) + } + return b +} diff --git a/cmp/internal/value/name_test.go b/cmp/internal/value/name_test.go new file mode 100644 index 0000000..c177e72 --- /dev/null +++ b/cmp/internal/value/name_test.go @@ -0,0 +1,144 @@ +// Copyright 2020, 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 value + +import ( + "reflect" + "strings" + "testing" +) + +type Named struct{} + +var pkgPath = reflect.TypeOf(Named{}).PkgPath() + +func TestTypeString(t *testing.T) { + tests := []struct { + in interface{} + want string + }{{ + in: bool(false), + want: "bool", + }, { + in: int(0), + want: "int", + }, { + in: float64(0), + want: "float64", + }, { + in: string(""), + want: "string", + }, { + in: Named{}, + want: "$PackagePath.Named", + }, { + in: (chan Named)(nil), + want: "chan $PackagePath.Named", + }, { + in: (<-chan Named)(nil), + want: "<-chan $PackagePath.Named", + }, { + in: (chan<- Named)(nil), + want: "chan<- $PackagePath.Named", + }, { + in: (func())(nil), + want: "func()", + }, { + in: (func(Named))(nil), + want: "func($PackagePath.Named)", + }, { + in: (func() Named)(nil), + want: "func() $PackagePath.Named", + }, { + in: (func(int, Named) (int, error))(nil), + want: "func(int, $PackagePath.Named) (int, error)", + }, { + in: (func(...Named))(nil), + want: "func(...$PackagePath.Named)", + }, { + in: struct{}{}, + want: "struct{}", + }, { + in: struct{ Named }{}, + want: "struct{ $PackagePath.Named }", + }, { + in: struct { + Named `tag` + }{}, + want: "struct{ $PackagePath.Named \"tag\" }", + }, { + in: struct{ Named Named }{}, + want: "struct{ Named $PackagePath.Named }", + }, { + in: struct { + Named Named `tag` + }{}, + want: "struct{ Named $PackagePath.Named \"tag\" }", + }, { + in: struct { + Int int + Named Named + }{}, + want: "struct{ Int int; Named $PackagePath.Named }", + }, { + in: struct { + _ int + x Named + }{}, + want: "struct{ $FieldPrefix._ int; $FieldPrefix.x $PackagePath.Named }", + }, { + in: []Named(nil), + want: "[]$PackagePath.Named", + }, { + in: []*Named(nil), + want: "[]*$PackagePath.Named", + }, { + in: [10]Named{}, + want: "[10]$PackagePath.Named", + }, { + in: [10]*Named{}, + want: "[10]*$PackagePath.Named", + }, { + in: map[string]string(nil), + want: "map[string]string", + }, { + in: map[Named]Named(nil), + want: "map[$PackagePath.Named]$PackagePath.Named", + }, { + in: (*Named)(nil), + want: "*$PackagePath.Named", + }, { + in: (*interface{})(nil), + want: "*any", + }, { + in: (*interface{ Read([]byte) (int, error) })(nil), + want: "*interface{ Read([]uint8) (int, error) }", + }, { + in: (*interface { + F1() + F2(Named) + F3() Named + F4(int, Named) (int, error) + F5(...Named) + })(nil), + want: "*interface{ F1(); F2($PackagePath.Named); F3() $PackagePath.Named; F4(int, $PackagePath.Named) (int, error); F5(...$PackagePath.Named) }", + }} + + for _, tt := range tests { + typ := reflect.TypeOf(tt.in) + wantShort := tt.want + wantShort = strings.Replace(wantShort, "$PackagePath", "value", -1) + wantShort = strings.Replace(wantShort, "$FieldPrefix.", "", -1) + if gotShort := TypeString(typ, false); gotShort != wantShort { + t.Errorf("TypeString(%v, false) mismatch:\ngot: %v\nwant: %v", typ, gotShort, wantShort) + } + wantQualified := tt.want + wantQualified = strings.Replace(wantQualified, "$PackagePath", `"`+pkgPath+`"`, -1) + wantQualified = strings.Replace(wantQualified, "$FieldPrefix", `"`+pkgPath+`"`, -1) + if gotQualified := TypeString(typ, true); gotQualified != wantQualified { + t.Errorf("TypeString(%v, true) mismatch:\ngot: %v\nwant: %v", typ, gotQualified, wantQualified) + } + } +} diff --git a/cmp/internal/value/pointer_purego.go b/cmp/internal/value/pointer_purego.go index 0a01c47..1a71bfc 100644 --- a/cmp/internal/value/pointer_purego.go +++ b/cmp/internal/value/pointer_purego.go @@ -1,7 +1,8 @@ // Copyright 2018, 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. +// license that can be found in the LICENSE file. +//go:build purego // +build purego package value @@ -21,3 +22,13 @@ func PointerOf(v reflect.Value) Pointer { // assumes that the GC implementation does not use a moving collector. return Pointer{v.Pointer(), v.Type()} } + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == 0 +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return p.p +} diff --git a/cmp/internal/value/pointer_unsafe.go b/cmp/internal/value/pointer_unsafe.go index da134ae..16e6860 100644 --- a/cmp/internal/value/pointer_unsafe.go +++ b/cmp/internal/value/pointer_unsafe.go @@ -1,7 +1,8 @@ // Copyright 2018, 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. +// license that can be found in the LICENSE file. +//go:build !purego // +build !purego package value @@ -24,3 +25,13 @@ func PointerOf(v reflect.Value) Pointer { // which is necessary if the GC ever uses a moving collector. return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} } + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == nil +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return uintptr(p.p) +} diff --git a/cmp/internal/value/sort.go b/cmp/internal/value/sort.go index 938f646..98533b0 100644 --- a/cmp/internal/value/sort.go +++ b/cmp/internal/value/sort.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package value @@ -19,7 +19,7 @@ func SortKeys(vs []reflect.Value) []reflect.Value { } // Sort the map keys. - sort.Slice(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) + sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) // Deduplicate keys (fails for NaNs). vs2 := vs[:1] @@ -42,6 +42,8 @@ func isLess(x, y reflect.Value) bool { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return x.Uint() < y.Uint() case reflect.Float32, reflect.Float64: + // NOTE: This does not sort -0 as less than +0 + // since Go maps treat -0 and +0 as equal keys. fx, fy := x.Float(), y.Float() return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) case reflect.Complex64, reflect.Complex128: diff --git a/cmp/internal/value/sort_test.go b/cmp/internal/value/sort_test.go index fb86fce..26222d6 100644 --- a/cmp/internal/value/sort_test.go +++ b/cmp/internal/value/sort_test.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package value_test diff --git a/cmp/options.go b/cmp/options.go index a9306b4..1f9ca9c 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmp @@ -29,11 +29,12 @@ type Option interface { // An Options is returned only if multiple comparers or transformers // can apply simultaneously and will only contain values of those types // or sub-Options containing values of those types. - filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption + filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption } // applicableOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer +// +// Fundamental: ignore | validator | *comparer | *transformer // Grouping: Options type applicableOption interface { Option @@ -43,7 +44,8 @@ type applicableOption interface { } // coreOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer +// +// Fundamental: ignore | validator | *comparer | *transformer // Filters: *pathFilter | *valuesFilter type coreOption interface { Option @@ -63,19 +65,19 @@ func (core) isCore() {} // on all individual options held within. type Options []Option -func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { +func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { for _, opt := range opts { - switch opt := opt.filter(s, vx, vy, t); opt.(type) { + switch opt := opt.filter(s, t, vx, vy); opt.(type) { case ignore: return ignore{} // Only ignore can short-circuit evaluation - case invalid: - out = invalid{} // Takes precedence over comparer or transformer + case validator: + out = validator{} // Takes precedence over comparer or transformer case *comparer, *transformer, Options: switch out.(type) { case nil: out = opt - case invalid: - // Keep invalid + case validator: + // Keep validator case *comparer, *transformer, Options: out = Options{out, opt} // Conflicting comparers or transformers } @@ -106,6 +108,11 @@ func (opts Options) String() string { // FilterPath returns a new Option where opt is only evaluated if filter f // returns true for the current Path in the value tree. // +// This filter is called even if a slice element or map entry is missing and +// provides an opportunity to ignore such cases. The filter function must be +// symmetric such that the filter result is identical regardless of whether the +// missing value is from x or y. +// // The option passed in may be an Ignore, Transformer, Comparer, Options, or // a previously filtered Option. func FilterPath(f func(Path) bool, opt Option) Option { @@ -124,9 +131,9 @@ type pathFilter struct { opt Option } -func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { +func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { if f.fnc(s.curPath) { - return f.opt.filter(s, vx, vy, t) + return f.opt.filter(s, t, vx, vy) } return nil } @@ -137,8 +144,9 @@ func (f pathFilter) String() string { // FilterValues returns a new Option where opt is only evaluated if filter f, // which is a function of the form "func(T, T) bool", returns true for the -// current pair of values being compared. If the type of the values is not -// assignable to T, then this filter implicitly returns false. +// current pair of values being compared. If either value is invalid or +// the type of the values is not assignable to T, then this filter implicitly +// returns false. // // The filter function must be // symmetric (i.e., agnostic to the order of the inputs) and @@ -170,12 +178,12 @@ type valuesFilter struct { opt Option } -func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { - if !vx.IsValid() || !vy.IsValid() { - return invalid{} +func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { + return nil } if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { - return f.opt.filter(s, vx, vy, t) + return f.opt.filter(s, t, vx, vy) } return nil } @@ -192,18 +200,53 @@ func Ignore() Option { return ignore{} } type ignore struct{ core } func (ignore) isFiltered() bool { return false } -func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } -func (ignore) apply(_ *state, _, _ reflect.Value) { return } +func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} } +func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportByIgnore) } func (ignore) String() string { return "Ignore()" } -// invalid is a sentinel Option type to indicate that some options could not -// be evaluated due to unexported fields. -type invalid struct{ core } +// validator is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields, missing slice elements, or +// missing map entries. Both values are validator only for unexported fields. +type validator struct{ core } + +func (validator) filter(_ *state, _ reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vy.IsValid() { + return validator{} + } + if !vx.CanInterface() || !vy.CanInterface() { + return validator{} + } + return nil +} +func (validator) apply(s *state, vx, vy reflect.Value) { + // Implies missing slice element or map entry. + if !vx.IsValid() || !vy.IsValid() { + s.report(vx.IsValid() == vy.IsValid(), 0) + return + } + + // Unable to Interface implies unexported field without visibility access. + if !vx.CanInterface() || !vy.CanInterface() { + help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported" + var name string + if t := s.curPath.Index(-2).Type(); t.Name() != "" { + // Named type with unexported fields. + name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType + if _, ok := reflect.New(t).Interface().(error); ok { + help = "consider using cmpopts.EquateErrors to compare error values" + } + } else { + // Unnamed type with unexported fields. Derive PkgPath from field. + var pkgPath string + for i := 0; i < t.NumField() && pkgPath == ""; i++ { + pkgPath = t.Field(i).PkgPath + } + name = fmt.Sprintf("%q.(%v)", pkgPath, t.String()) // e.g., "path/to/package".(struct { a int }) + } + panic(fmt.Sprintf("cannot handle unexported field at %#v:\n\t%v\n%s", s.curPath, name, help)) + } -func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } -func (invalid) apply(s *state, _, _ reflect.Value) { - const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" - panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) + panic("not reachable") } // identRx represents a valid identifier according to the Go specification. @@ -260,9 +303,9 @@ type transformer struct { func (tr *transformer) isFiltered() bool { return tr.typ != nil } -func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption { +func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) applicableOption { for i := len(s.curPath) - 1; i >= 0; i-- { - if t, ok := s.curPath[i].(*transform); !ok { + if t, ok := s.curPath[i].(Transform); !ok { break // Hit most recent non-Transform step } else if tr == t.trans { return nil // Cannot directly use same Transform @@ -275,14 +318,11 @@ func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) appl } func (tr *transformer) apply(s *state, vx, vy reflect.Value) { - // Update path before calling the Transformer so that dynamic checks - // will use the updated path. - s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) - defer s.curPath.pop() - - vx = s.callTRFunc(tr.fnc, vx) - vy = s.callTRFunc(tr.fnc, vy) - s.compareAny(vx, vy) + step := Transform{&transform{pathStep{typ: tr.fnc.Type().Out(0)}, tr}} + vvx := s.callTRFunc(tr.fnc, vx, step) + vvy := s.callTRFunc(tr.fnc, vy, step) + step.vx, step.vy = vvx, vvy + s.compareAny(step) } func (tr transformer) String() string { @@ -298,9 +338,9 @@ func (tr transformer) String() string { // both implement T. // // The equality function must be: -// • Symmetric: equal(x, y) == equal(y, x) -// • Deterministic: equal(x, y) == equal(x, y) -// • Pure: equal(x, y) does not modify x or y +// - Symmetric: equal(x, y) == equal(y, x) +// - Deterministic: equal(x, y) == equal(x, y) +// - Pure: equal(x, y) does not modify x or y func Comparer(f interface{}) Option { v := reflect.ValueOf(f) if !function.IsType(v.Type(), function.Equal) || v.IsNil() { @@ -321,7 +361,7 @@ type comparer struct { func (cm *comparer) isFiltered() bool { return cm.typ != nil } -func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { +func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { if cm.typ == nil || t.AssignableTo(cm.typ) { return cm } @@ -330,16 +370,15 @@ func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applica func (cm *comparer) apply(s *state, vx, vy reflect.Value) { eq := s.callTTBFunc(cm.fnc, vx, vy) - s.report(eq, vx, vy) + s.report(eq, reportByFunc) } func (cm comparer) String() string { return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) } -// AllowUnexported returns an Option that forcibly allows operations on -// unexported fields in certain structs, which are specified by passing in a -// value of each struct type. +// Exporter returns an Option that specifies whether Equal is allowed to +// introspect into the unexported fields of certain struct types. // // Users of this option must understand that comparing on unexported fields // from external packages is not safe since changes in the internal @@ -348,7 +387,7 @@ func (cm comparer) String() string { // defined in an internal package where the semantic meaning of an unexported // field is in the control of the user. // -// For some cases, a custom Comparer should be used instead that defines +// In many cases, a custom Comparer should be used instead that defines // equality as a function of the public API of a type rather than the underlying // unexported implementation. // @@ -363,10 +402,24 @@ func (cm comparer) String() string { // // In other cases, the cmpopts.IgnoreUnexported option can be used to ignore // all unexported fields on specified struct types. -func AllowUnexported(types ...interface{}) Option { - if !supportAllowUnexported { - panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") +func Exporter(f func(reflect.Type) bool) Option { + if !supportExporters { + panic("Exporter is not supported on purego builds") } + return exporter(f) +} + +type exporter func(reflect.Type) bool + +func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// AllowUnexported returns an Options that allows Equal to forcibly introspect +// unexported fields of the specified struct types. +// +// See Exporter for the proper use of this option. +func AllowUnexported(types ...interface{}) Option { m := make(map[reflect.Type]bool) for _, typ := range types { t := reflect.TypeOf(typ) @@ -375,32 +428,97 @@ func AllowUnexported(types ...interface{}) Option { } m[t] = true } - return visibleStructs(m) + return exporter(func(t reflect.Type) bool { return m[t] }) } -type visibleStructs map[reflect.Type]bool +// Result represents the comparison result for a single node and +// is provided by cmp when calling Report (see Reporter). +type Result struct { + _ [0]func() // Make Result incomparable + flags resultFlags +} -func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { - panic("not implemented") +// Equal reports whether the node was determined to be equal or not. +// As a special case, ignored nodes are considered equal. +func (r Result) Equal() bool { + return r.flags&(reportEqual|reportByIgnore) != 0 +} + +// ByIgnore reports whether the node is equal because it was ignored. +// This never reports true if Equal reports false. +func (r Result) ByIgnore() bool { + return r.flags&reportByIgnore != 0 +} + +// ByMethod reports whether the Equal method determined equality. +func (r Result) ByMethod() bool { + return r.flags&reportByMethod != 0 +} + +// ByFunc reports whether a Comparer function determined equality. +func (r Result) ByFunc() bool { + return r.flags&reportByFunc != 0 } -// reporter is an Option that configures how differences are reported. -type reporter interface { - // TODO: Not exported yet. +// ByCycle reports whether a reference cycle was detected. +func (r Result) ByCycle() bool { + return r.flags&reportByCycle != 0 +} + +type resultFlags uint + +const ( + _ resultFlags = (1 << iota) / 2 + + reportEqual + reportUnequal + reportByIgnore + reportByMethod + reportByFunc + reportByCycle +) + +// Reporter is an Option that can be passed to Equal. When Equal traverses +// the value trees, it calls PushStep as it descends into each node in the +// tree and PopStep as it ascend out of the node. The leaves of the tree are +// either compared (determined to be equal or not equal) or ignored and reported +// as such by calling the Report method. +func Reporter(r interface { + // PushStep is called when a tree-traversal operation is performed. + // The PathStep itself is only valid until the step is popped. + // The PathStep.Values are valid for the duration of the entire traversal + // and must not be mutated. // - // Perhaps add PushStep and PopStep and change Report to only accept - // a PathStep instead of the full-path? Adding a PushStep and PopStep makes - // it clear that we are traversing the value tree in a depth-first-search - // manner, which has an effect on how values are printed. + // Equal always calls PushStep at the start to provide an operation-less + // PathStep used to report the root values. + // + // Within a slice, the exact set of inserted, removed, or modified elements + // is unspecified and may change in future implementations. + // The entries of a map are iterated through in an unspecified order. + PushStep(PathStep) + + // Report is called exactly once on leaf nodes to report whether the + // comparison identified the node as equal, unequal, or ignored. + // A leaf node is one that is immediately preceded by and followed by + // a pair of PushStep and PopStep calls. + Report(Result) + + // PopStep ascends back up the value tree. + // There is always a matching pop call for every push call. + PopStep() +}) Option { + return reporter{r} +} - Option +type reporter struct{ reporterIface } +type reporterIface interface { + PushStep(PathStep) + Report(Result) + PopStep() +} - // Report is called for every comparison made and will be provided with - // the two values being compared, the equality result, and the - // current path in the value tree. It is possible for x or y to be an - // invalid reflect.Value if one of the values is non-existent; - // which is possible with maps and slices. - Report(x, y reflect.Value, eq bool, p Path) +func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") } // normalizeOption normalizes the input options such that all Options groups diff --git a/cmp/options_test.go b/cmp/options_test.go index f5d2b4a..c7d45f3 100644 --- a/cmp/options_test.go +++ b/cmp/options_test.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmp @@ -128,7 +128,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, &defaultReporter{}}, + args: []interface{}{func(Path) bool { return true }, Reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterPath", @@ -137,7 +137,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -170,7 +170,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, &defaultReporter{}}, + args: []interface{}{func(int, int) bool { return true }, Reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -179,7 +179,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }} diff --git a/cmp/path.go b/cmp/path.go index 49b622b..a0a5885 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -1,6 +1,6 @@ // 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. +// license that can be found in the LICENSE file. package cmp @@ -10,82 +10,56 @@ import ( "strings" "unicode" "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" ) -type ( - // Path is a list of PathSteps describing the sequence of operations to get - // from some root type to the current position in the value tree. - // The first Path element is always an operation-less PathStep that exists - // simply to identify the initial type. - // - // When traversing structs with embedded structs, the embedded struct will - // always be accessed as a field before traversing the fields of the - // embedded struct themselves. That is, an exported field from the - // embedded struct will never be accessed directly from the parent struct. - Path []PathStep - - // PathStep is a union-type for specific operations to traverse - // a value's tree structure. Users of this package never need to implement - // these types as values of this type will be returned by this package. - PathStep interface { - String() string - Type() reflect.Type // Resulting type after performing the path step - isPathStep() - } +// Path is a list of PathSteps describing the sequence of operations to get +// from some root type to the current position in the value tree. +// The first Path element is always an operation-less PathStep that exists +// simply to identify the initial type. +// +// When traversing structs with embedded structs, the embedded struct will +// always be accessed as a field before traversing the fields of the +// embedded struct themselves. That is, an exported field from the +// embedded struct will never be accessed directly from the parent struct. +type Path []PathStep - // StructField represents a struct field access on a field called Name. - StructField interface { - PathStep - Name() string - Index() int - isStructField() - } - // SliceIndex is an index operation on a slice or array at some index Key. - SliceIndex interface { - PathStep - Key() int // May return -1 if in a split state - - // SplitKeys returns the indexes for indexing into slices in the - // x and y values, respectively. These indexes may differ due to the - // insertion or removal of an element in one of the slices, causing - // all of the indexes to be shifted. If an index is -1, then that - // indicates that the element does not exist in the associated slice. - // - // Key is guaranteed to return -1 if and only if the indexes returned - // by SplitKeys are not the same. SplitKeys will never return -1 for - // both indexes. - SplitKeys() (x int, y int) - - isSliceIndex() - } - // MapIndex is an index operation on a map at some index Key. - MapIndex interface { - PathStep - Key() reflect.Value - isMapIndex() - } - // Indirect represents pointer indirection on the parent type. - Indirect interface { - PathStep - isIndirect() - } - // TypeAssertion represents a type assertion on an interface. - TypeAssertion interface { - PathStep - isTypeAssertion() - } - // Transform is a transformation from the parent type to the current type. - Transform interface { - PathStep - Name() string - Func() reflect.Value +// PathStep is a union-type for specific operations to traverse +// a value's tree structure. Users of this package never need to implement +// these types as values of this type will be returned by this package. +// +// Implementations of this interface are +// StructField, SliceIndex, MapIndex, Indirect, TypeAssertion, and Transform. +type PathStep interface { + String() string - // Option returns the originally constructed Transformer option. - // The == operator can be used to detect the exact option used. - Option() Option + // Type is the resulting type after performing the path step. + Type() reflect.Type - isTransform() - } + // Values is the resulting values after performing the path step. + // The type of each valid value is guaranteed to be identical to Type. + // + // In some cases, one or both may be invalid or have restrictions: + // - For StructField, both are not interface-able if the current field + // is unexported and the struct type is not explicitly permitted by + // an Exporter to traverse unexported fields. + // - For SliceIndex, one may be invalid if an element is missing from + // either the x or y slice. + // - For MapIndex, one may be invalid if an entry is missing from + // either the x or y map. + // + // The provided values must not be mutated. + Values() (vx, vy reflect.Value) +} + +var ( + _ PathStep = StructField{} + _ PathStep = SliceIndex{} + _ PathStep = MapIndex{} + _ PathStep = Indirect{} + _ PathStep = TypeAssertion{} + _ PathStep = Transform{} ) func (pa *Path) push(s PathStep) { @@ -120,11 +94,12 @@ func (pa Path) Index(i int) PathStep { // The simplified path only contains struct field accesses. // // For example: +// // MyMap.MySlices.MyField func (pa Path) String() string { var ss []string for _, s := range pa { - if _, ok := s.(*structField); ok { + if _, ok := s.(StructField); ok { ss = append(ss, s.String()) } } @@ -134,6 +109,7 @@ func (pa Path) String() string { // GoString returns the path to a specific node using Go syntax. // // For example: +// // (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField func (pa Path) GoString() string { var ssPre, ssPost []string @@ -144,13 +120,13 @@ func (pa Path) GoString() string { nextStep = pa[i+1] } switch s := s.(type) { - case *indirect: + case Indirect: numIndirect++ pPre, pPost := "(", ")" switch nextStep.(type) { - case *indirect: + case Indirect: continue // Next step is indirection, so let them batch up - case *structField: + case StructField: numIndirect-- // Automatic indirection on struct fields case nil: pPre, pPost = "", "" // Last step; no need for parenthesis @@ -161,19 +137,10 @@ func (pa Path) GoString() string { } numIndirect = 0 continue - case *transform: + case Transform: ssPre = append(ssPre, s.trans.name+"(") ssPost = append(ssPost, ")") continue - case *typeAssertion: - // As a special-case, elide type assertions on anonymous types - // since they are typically generated dynamically and can be very - // verbose. For example, some transforms return interface{} because - // of Go's lack of generics, but typically take in and return the - // exact same concrete type. - if s.Type().PkgPath() == "" { - continue - } } ssPost = append(ssPost, s.String()) } @@ -183,62 +150,74 @@ func (pa Path) GoString() string { return strings.Join(ssPre, "") + strings.Join(ssPost, "") } -type ( - pathStep struct { - typ reflect.Type - } - - structField struct { - pathStep - name string - idx int - - // These fields are used for forcibly accessing an unexported field. - // pvx, pvy, and field are only valid if unexported is true. - unexported bool - force bool // Forcibly allow visibility - pvx, pvy reflect.Value // Parent values - field reflect.StructField // Field information - } - sliceIndex struct { - pathStep - xkey, ykey int - } - mapIndex struct { - pathStep - key reflect.Value - } - indirect struct { - pathStep - } - typeAssertion struct { - pathStep - } - transform struct { - pathStep - trans *transformer - } -) +type pathStep struct { + typ reflect.Type + vx, vy reflect.Value +} -func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } func (ps pathStep) String() string { if ps.typ == nil { return "" } - s := ps.typ.String() + s := value.TypeString(ps.typ, false) if s == "" || strings.ContainsAny(s, "{}\n") { return "root" // Type too simple or complex to print } return fmt.Sprintf("{%s}", s) } -func (ps pathStep) isPathStep() {} -func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) } -func (sf structField) Name() string { return sf.name } -func (sf structField) Index() int { return sf.idx } -func (sf structField) isStructField() {} +// StructField represents a struct field access on a field called Name. +type StructField struct{ *structField } +type structField struct { + pathStep + name string + idx int + + // These fields are used for forcibly accessing an unexported field. + // pvx, pvy, and field are only valid if unexported is true. + unexported bool + mayForce bool // Forcibly allow visibility + paddr bool // Was parent addressable? + pvx, pvy reflect.Value // Parent values (always addressable) + field reflect.StructField // Field information +} + +func (sf StructField) Type() reflect.Type { return sf.typ } +func (sf StructField) Values() (vx, vy reflect.Value) { + if !sf.unexported { + return sf.vx, sf.vy // CanInterface reports true + } + + // Forcibly obtain read-write access to an unexported struct field. + if sf.mayForce { + vx = retrieveUnexportedField(sf.pvx, sf.field, sf.paddr) + vy = retrieveUnexportedField(sf.pvy, sf.field, sf.paddr) + return vx, vy // CanInterface reports true + } + return sf.vx, sf.vy // CanInterface reports false +} +func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } + +// Name is the field name. +func (sf StructField) Name() string { return sf.name } + +// Index is the index of the field in the parent struct type. +// See reflect.Type.Field. +func (sf StructField) Index() int { return sf.idx } + +// SliceIndex is an index operation on a slice or array at some index Key. +type SliceIndex struct{ *sliceIndex } +type sliceIndex struct { + pathStep + xkey, ykey int + isSlice bool // False for reflect.Array +} -func (si sliceIndex) String() string { +func (si SliceIndex) Type() reflect.Type { return si.typ } +func (si SliceIndex) Values() (vx, vy reflect.Value) { return si.vx, si.vy } +func (si SliceIndex) String() string { switch { case si.xkey == si.ykey: return fmt.Sprintf("[%d]", si.xkey) @@ -253,39 +232,146 @@ func (si sliceIndex) String() string { return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) } } -func (si sliceIndex) Key() int { + +// Key is the index key; it may return -1 if in a split state +func (si SliceIndex) Key() int { if si.xkey != si.ykey { return -1 } return si.xkey } -func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey } -func (si sliceIndex) isSliceIndex() {} -func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } -func (mi mapIndex) Key() reflect.Value { return mi.key } -func (mi mapIndex) isMapIndex() {} +// SplitKeys are the indexes for indexing into slices in the +// x and y values, respectively. These indexes may differ due to the +// insertion or removal of an element in one of the slices, causing +// all of the indexes to be shifted. If an index is -1, then that +// indicates that the element does not exist in the associated slice. +// +// Key is guaranteed to return -1 if and only if the indexes returned +// by SplitKeys are not the same. SplitKeys will never return -1 for +// both indexes. +func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } -func (in indirect) String() string { return "*" } -func (in indirect) isIndirect() {} +// MapIndex is an index operation on a map at some index Key. +type MapIndex struct{ *mapIndex } +type mapIndex struct { + pathStep + key reflect.Value +} -func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } -func (ta typeAssertion) isTypeAssertion() {} +func (mi MapIndex) Type() reflect.Type { return mi.typ } +func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } +func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } -func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } -func (tf transform) Name() string { return tf.trans.name } -func (tf transform) Func() reflect.Value { return tf.trans.fnc } -func (tf transform) Option() Option { return tf.trans } -func (tf transform) isTransform() {} +// Key is the value of the map key. +func (mi MapIndex) Key() reflect.Value { return mi.key } -var ( - _ PathStep = StructField(structField{}) - _ PathStep = SliceIndex(sliceIndex{}) - _ PathStep = MapIndex(mapIndex{}) - _ PathStep = Indirect(indirect{}) - _ PathStep = TypeAssertion(typeAssertion{}) - _ PathStep = Transform(transform{}) -) +// Indirect represents pointer indirection on the parent type. +type Indirect struct{ *indirect } +type indirect struct { + pathStep +} + +func (in Indirect) Type() reflect.Type { return in.typ } +func (in Indirect) Values() (vx, vy reflect.Value) { return in.vx, in.vy } +func (in Indirect) String() string { return "*" } + +// TypeAssertion represents a type assertion on an interface. +type TypeAssertion struct{ *typeAssertion } +type typeAssertion struct { + pathStep +} + +func (ta TypeAssertion) Type() reflect.Type { return ta.typ } +func (ta TypeAssertion) Values() (vx, vy reflect.Value) { return ta.vx, ta.vy } +func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", value.TypeString(ta.typ, false)) } + +// Transform is a transformation from the parent type to the current type. +type Transform struct{ *transform } +type transform struct { + pathStep + trans *transformer +} + +func (tf Transform) Type() reflect.Type { return tf.typ } +func (tf Transform) Values() (vx, vy reflect.Value) { return tf.vx, tf.vy } +func (tf Transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } + +// Name is the name of the Transformer. +func (tf Transform) Name() string { return tf.trans.name } + +// Func is the function pointer to the transformer function. +func (tf Transform) Func() reflect.Value { return tf.trans.fnc } + +// Option returns the originally constructed Transformer option. +// The == operator can be used to detect the exact option used. +func (tf Transform) Option() Option { return tf.trans } + +// pointerPath represents a dual-stack of pointers encountered when +// recursively traversing the x and y values. This data structure supports +// detection of cycles and determining whether the cycles are equal. +// In Go, cycles can occur via pointers, slices, and maps. +// +// The pointerPath uses a map to represent a stack; where descension into a +// pointer pushes the address onto the stack, and ascension from a pointer +// pops the address from the stack. Thus, when traversing into a pointer from +// reflect.Ptr, reflect.Slice element, or reflect.Map, we can detect cycles +// by checking whether the pointer has already been visited. The cycle detection +// uses a separate stack for the x and y values. +// +// If a cycle is detected we need to determine whether the two pointers +// should be considered equal. The definition of equality chosen by Equal +// requires two graphs to have the same structure. To determine this, both the +// x and y values must have a cycle where the previous pointers were also +// encountered together as a pair. +// +// Semantically, this is equivalent to augmenting Indirect, SliceIndex, and +// MapIndex with pointer information for the x and y values. +// Suppose px and py are two pointers to compare, we then search the +// Path for whether px was ever encountered in the Path history of x, and +// similarly so with py. If either side has a cycle, the comparison is only +// equal if both px and py have a cycle resulting from the same PathStep. +// +// Using a map as a stack is more performant as we can perform cycle detection +// in O(1) instead of O(N) where N is len(Path). +type pointerPath struct { + // mx is keyed by x pointers, where the value is the associated y pointer. + mx map[value.Pointer]value.Pointer + // my is keyed by y pointers, where the value is the associated x pointer. + my map[value.Pointer]value.Pointer +} + +func (p *pointerPath) Init() { + p.mx = make(map[value.Pointer]value.Pointer) + p.my = make(map[value.Pointer]value.Pointer) +} + +// Push indicates intent to descend into pointers vx and vy where +// visited reports whether either has been seen before. If visited before, +// equal reports whether both pointers were encountered together. +// Pop must be called if and only if the pointers were never visited. +// +// The pointers vx and vy must be a reflect.Ptr, reflect.Slice, or reflect.Map +// and be non-nil. +func (p pointerPath) Push(vx, vy reflect.Value) (equal, visited bool) { + px := value.PointerOf(vx) + py := value.PointerOf(vy) + _, ok1 := p.mx[px] + _, ok2 := p.my[py] + if ok1 || ok2 { + equal = p.mx[px] == py && p.my[py] == px // Pointers paired together + return equal, true + } + p.mx[px] = py + p.my[py] = px + return false, false +} + +// Pop ascends from pointers vx and vy. +func (p pointerPath) Pop(vx, vy reflect.Value) { + delete(p.mx, value.PointerOf(vx)) + delete(p.my, value.PointerOf(vy)) +} // isExported reports whether the identifier is exported. func isExported(id string) bool { diff --git a/cmp/report.go b/cmp/report.go new file mode 100644 index 0000000..f43cd12 --- /dev/null +++ b/cmp/report.go @@ -0,0 +1,54 @@ +// 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 cmp + +// defaultReporter implements the reporter interface. +// +// As Equal serially calls the PushStep, Report, and PopStep methods, the +// defaultReporter constructs a tree-based representation of the compared value +// and the result of each comparison (see valueNode). +// +// When the String method is called, the FormatDiff method transforms the +// valueNode tree into a textNode tree, which is a tree-based representation +// of the textual output (see textNode). +// +// Lastly, the textNode.String method produces the final report as a string. +type defaultReporter struct { + root *valueNode + curr *valueNode +} + +func (r *defaultReporter) PushStep(ps PathStep) { + r.curr = r.curr.PushStep(ps) + if r.root == nil { + r.root = r.curr + } +} +func (r *defaultReporter) Report(rs Result) { + r.curr.Report(rs) +} +func (r *defaultReporter) PopStep() { + r.curr = r.curr.PopStep() +} + +// String provides a full report of the differences detected as a structured +// literal in pseudo-Go syntax. String may only be called after the entire tree +// has been traversed. +func (r *defaultReporter) String() string { + assert(r.root != nil && r.curr == nil) + if r.root.NumDiff == 0 { + return "" + } + ptrs := new(pointerReferences) + text := formatOptions{}.FormatDiff(r.root, ptrs) + resolveReferences(text) + return text.String() +} + +func assert(ok bool) { + if !ok { + panic("assertion failure") + } +} diff --git a/cmp/report_compare.go b/cmp/report_compare.go new file mode 100644 index 0000000..2050bf6 --- /dev/null +++ b/cmp/report_compare.go @@ -0,0 +1,433 @@ +// Copyright 2019, 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 cmp + +import ( + "fmt" + "reflect" +) + +// numContextRecords is the number of surrounding equal records to print. +const numContextRecords = 2 + +type diffMode byte + +const ( + diffUnknown diffMode = 0 + diffIdentical diffMode = ' ' + diffRemoved diffMode = '-' + diffInserted diffMode = '+' +) + +type typeMode int + +const ( + // emitType always prints the type. + emitType typeMode = iota + // elideType never prints the type. + elideType + // autoType prints the type only for composite kinds + // (i.e., structs, slices, arrays, and maps). + autoType +) + +type formatOptions struct { + // DiffMode controls the output mode of FormatDiff. + // + // If diffUnknown, then produce a diff of the x and y values. + // If diffIdentical, then emit values as if they were equal. + // If diffRemoved, then only emit x values (ignoring y values). + // If diffInserted, then only emit y values (ignoring x values). + DiffMode diffMode + + // TypeMode controls whether to print the type for the current node. + // + // As a general rule of thumb, we always print the type of the next node + // after an interface, and always elide the type of the next node after + // a slice or map node. + TypeMode typeMode + + // formatValueOptions are options specific to printing reflect.Values. + formatValueOptions +} + +func (opts formatOptions) WithDiffMode(d diffMode) formatOptions { + opts.DiffMode = d + return opts +} +func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { + opts.TypeMode = t + return opts +} +func (opts formatOptions) WithVerbosity(level int) formatOptions { + opts.VerbosityLevel = level + opts.LimitVerbosity = true + return opts +} +func (opts formatOptions) verbosity() uint { + switch { + case opts.VerbosityLevel < 0: + return 0 + case opts.VerbosityLevel > 16: + return 16 // some reasonable maximum to avoid shift overflow + default: + return uint(opts.VerbosityLevel) + } +} + +const maxVerbosityPreset = 6 + +// verbosityPreset modifies the verbosity settings given an index +// between 0 and maxVerbosityPreset, inclusive. +func verbosityPreset(opts formatOptions, i int) formatOptions { + opts.VerbosityLevel = int(opts.verbosity()) + 2*i + if i > 0 { + opts.AvoidStringer = true + } + if i >= maxVerbosityPreset { + opts.PrintAddresses = true + opts.QualifiedNames = true + } + return opts +} + +// FormatDiff converts a valueNode tree into a textNode tree, where the later +// is a textual representation of the differences detected in the former. +func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { + if opts.DiffMode == diffIdentical { + opts = opts.WithVerbosity(1) + } else if opts.verbosity() < 3 { + opts = opts.WithVerbosity(3) + } + + // Check whether we have specialized formatting for this node. + // This is not necessary, but helpful for producing more readable outputs. + if opts.CanFormatDiffSlice(v) { + return opts.FormatDiffSlice(v) + } + + var parentKind reflect.Kind + if v.parent != nil && v.parent.TransformerName == "" { + parentKind = v.parent.Type.Kind() + } + + // For leaf nodes, format the value based on the reflect.Values alone. + // As a special case, treat equal []byte as a leaf nodes. + isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType + isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0 + if v.MaxDepth == 0 || isEqualBytes { + switch opts.DiffMode { + case diffUnknown, diffIdentical: + // Format Equal. + if v.NumDiff == 0 { + outx := opts.FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.FormatValue(v.ValueY, parentKind, ptrs) + if v.NumIgnored > 0 && v.NumSame == 0 { + return textEllipsis + } else if outx.Len() < outy.Len() { + return outx + } else { + return outy + } + } + + // Format unequal. + assert(opts.DiffMode == diffUnknown) + var list textList + outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) + outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) + outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: '-', Value: outx}) + } + if outy != nil { + list = append(list, textRecord{Diff: '+', Value: outy}) + } + return opts.WithTypeMode(emitType).FormatType(v.Type, list) + case diffRemoved: + return opts.FormatValue(v.ValueX, parentKind, ptrs) + case diffInserted: + return opts.FormatValue(v.ValueY, parentKind, ptrs) + default: + panic("invalid diff mode") + } + } + + // Register slice element to support cycle detection. + if parentKind == reflect.Slice { + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) + defer ptrs.Pop() + defer func() { out = wrapTrunkReferences(ptrRefs, out) }() + } + + // Descend into the child value node. + if v.TransformerName != "" { + out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} + return opts.FormatType(v.Type, out) + } else { + switch k := v.Type.Kind(); k { + case reflect.Struct, reflect.Array, reflect.Slice: + out = opts.formatDiffList(v.Records, k, ptrs) + out = opts.FormatType(v.Type, out) + case reflect.Map: + // Register map to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.formatDiffList(v.Records, k, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = opts.FormatType(v.Type, out) + case reflect.Ptr: + // Register pointer to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.FormatDiff(v.Value, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = &textWrap{Prefix: "&", Value: out} + case reflect.Interface: + out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + default: + panic(fmt.Sprintf("%v cannot have children", k)) + } + return out + } +} + +func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { + // Derive record name based on the data structure kind. + var name string + var formatKey func(reflect.Value) string + switch k { + case reflect.Struct: + name = "field" + opts = opts.WithTypeMode(autoType) + formatKey = func(v reflect.Value) string { return v.String() } + case reflect.Slice, reflect.Array: + name = "element" + opts = opts.WithTypeMode(elideType) + formatKey = func(reflect.Value) string { return "" } + case reflect.Map: + name = "entry" + opts = opts.WithTypeMode(elideType) + formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } + } + + maxLen := -1 + if opts.LimitVerbosity { + if opts.DiffMode == diffIdentical { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + } else { + maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... + } + opts.VerbosityLevel-- + } + + // Handle unification. + switch opts.DiffMode { + case diffIdentical, diffRemoved, diffInserted: + var list textList + var deferredEllipsis bool // Add final "..." to indicate records were dropped + for _, r := range recs { + if len(list) == maxLen { + deferredEllipsis = true + break + } + + // Elide struct fields that are zero value. + if k == reflect.Struct { + var isZero bool + switch opts.DiffMode { + case diffIdentical: + isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero() + case diffRemoved: + isZero = r.Value.ValueX.IsZero() + case diffInserted: + isZero = r.Value.ValueY.IsZero() + } + if isZero { + continue + } + } + // Elide ignored nodes. + if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 { + deferredEllipsis = !(k == reflect.Slice || k == reflect.Array) + if !deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + continue + } + if out := opts.FormatDiff(r.Value, ptrs); out != nil { + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + } + if deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case diffUnknown: + default: + panic("invalid diff mode") + } + + // Handle differencing. + var numDiffs int + var list textList + var keys []reflect.Value // invariant: len(list) == len(keys) + groups := coalesceAdjacentRecords(name, recs) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Handle equal records. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing records to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 { + if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numLo++ + } + for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 { + break + } + numHi++ + } + if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 { + numHi++ // Avoid pointless coalescing of a single equal record + } + + // Format the equal values. + for _, r := range recs[:numLo] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + for _, r := range recs[numEqual-numHi : numEqual] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + recs = recs[numEqual:] + continue + } + + // Handle unequal records. + for _, r := range recs[:ds.NumDiff()] { + switch { + case opts.CanFormatDiffSlice(r.Value): + out := opts.FormatDiffSlice(r.Value) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + case r.Value.NumChildren == r.Value.MaxDepth: + outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i) + outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) + } + if outx != nil { + list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) + keys = append(keys, r.Key) + } + if outy != nil { + list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) + keys = append(keys, r.Key) + } + default: + out := opts.FormatDiff(r.Value, ptrs) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) + } + } + recs = recs[ds.NumDiff():] + numDiffs += ds.NumDiff() + } + if maxGroup.IsZero() { + assert(len(recs) == 0) + } else { + list.AppendEllipsis(maxGroup) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + assert(len(list) == len(keys)) + + // For maps, the default formatting logic uses fmt.Stringer which may + // produce ambiguous output. Avoid calling String to disambiguate. + if k == reflect.Map { + var ambiguous bool + seenKeys := map[string]reflect.Value{} + for i, currKey := range keys { + if currKey.IsValid() { + strKey := list[i].Key + prevKey, seen := seenKeys[strKey] + if seen && prevKey.CanInterface() && currKey.CanInterface() { + ambiguous = prevKey.Interface() != currKey.Interface() + if ambiguous { + break + } + } + seenKeys[strKey] = currKey + } + } + if ambiguous { + for i, k := range keys { + if k.IsValid() { + list[i].Key = formatMapKey(k, true, ptrs) + } + } + } + } + + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} +} + +// coalesceAdjacentRecords coalesces the list of records into groups of +// adjacent equal, or unequal counts. +func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) { + var prevCase int // Arbitrary index into which case last occurred + lastStats := func(i int) *diffStats { + if prevCase != i { + groups = append(groups, diffStats{Name: name}) + prevCase = i + } + return &groups[len(groups)-1] + } + for _, r := range recs { + switch rv := r.Value; { + case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0: + lastStats(1).NumIgnored++ + case rv.NumDiff == 0: + lastStats(1).NumIdentical++ + case rv.NumDiff > 0 && !rv.ValueY.IsValid(): + lastStats(2).NumRemoved++ + case rv.NumDiff > 0 && !rv.ValueX.IsValid(): + lastStats(2).NumInserted++ + default: + lastStats(2).NumModified++ + } + } + return groups +} diff --git a/cmp/report_references.go b/cmp/report_references.go new file mode 100644 index 0000000..be31b33 --- /dev/null +++ b/cmp/report_references.go @@ -0,0 +1,264 @@ +// Copyright 2020, 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 cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/value" +) + +const ( + pointerDelimPrefix = "⟪" + pointerDelimSuffix = "⟫" +) + +// formatPointer prints the address of the pointer. +func formatPointer(p value.Pointer, withDelims bool) string { + v := p.Uintptr() + if flags.Deterministic { + v = 0xdeadf00f // Only used for stable testing purposes + } + if withDelims { + return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix + } + return formatHex(uint64(v)) +} + +// pointerReferences is a stack of pointers visited so far. +type pointerReferences [][2]value.Pointer + +func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { + if deref && vx.IsValid() { + vx = vx.Addr() + } + if deref && vy.IsValid() { + vy = vy.Addr() + } + switch d { + case diffUnknown, diffIdentical: + pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} + case diffRemoved: + pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} + case diffInserted: + pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} + } + *ps = append(*ps, pp) + return pp +} + +func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { + p = value.PointerOf(v) + for _, pp := range *ps { + if p == pp[0] || p == pp[1] { + return p, true + } + } + *ps = append(*ps, [2]value.Pointer{p, p}) + return p, false +} + +func (ps *pointerReferences) Pop() { + *ps = (*ps)[:len(*ps)-1] +} + +// trunkReferences is metadata for a textNode indicating that the sub-tree +// represents the value for either pointer in a pair of references. +type trunkReferences struct{ pp [2]value.Pointer } + +// trunkReference is metadata for a textNode indicating that the sub-tree +// represents the value for the given pointer reference. +type trunkReference struct{ p value.Pointer } + +// leafReference is metadata for a textNode indicating that the value is +// truncated as it refers to another part of the tree (i.e., a trunk). +type leafReference struct{ p value.Pointer } + +func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { + switch { + case pp[0].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} + case pp[1].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + case pp[0] == pp[1]: + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + default: + return &textWrap{Value: s, Metadata: trunkReferences{pp}} + } +} +func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} +} +func makeLeafReference(p value.Pointer, printAddress bool) textNode { + out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} +} + +// resolveReferences walks the textNode tree searching for any leaf reference +// metadata and resolves each against the corresponding trunk references. +// Since pointer addresses in memory are not particularly readable to the user, +// it replaces each pointer value with an arbitrary and unique reference ID. +func resolveReferences(s textNode) { + var walkNodes func(textNode, func(textNode)) + walkNodes = func(s textNode, f func(textNode)) { + f(s) + switch s := s.(type) { + case *textWrap: + walkNodes(s.Value, f) + case textList: + for _, r := range s { + walkNodes(r.Value, f) + } + } + } + + // Collect all trunks and leaves with reference metadata. + var trunks, leaves []*textWrap + walkNodes(s, func(s textNode) { + if s, ok := s.(*textWrap); ok { + switch s.Metadata.(type) { + case leafReference: + leaves = append(leaves, s) + case trunkReference, trunkReferences: + trunks = append(trunks, s) + } + } + }) + + // No leaf references to resolve. + if len(leaves) == 0 { + return + } + + // Collect the set of all leaf references to resolve. + leafPtrs := make(map[value.Pointer]bool) + for _, leaf := range leaves { + leafPtrs[leaf.Metadata.(leafReference).p] = true + } + + // Collect the set of trunk pointers that are always paired together. + // This allows us to assign a single ID to both pointers for brevity. + // If a pointer in a pair ever occurs by itself or as a different pair, + // then the pair is broken. + pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) + unpair := func(p value.Pointer) { + if !pairedTrunkPtrs[p].IsNil() { + pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half + } + pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + unpair(p.p) // standalone pointer cannot be part of a pair + case trunkReferences: + p0, ok0 := pairedTrunkPtrs[p.pp[0]] + p1, ok1 := pairedTrunkPtrs[p.pp[1]] + switch { + case !ok0 && !ok1: + // Register the newly seen pair. + pairedTrunkPtrs[p.pp[0]] = p.pp[1] + pairedTrunkPtrs[p.pp[1]] = p.pp[0] + case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: + // Exact pair already seen; do nothing. + default: + // Pair conflicts with some other pair; break all pairs. + unpair(p.pp[0]) + unpair(p.pp[1]) + } + } + } + + // Correlate each pointer referenced by leaves to a unique identifier, + // and print the IDs for each trunk that matches those pointers. + var nextID uint + ptrIDs := make(map[value.Pointer]uint) + newID := func() uint { + id := nextID + nextID++ + return id + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + if print := leafPtrs[p.p]; print { + id, ok := ptrIDs[p.p] + if !ok { + id = newID() + ptrIDs[p.p] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } + case trunkReferences: + print0 := leafPtrs[p.pp[0]] + print1 := leafPtrs[p.pp[1]] + if print0 || print1 { + id0, ok0 := ptrIDs[p.pp[0]] + id1, ok1 := ptrIDs[p.pp[1]] + isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] + if isPair { + var id uint + assert(ok0 == ok1) // must be seen together or not at all + if ok0 { + assert(id0 == id1) // must have the same ID + id = id0 + } else { + id = newID() + ptrIDs[p.pp[0]] = id + ptrIDs[p.pp[1]] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } else { + if print0 && !ok0 { + id0 = newID() + ptrIDs[p.pp[0]] = id0 + } + if print1 && !ok1 { + id1 = newID() + ptrIDs[p.pp[1]] = id1 + } + switch { + case print0 && print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) + case print0: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) + case print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) + } + } + } + } + } + + // Update all leaf references with the unique identifier. + for _, leaf := range leaves { + if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { + leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) + } + } +} + +func formatReference(id uint) string { + return fmt.Sprintf("ref#%d", id) +} + +func updateReferencePrefix(prefix, ref string) string { + if prefix == "" { + return pointerDelimPrefix + ref + pointerDelimSuffix + } + suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) + return pointerDelimPrefix + ref + ": " + suffix +} diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go new file mode 100644 index 0000000..2ab41fa --- /dev/null +++ b/cmp/report_reflect.go @@ -0,0 +1,414 @@ +// Copyright 2019, 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 cmp + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +var ( + anyType = reflect.TypeOf((*interface{})(nil)).Elem() + stringType = reflect.TypeOf((*string)(nil)).Elem() + bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() + byteType = reflect.TypeOf((*byte)(nil)).Elem() +) + +type formatValueOptions struct { + // AvoidStringer controls whether to avoid calling custom stringer + // methods like error.Error or fmt.Stringer.String. + AvoidStringer bool + + // PrintAddresses controls whether to print the address of all pointers, + // slice elements, and maps. + PrintAddresses bool + + // QualifiedNames controls whether FormatType uses the fully qualified name + // (including the full package path as opposed to just the package name). + QualifiedNames bool + + // VerbosityLevel controls the amount of output to produce. + // A higher value produces more output. A value of zero or lower produces + // no output (represented using an ellipsis). + // If LimitVerbosity is false, then the level is treated as infinite. + VerbosityLevel int + + // LimitVerbosity specifies that formatting should respect VerbosityLevel. + LimitVerbosity bool +} + +// FormatType prints the type as if it were wrapping s. +// This may return s as-is depending on the current type and TypeMode mode. +func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { + // Check whether to emit the type or not. + switch opts.TypeMode { + case autoType: + switch t.Kind() { + case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map: + if s.Equal(textNil) { + return s + } + default: + return s + } + if opts.DiffMode == diffIdentical { + return s // elide type for identical nodes + } + case elideType: + return s + } + + // Determine the type label, applying special handling for unnamed types. + typeName := value.TypeString(t, opts.QualifiedNames) + if t.Name() == "" { + // According to Go grammar, certain type literals contain symbols that + // do not strongly bind to the next lexicographical token (e.g., *T). + switch t.Kind() { + case reflect.Chan, reflect.Func, reflect.Ptr: + typeName = "(" + typeName + ")" + } + } + return &textWrap{Prefix: typeName, Value: wrapParens(s)} +} + +// wrapParens wraps s with a set of parenthesis, but avoids it if the +// wrapped node itself is already surrounded by a pair of parenthesis or braces. +// It handles unwrapping one level of pointer-reference nodes. +func wrapParens(s textNode) textNode { + var refNode *textWrap + if s2, ok := s.(*textWrap); ok { + // Unwrap a single pointer reference node. + switch s2.Metadata.(type) { + case leafReference, trunkReference, trunkReferences: + refNode = s2 + if s3, ok := refNode.Value.(*textWrap); ok { + s2 = s3 + } + } + + // Already has delimiters that make parenthesis unnecessary. + hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") + hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") + if hasParens || hasBraces { + return s + } + } + if refNode != nil { + refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} + return s + } + return &textWrap{Prefix: "(", Value: s, Suffix: ")"} +} + +// FormatValue prints the reflect.Value, taking extra care to avoid descending +// into pointers already in ptrs. As pointers are visited, ptrs is also updated. +func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { + if !v.IsValid() { + return nil + } + t := v.Type() + + // Check slice element for cycles. + if parentKind == reflect.Slice { + ptrRef, visited := ptrs.Push(v.Addr()) + if visited { + return makeLeafReference(ptrRef, false) + } + defer ptrs.Pop() + defer func() { out = wrapTrunkReference(ptrRef, false, out) }() + } + + // Check whether there is an Error or String method to call. + if !opts.AvoidStringer && v.CanInterface() { + // Avoid calling Error or String methods on nil receivers since many + // implementations crash when doing so. + if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { + var prefix, strVal string + func() { + // Swallow and ignore any panics from String or Error. + defer func() { recover() }() + switch v := v.Interface().(type) { + case error: + strVal = v.Error() + prefix = "e" + case fmt.Stringer: + strVal = v.String() + prefix = "s" + } + }() + if prefix != "" { + return opts.formatString(prefix, strVal) + } + } + } + + // Check whether to explicitly wrap the result with the type. + var skipType bool + defer func() { + if !skipType { + out = opts.FormatType(t, out) + } + }() + + switch t.Kind() { + case reflect.Bool: + return textLine(fmt.Sprint(v.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return textLine(fmt.Sprint(v.Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uint8: + if parentKind == reflect.Slice || parentKind == reflect.Array { + return textLine(formatHex(v.Uint())) + } + return textLine(fmt.Sprint(v.Uint())) + case reflect.Uintptr: + return textLine(formatHex(v.Uint())) + case reflect.Float32, reflect.Float64: + return textLine(fmt.Sprint(v.Float())) + case reflect.Complex64, reflect.Complex128: + return textLine(fmt.Sprint(v.Complex())) + case reflect.String: + return opts.formatString("", v.String()) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return textLine(formatPointer(value.PointerOf(v), true)) + case reflect.Struct: + var list textList + v := makeAddressable(v) // needed for retrieveUnexportedField + maxLen := v.NumField() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if vv.IsZero() { + continue // Elide fields with zero values + } + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sf := t.Field(i) + if supportExporters && !isExported(sf.Name) { + vv = retrieveUnexportedField(v, sf, true) + } + s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) + list = append(list, textRecord{Key: sf.Name, Value: s}) + } + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} + case reflect.Slice: + if v.IsNil() { + return textNil + } + + // Check whether this is a []byte of text data. + if t.Elem() == byteType { + b := v.Bytes() + isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } + if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { + out = opts.formatString("", string(b)) + skipType = true + return opts.FormatType(t, out) + } + } + + fallthrough + case reflect.Array: + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for i := 0; i < v.Len(); i++ { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) + list = append(list, textRecord{Value: s}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if t.Kind() == reflect.Slice && opts.PrintAddresses { + header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) + out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} + } + return out + case reflect.Map: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + return makeLeafReference(ptrRef, opts.PrintAddresses) + } + defer ptrs.Pop() + + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } + var list textList + for _, k := range value.SortKeys(v.MapKeys()) { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } + sk := formatMapKey(k, false, ptrs) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) + list = append(list, textRecord{Key: sk, Value: sv}) + } + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + return out + case reflect.Ptr: + if v.IsNil() { + return textNil + } + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + out = makeLeafReference(ptrRef, opts.PrintAddresses) + return &textWrap{Prefix: "&", Value: out} + } + defer ptrs.Pop() + + // Skip the name only if this is an unnamed pointer type. + // Otherwise taking the address of a value does not reproduce + // the named pointer type. + if v.Type().Name() == "" { + skipType = true // Let the underlying value print the type instead + } + out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + out = &textWrap{Prefix: "&", Value: out} + return out + case reflect.Interface: + if v.IsNil() { + return textNil + } + // Interfaces accept different concrete types, + // so configure the underlying value to explicitly print the type. + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +func (opts formatOptions) formatString(prefix, s string) textNode { + maxLen := len(s) + maxLines := strings.Count(s, "\n") + 1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + } + + // For multiline strings, use the triple-quote syntax, + // but only use it when printing removed or inserted nodes since + // we only want the extra verbosity for those cases. + lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") + isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') + for i := 0; i < len(lines) && isTripleQuoted; i++ { + lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + line := lines[i] + isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen + } + if isTripleQuoted { + var list textList + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + for i, line := range lines { + if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { + comment := commentString(fmt.Sprintf("%d elided lines", numElided)) + list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) + break + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + return &textWrap{Prefix: "(", Value: list, Suffix: ")"} + } + + // Format the string as a single-line quoted string. + if len(s) > maxLen+len(textEllipsis) { + return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) + } + return textLine(prefix + formatString(s)) +} + +// formatMapKey formats v as if it were a map key. +// The result is guaranteed to be a single line. +func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { + var opts formatOptions + opts.DiffMode = diffIdentical + opts.TypeMode = elideType + opts.PrintAddresses = disambiguate + opts.AvoidStringer = disambiguate + opts.QualifiedNames = disambiguate + opts.VerbosityLevel = maxVerbosityPreset + opts.LimitVerbosity = true + s := opts.FormatValue(v, reflect.Map, ptrs).String() + return strings.TrimSpace(s) +} + +// formatString prints s as a double-quoted or backtick-quoted string. +func formatString(s string) string { + // Use quoted string if it the same length as a raw string literal. + // Otherwise, attempt to use the raw string form. + qs := strconv.Quote(s) + if len(qs) == 1+len(s)+1 { + return qs + } + + // Disallow newlines to ensure output is a single line. + // Only allow printable runes for readability purposes. + rawInvalid := func(r rune) bool { + return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') + } + if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { + return "`" + s + "`" + } + return qs +} + +// formatHex prints u as a hexadecimal integer in Go notation. +func formatHex(u uint64) string { + var f string + switch { + case u <= 0xff: + f = "0x%02x" + case u <= 0xffff: + f = "0x%04x" + case u <= 0xffffff: + f = "0x%06x" + case u <= 0xffffffff: + f = "0x%08x" + case u <= 0xffffffffff: + f = "0x%010x" + case u <= 0xffffffffffff: + f = "0x%012x" + case u <= 0xffffffffffffff: + f = "0x%014x" + case u <= 0xffffffffffffffff: + f = "0x%016x" + } + return fmt.Sprintf(f, u) +} diff --git a/cmp/report_slices.go b/cmp/report_slices.go new file mode 100644 index 0000000..23e444f --- /dev/null +++ b/cmp/report_slices.go @@ -0,0 +1,614 @@ +// Copyright 2019, 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 cmp + +import ( + "bytes" + "fmt" + "math" + "reflect" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/diff" +) + +// CanFormatDiffSlice reports whether we support custom formatting for nodes +// that are slices of primitive kinds or strings. +func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { + switch { + case opts.DiffMode != diffUnknown: + return false // Must be formatting in diff mode + case v.NumDiff == 0: + return false // No differences detected + case !v.ValueX.IsValid() || !v.ValueY.IsValid(): + return false // Both values must be valid + case v.NumIgnored > 0: + return false // Some ignore option was used + case v.NumTransformed > 0: + return false // Some transform option was used + case v.NumCompared > 1: + return false // More than one comparison was used + case v.NumCompared == 1 && v.Type.Name() != "": + // The need for cmp to check applicability of options on every element + // in a slice is a significant performance detriment for large []byte. + // The workaround is to specify Comparer(bytes.Equal), + // which enables cmp to compare []byte more efficiently. + // If they differ, we still want to provide batched diffing. + // The logic disallows named types since they tend to have their own + // String method, with nicer formatting than what this provides. + return false + } + + // Check whether this is an interface with the same concrete types. + t := v.Type + vx, vy := v.ValueX, v.ValueY + if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + } + + // Check whether we provide specialized diffing for this type. + switch t.Kind() { + case reflect.String: + case reflect.Array, reflect.Slice: + // Only slices of primitive types have specialized handling. + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + default: + return false + } + + // Both slice values have to be non-empty. + if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) { + return false + } + + // If a sufficient number of elements already differ, + // use specialized formatting even if length requirement is not met. + if v.NumDiff > v.NumSame { + return true + } + default: + return false + } + + // Use specialized string diffing for longer slices or strings. + const minLength = 32 + return vx.Len() >= minLength && vy.Len() >= minLength +} + +// FormatDiffSlice prints a diff for the slices (or strings) represented by v. +// This provides custom-tailored logic to make printing of differences in +// textual strings and slices of primitive kinds more readable. +func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { + assert(opts.DiffMode == diffUnknown) + t, vx, vy := v.Type, v.ValueX, v.ValueY + if t.Kind() == reflect.Interface { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + opts = opts.WithTypeMode(emitType) + } + + // Auto-detect the type of the data. + var sx, sy string + var ssx, ssy []string + var isString, isMostlyText, isPureLinedText, isBinary bool + switch { + case t.Kind() == reflect.String: + sx, sy = vx.String(), vy.String() + isString = true + case t.Kind() == reflect.Slice && t.Elem() == byteType: + sx, sy = string(vx.Bytes()), string(vy.Bytes()) + isString = true + case t.Kind() == reflect.Array: + // Arrays need to be addressable for slice operations to work. + vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem() + vx2.Set(vx) + vy2.Set(vy) + vx, vy = vx2, vy2 + } + if isString { + var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int + for i, r := range sx + sy { + numTotalRunes++ + if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError { + numValidRunes++ + } + if r == '\n' { + if maxLineLen < i-lastLineIdx { + maxLineLen = i - lastLineIdx + } + lastLineIdx = i + 1 + numLines++ + } + } + isPureText := numValidRunes == numTotalRunes + isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes)) + isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024 + isBinary = !isMostlyText + + // Avoid diffing by lines if it produces a significantly more complex + // edit script than diffing by bytes. + if isPureLinedText { + ssx = strings.Split(sx, "\n") + ssy = strings.Split(sy, "\n") + esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result { + return diff.BoolResult(ssx[ix] == ssy[iy]) + }) + esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result { + return diff.BoolResult(sx[ix] == sy[iy]) + }) + efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) + efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) + quotedLength := len(strconv.Quote(sx + sy)) + unquotedLength := len(sx) + len(sy) + escapeExpansionRatio := float64(quotedLength) / float64(unquotedLength) + isPureLinedText = efficiencyLines < 4*efficiencyBytes || escapeExpansionRatio > 1.1 + } + } + + // Format the string into printable records. + var list textList + var delim string + switch { + // If the text appears to be multi-lined text, + // then perform differencing across individual lines. + case isPureLinedText: + list = opts.formatDiffSlice( + reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.Index(0).String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + delim = "\n" + + // If possible, use a custom triple-quote (""") syntax for printing + // differences in a string literal. This format is more readable, + // but has edge-cases where differences are visually indistinguishable. + // This format is avoided under the following conditions: + // - A line starts with `"""` + // - A line starts with "..." + // - A line contains non-printable characters + // - Adjacent different lines differ only by whitespace + // + // For example: + // + // """ + // ... // 3 identical lines + // foo + // bar + // - baz + // + BAZ + // """ + isTripleQuoted := true + prevRemoveLines := map[string]bool{} + prevInsertLines := map[string]bool{} + var list2 textList + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + for _, r := range list { + if !r.Value.Equal(textEllipsis) { + line, _ := strconv.Unquote(string(r.Value.(textLine))) + line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + normLine := strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 // drop whitespace to avoid visually indistinguishable output + } + return r + }, line) + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" + switch r.Diff { + case diffRemoved: + isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine] + prevRemoveLines[normLine] = true + case diffInserted: + isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine] + prevInsertLines[normLine] = true + } + if !isTripleQuoted { + break + } + r.Value = textLine(line) + r.ElideComma = true + } + if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group + prevRemoveLines = map[string]bool{} + prevInsertLines = map[string]bool{} + } + list2 = append(list2, r) + } + if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 { + list2 = list2[:len(list2)-1] // elide single empty line at the end + } + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + if isTripleQuoted { + var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} + switch t.Kind() { + case reflect.String: + if t != stringType { + out = opts.FormatType(t, out) + } + case reflect.Slice: + // Always emit type for slices since the triple-quote syntax + // looks like a string (not a slice). + opts = opts.WithTypeMode(emitType) + out = opts.FormatType(t, out) + } + return out + } + + // If the text appears to be single-lined text, + // then perform differencing in approximately fixed-sized chunks. + // The output is printed as quoted strings. + case isMostlyText: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte", + func(v reflect.Value, d diffMode) textRecord { + s := formatString(v.String()) + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + + // If the text appears to be binary data, + // then perform differencing in approximately fixed-sized chunks. + // The output is inspired by hexdump. + case isBinary: + list = opts.formatDiffSlice( + reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte", + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + ss = append(ss, formatHex(v.Index(i).Uint())) + } + s := strings.Join(ss, ", ") + comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String()))) + return textRecord{Diff: d, Value: textLine(s), Comment: comment} + }, + ) + + // For all other slices of primitive types, + // then perform differencing in approximately fixed-sized chunks. + // The size of each chunk depends on the width of the element kind. + default: + var chunkSize int + if t.Elem().Kind() == reflect.Bool { + chunkSize = 16 + } else { + switch t.Elem().Bits() { + case 8: + chunkSize = 16 + case 16: + chunkSize = 12 + case 32: + chunkSize = 8 + default: + chunkSize = 8 + } + } + list = opts.formatDiffSlice( + vx, vy, chunkSize, t.Elem().Kind().String(), + func(v reflect.Value, d diffMode) textRecord { + var ss []string + for i := 0; i < v.Len(); i++ { + switch t.Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + ss = append(ss, fmt.Sprint(v.Index(i).Int())) + case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: + ss = append(ss, fmt.Sprint(v.Index(i).Uint())) + case reflect.Uint8, reflect.Uintptr: + ss = append(ss, formatHex(v.Index(i).Uint())) + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + ss = append(ss, fmt.Sprint(v.Index(i).Interface())) + } + } + s := strings.Join(ss, ", ") + return textRecord{Diff: d, Value: textLine(s)} + }, + ) + } + + // Wrap the output with appropriate type information. + var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if !isMostlyText { + // The "{...}" byte-sequence literal is not valid Go syntax for strings. + // Emit the type for extra clarity (e.g. "string{...}"). + if t.Kind() == reflect.String { + opts = opts.WithTypeMode(emitType) + } + return opts.FormatType(t, out) + } + switch t.Kind() { + case reflect.String: + out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != stringType { + out = opts.FormatType(t, out) + } + case reflect.Slice: + out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} + if t != bytesType { + out = opts.FormatType(t, out) + } + } + return out +} + +// formatASCII formats s as an ASCII string. +// This is useful for printing binary strings in a semi-legible way. +func formatASCII(s string) string { + b := bytes.Repeat([]byte{'.'}, len(s)) + for i := 0; i < len(s); i++ { + if ' ' <= s[i] && s[i] <= '~' { + b[i] = s[i] + } + } + return string(b) +} + +func (opts formatOptions) formatDiffSlice( + vx, vy reflect.Value, chunkSize int, name string, + makeRec func(reflect.Value, diffMode) textRecord, +) (list textList) { + eq := func(ix, iy int) bool { + return vx.Index(ix).Interface() == vy.Index(iy).Interface() + } + es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + return diff.BoolResult(eq(ix, iy)) + }) + + appendChunks := func(v reflect.Value, d diffMode) int { + n0 := v.Len() + for v.Len() > 0 { + n := chunkSize + if n > v.Len() { + n = v.Len() + } + list = append(list, makeRec(v.Slice(0, n), d)) + v = v.Slice(n, v.Len()) + } + return n0 - v.Len() + } + + var numDiffs int + maxLen := -1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + opts.VerbosityLevel-- + } + + groups := coalesceAdjacentEdits(name, es) + groups = coalesceInterveningIdentical(groups, chunkSize/4) + groups = cleanupSurroundingIdentical(groups, eq) + maxGroup := diffStats{Name: name} + for i, ds := range groups { + if maxLen >= 0 && numDiffs >= maxLen { + maxGroup = maxGroup.Append(ds) + continue + } + + // Print equal. + if ds.NumDiff() == 0 { + // Compute the number of leading and trailing equal bytes to print. + var numLo, numHi int + numEqual := ds.NumIgnored + ds.NumIdentical + for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 { + numLo++ + } + for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 { + numHi++ + } + if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 { + numHi = numEqual - numLo // Avoid pointless coalescing of single equal row + } + + // Print the equal bytes. + appendChunks(vx.Slice(0, numLo), diffIdentical) + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + } + appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical) + vx = vx.Slice(numEqual, vx.Len()) + vy = vy.Slice(numEqual, vy.Len()) + continue + } + + // Print unequal. + len0 := len(list) + nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved) + vx = vx.Slice(nx, vx.Len()) + ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted) + vy = vy.Slice(ny, vy.Len()) + numDiffs += len(list) - len0 + } + if maxGroup.IsZero() { + assert(vx.Len() == 0 && vy.Len() == 0) + } else { + list.AppendEllipsis(maxGroup) + } + return list +} + +// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent +// equal or unequal counts. +// +// Example: +// +// Input: "..XXY...Y" +// Output: [ +// {NumIdentical: 2}, +// {NumRemoved: 2, NumInserted 1}, +// {NumIdentical: 3}, +// {NumInserted: 1}, +// ] +func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { + var prevMode byte + lastStats := func(mode byte) *diffStats { + if prevMode != mode { + groups = append(groups, diffStats{Name: name}) + prevMode = mode + } + return &groups[len(groups)-1] + } + for _, e := range es { + switch e { + case diff.Identity: + lastStats('=').NumIdentical++ + case diff.UniqueX: + lastStats('!').NumRemoved++ + case diff.UniqueY: + lastStats('!').NumInserted++ + case diff.Modified: + lastStats('!').NumModified++ + } + } + return groups +} + +// coalesceInterveningIdentical coalesces sufficiently short (<= windowSize) +// equal groups into adjacent unequal groups that currently result in a +// dual inserted/removed printout. This acts as a high-pass filter to smooth +// out high-frequency changes within the windowSize. +// +// Example: +// +// WindowSize: 16, +// Input: [ +// {NumIdentical: 61}, // group 0 +// {NumRemoved: 3, NumInserted: 1}, // group 1 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 9}, // └── coalesce +// {NumIdentical: 64}, // group 2 +// {NumRemoved: 3, NumInserted: 1}, // group 3 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 7}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 2}, // └── coalesce +// {NumIdentical: 63}, // group 4 +// ] +// Output: [ +// {NumIdentical: 61}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 64}, +// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 63}, +// ] +func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { + groups, groupsOrig := groups[:0], groups + for i, ds := range groupsOrig { + if len(groups) >= 2 && ds.NumDiff() > 0 { + prev := &groups[len(groups)-2] // Unequal group + curr := &groups[len(groups)-1] // Equal group + next := &groupsOrig[i] // Unequal group + hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0 + hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0 + if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize { + *prev = prev.Append(*curr).Append(*next) + groups = groups[:len(groups)-1] // Truncate off equal group + continue + } + } + groups = append(groups, ds) + } + return groups +} + +// cleanupSurroundingIdentical scans through all unequal groups, and +// moves any leading sequence of equal elements to the preceding equal group and +// moves and trailing sequence of equal elements to the succeeding equal group. +// +// This is necessary since coalesceInterveningIdentical may coalesce edit groups +// together such that leading/trailing spans of equal elements becomes possible. +// Note that this can occur even with an optimal diffing algorithm. +// +// Example: +// +// Input: [ +// {NumIdentical: 61}, +// {NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements +// {NumIdentical: 67}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, // assume 10 trailing identical elements +// {NumIdentical: 54}, +// ] +// Output: [ +// {NumIdentical: 64}, // incremented by 3 +// {NumRemoved: 9}, +// {NumIdentical: 67}, +// {NumRemoved: 9}, +// {NumIdentical: 64}, // incremented by 10 +// ] +func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats { + var ix, iy int // indexes into sequence x and y + for i, ds := range groups { + // Handle equal group. + if ds.NumDiff() == 0 { + ix += ds.NumIdentical + iy += ds.NumIdentical + continue + } + + // Handle unequal group. + nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified + ny := ds.NumIdentical + ds.NumInserted + ds.NumModified + var numLeadingIdentical, numTrailingIdentical int + for j := 0; j < nx && j < ny && eq(ix+j, iy+j); j++ { + numLeadingIdentical++ + } + for j := 0; j < nx && j < ny && eq(ix+nx-1-j, iy+ny-1-j); j++ { + numTrailingIdentical++ + } + if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 { + if numLeadingIdentical > 0 { + // Remove leading identical span from this group and + // insert it into the preceding group. + if i-1 >= 0 { + groups[i-1].NumIdentical += numLeadingIdentical + } else { + // No preceding group exists, so prepend a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...) + }() + } + // Increment indexes since the preceding group would have handled this. + ix += numLeadingIdentical + iy += numLeadingIdentical + } + if numTrailingIdentical > 0 { + // Remove trailing identical span from this group and + // insert it into the succeeding group. + if i+1 < len(groups) { + groups[i+1].NumIdentical += numTrailingIdentical + } else { + // No succeeding group exists, so append a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical}) + }() + } + // Do not increment indexes since the succeeding group will handle this. + } + + // Update this group since some identical elements were removed. + nx -= numIdentical + ny -= numIdentical + groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny} + } + ix += nx + iy += ny + } + return groups +} diff --git a/cmp/report_text.go b/cmp/report_text.go new file mode 100644 index 0000000..388fcf5 --- /dev/null +++ b/cmp/report_text.go @@ -0,0 +1,432 @@ +// Copyright 2019, 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 cmp + +import ( + "bytes" + "fmt" + "math/rand" + "strings" + "time" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +const maxColumnLength = 80 + +type indentMode int + +func (n indentMode) appendIndent(b []byte, d diffMode) []byte { + // The output of Diff is documented as being unstable to provide future + // flexibility in changing the output for more humanly readable reports. + // This logic intentionally introduces instability to the exact output + // so that users can detect accidental reliance on stability early on, + // rather than much later when an actual change to the format occurs. + if flags.Deterministic || randBool { + // Use regular spaces (U+0020). + switch d { + case diffUnknown, diffIdentical: + b = append(b, " "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } else { + // Use non-breaking spaces (U+00a0). + switch d { + case diffUnknown, diffIdentical: + b = append(b, "  "...) + case diffRemoved: + b = append(b, "- "...) + case diffInserted: + b = append(b, "+ "...) + } + } + return repeatCount(n).appendChar(b, '\t') +} + +type repeatCount int + +func (n repeatCount) appendChar(b []byte, c byte) []byte { + for ; n > 0; n-- { + b = append(b, c) + } + return b +} + +// textNode is a simplified tree-based representation of structured text. +// Possible node types are textWrap, textList, or textLine. +type textNode interface { + // Len reports the length in bytes of a single-line version of the tree. + // Nested textRecord.Diff and textRecord.Comment fields are ignored. + Len() int + // Equal reports whether the two trees are structurally identical. + // Nested textRecord.Diff and textRecord.Comment fields are compared. + Equal(textNode) bool + // String returns the string representation of the text tree. + // It is not guaranteed that len(x.String()) == x.Len(), + // nor that x.String() == y.String() implies that x.Equal(y). + String() string + + // formatCompactTo formats the contents of the tree as a single-line string + // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment + // fields are ignored. + // + // However, not all nodes in the tree should be collapsed as a single-line. + // If a node can be collapsed as a single-line, it is replaced by a textLine + // node. Since the top-level node cannot replace itself, this also returns + // the current node itself. + // + // This does not mutate the receiver. + formatCompactTo([]byte, diffMode) ([]byte, textNode) + // formatExpandedTo formats the contents of the tree as a multi-line string + // to the provided buffer. In order for column alignment to operate well, + // formatCompactTo must be called before calling formatExpandedTo. + formatExpandedTo([]byte, diffMode, indentMode) []byte +} + +// textWrap is a wrapper that concatenates a prefix and/or a suffix +// to the underlying node. +type textWrap struct { + Prefix string // e.g., "bytes.Buffer{" + Value textNode // textWrap | textList | textLine + Suffix string // e.g., "}" + Metadata interface{} // arbitrary metadata; has no effect on formatting +} + +func (s *textWrap) Len() int { + return len(s.Prefix) + s.Value.Len() + len(s.Suffix) +} +func (s1 *textWrap) Equal(s2 textNode) bool { + if s2, ok := s2.(*textWrap); ok { + return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix + } + return false +} +func (s *textWrap) String() string { + var d diffMode + var n indentMode + _, s2 := s.formatCompactTo(nil, d) + b := n.appendIndent(nil, d) // Leading indent + b = s2.formatExpandedTo(b, d, n) // Main body + b = append(b, '\n') // Trailing newline + return string(b) +} +func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + n0 := len(b) // Original buffer length + b = append(b, s.Prefix...) + b, s.Value = s.Value.formatCompactTo(b, d) + b = append(b, s.Suffix...) + if _, ok := s.Value.(textLine); ok { + return b, textLine(b[n0:]) + } + return b, s +} +func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + b = append(b, s.Prefix...) + b = s.Value.formatExpandedTo(b, d, n) + b = append(b, s.Suffix...) + return b +} + +// textList is a comma-separated list of textWrap or textLine nodes. +// The list may be formatted as multi-lines or single-line at the discretion +// of the textList.formatCompactTo method. +type textList []textRecord +type textRecord struct { + Diff diffMode // e.g., 0 or '-' or '+' + Key string // e.g., "MyField" + Value textNode // textWrap | textLine + ElideComma bool // avoid trailing comma + Comment fmt.Stringer // e.g., "6 identical fields" +} + +// AppendEllipsis appends a new ellipsis node to the list if none already +// exists at the end. If cs is non-zero it coalesces the statistics with the +// previous diffStats. +func (s *textList) AppendEllipsis(ds diffStats) { + hasStats := !ds.IsZero() + if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { + if hasStats { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) + } else { + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) + } + return + } + if hasStats { + (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds) + } +} + +func (s textList) Len() (n int) { + for i, r := range s { + n += len(r.Key) + if r.Key != "" { + n += len(": ") + } + n += r.Value.Len() + if i < len(s)-1 { + n += len(", ") + } + } + return n +} + +func (s1 textList) Equal(s2 textNode) bool { + if s2, ok := s2.(textList); ok { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + r1, r2 := s1[i], s2[i] + if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) { + return false + } + } + return true + } + return false +} + +func (s textList) String() string { + return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() +} + +func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + s = append(textList(nil), s...) // Avoid mutating original + + // Determine whether we can collapse this list as a single line. + n0 := len(b) // Original buffer length + var multiLine bool + for i, r := range s { + if r.Diff == diffInserted || r.Diff == diffRemoved { + multiLine = true + } + b = append(b, r.Key...) + if r.Key != "" { + b = append(b, ": "...) + } + b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff) + if _, ok := s[i].Value.(textLine); !ok { + multiLine = true + } + if r.Comment != nil { + multiLine = true + } + if i < len(s)-1 { + b = append(b, ", "...) + } + } + // Force multi-lined output when printing a removed/inserted node that + // is sufficiently long. + if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { + multiLine = true + } + if !multiLine { + return b, textLine(b[n0:]) + } + return b, s +} + +func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { + alignKeyLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return r.Key == "" || !isLine + }, + func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, + ) + alignValueLens := s.alignLens( + func(r textRecord) bool { + _, isLine := r.Value.(textLine) + return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil + }, + func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, + ) + + // Format lists of simple lists in a batched form. + // If the list is sequence of only textLine values, + // then batch multiple values on a single line. + var isSimple bool + for _, r := range s { + _, isLine := r.Value.(textLine) + isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil + if !isSimple { + break + } + } + if isSimple { + n++ + var batch []byte + emitBatch := func() { + if len(batch) > 0 { + b = n.appendIndent(append(b, '\n'), d) + b = append(b, bytes.TrimRight(batch, " ")...) + batch = batch[:0] + } + } + for _, r := range s { + line := r.Value.(textLine) + if len(batch)+len(line)+len(", ") > maxColumnLength { + emitBatch() + } + batch = append(batch, line...) + batch = append(batch, ", "...) + } + emitBatch() + n-- + return n.appendIndent(append(b, '\n'), d) + } + + // Format the list as a multi-lined output. + n++ + for i, r := range s { + b = n.appendIndent(append(b, '\n'), d|r.Diff) + if r.Key != "" { + b = append(b, r.Key+": "...) + } + b = alignKeyLens[i].appendChar(b, ' ') + + b = r.Value.formatExpandedTo(b, d|r.Diff, n) + if !r.ElideComma { + b = append(b, ',') + } + b = alignValueLens[i].appendChar(b, ' ') + + if r.Comment != nil { + b = append(b, " // "+r.Comment.String()...) + } + } + n-- + + return n.appendIndent(append(b, '\n'), d) +} + +func (s textList) alignLens( + skipFunc func(textRecord) bool, + lenFunc func(textRecord) int, +) []repeatCount { + var startIdx, endIdx, maxLen int + lens := make([]repeatCount, len(s)) + for i, r := range s { + if skipFunc(r) { + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + startIdx, endIdx, maxLen = i+1, i+1, 0 + } else { + if maxLen < lenFunc(r) { + maxLen = lenFunc(r) + } + endIdx = i + 1 + } + } + for j := startIdx; j < endIdx && j < len(s); j++ { + lens[j] = repeatCount(maxLen - lenFunc(s[j])) + } + return lens +} + +// textLine is a single-line segment of text and is always a leaf node +// in the textNode tree. +type textLine []byte + +var ( + textNil = textLine("nil") + textEllipsis = textLine("...") +) + +func (s textLine) Len() int { + return len(s) +} +func (s1 textLine) Equal(s2 textNode) bool { + if s2, ok := s2.(textLine); ok { + return bytes.Equal([]byte(s1), []byte(s2)) + } + return false +} +func (s textLine) String() string { + return string(s) +} +func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { + return append(b, s...), s +} +func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte { + return append(b, s...) +} + +type diffStats struct { + Name string + NumIgnored int + NumIdentical int + NumRemoved int + NumInserted int + NumModified int +} + +func (s diffStats) IsZero() bool { + s.Name = "" + return s == diffStats{} +} + +func (s diffStats) NumDiff() int { + return s.NumRemoved + s.NumInserted + s.NumModified +} + +func (s diffStats) Append(ds diffStats) diffStats { + assert(s.Name == ds.Name) + s.NumIgnored += ds.NumIgnored + s.NumIdentical += ds.NumIdentical + s.NumRemoved += ds.NumRemoved + s.NumInserted += ds.NumInserted + s.NumModified += ds.NumModified + return s +} + +// String prints a humanly-readable summary of coalesced records. +// +// Example: +// +// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields" +func (s diffStats) String() string { + var ss []string + var sum int + labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"} + counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified} + for i, n := range counts { + if n > 0 { + ss = append(ss, fmt.Sprintf("%d %v", n, labels[i])) + } + sum += n + } + + // Pluralize the name (adjusting for some obscure English grammar rules). + name := s.Name + if sum > 1 { + name += "s" + if strings.HasSuffix(name, "ys") { + name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" + } + } + + // Format the list according to English grammar (with Oxford comma). + switch n := len(ss); n { + case 0: + return "" + case 1, 2: + return strings.Join(ss, " and ") + " " + name + default: + return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name + } +} + +type commentString string + +func (s commentString) String() string { return string(s) } diff --git a/cmp/report_value.go b/cmp/report_value.go new file mode 100644 index 0000000..668d470 --- /dev/null +++ b/cmp/report_value.go @@ -0,0 +1,121 @@ +// Copyright 2019, 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 cmp + +import "reflect" + +// valueNode represents a single node within a report, which is a +// structured representation of the value tree, containing information +// regarding which nodes are equal or not. +type valueNode struct { + parent *valueNode + + Type reflect.Type + ValueX reflect.Value + ValueY reflect.Value + + // NumSame is the number of leaf nodes that are equal. + // All descendants are equal only if NumDiff is 0. + NumSame int + // NumDiff is the number of leaf nodes that are not equal. + NumDiff int + // NumIgnored is the number of leaf nodes that are ignored. + NumIgnored int + // NumCompared is the number of leaf nodes that were compared + // using an Equal method or Comparer function. + NumCompared int + // NumTransformed is the number of non-leaf nodes that were transformed. + NumTransformed int + // NumChildren is the number of transitive descendants of this node. + // This counts from zero; thus, leaf nodes have no descendants. + NumChildren int + // MaxDepth is the maximum depth of the tree. This counts from zero; + // thus, leaf nodes have a depth of zero. + MaxDepth int + + // Records is a list of struct fields, slice elements, or map entries. + Records []reportRecord // If populated, implies Value is not populated + + // Value is the result of a transformation, pointer indirect, of + // type assertion. + Value *valueNode // If populated, implies Records is not populated + + // TransformerName is the name of the transformer. + TransformerName string // If non-empty, implies Value is populated +} +type reportRecord struct { + Key reflect.Value // Invalid for slice element + Value *valueNode +} + +func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { + vx, vy := ps.Values() + child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy} + switch s := ps.(type) { + case StructField: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child}) + case SliceIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Value: child}) + case MapIndex: + assert(parent.Value == nil) + parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child}) + case Indirect: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case TypeAssertion: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + case Transform: + assert(parent.Value == nil && parent.Records == nil) + parent.Value = child + parent.TransformerName = s.Name() + parent.NumTransformed++ + default: + assert(parent == nil) // Must be the root step + } + return child +} + +func (r *valueNode) Report(rs Result) { + assert(r.MaxDepth == 0) // May only be called on leaf nodes + + if rs.ByIgnore() { + r.NumIgnored++ + } else { + if rs.Equal() { + r.NumSame++ + } else { + r.NumDiff++ + } + } + assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) + + if rs.ByMethod() { + r.NumCompared++ + } + if rs.ByFunc() { + r.NumCompared++ + } + assert(r.NumCompared <= 1) +} + +func (child *valueNode) PopStep() (parent *valueNode) { + if child.parent == nil { + return nil + } + parent = child.parent + parent.NumSame += child.NumSame + parent.NumDiff += child.NumDiff + parent.NumIgnored += child.NumIgnored + parent.NumCompared += child.NumCompared + parent.NumTransformed += child.NumTransformed + parent.NumChildren += child.NumChildren + 1 + if parent.MaxDepth < child.MaxDepth+1 { + parent.MaxDepth = child.MaxDepth + 1 + } + return parent +} diff --git a/cmp/reporter.go b/cmp/reporter.go deleted file mode 100644 index 20e9f18..0000000 --- a/cmp/reporter.go +++ /dev/null @@ -1,53 +0,0 @@ -// 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 cmp - -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp/internal/value" -) - -type defaultReporter struct { - Option - diffs []string // List of differences, possibly truncated - ndiffs int // Total number of differences - nbytes int // Number of bytes in diffs - nlines int // Number of lines in diffs -} - -var _ reporter = (*defaultReporter)(nil) - -func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { - if eq { - return // Ignore equal results - } - const maxBytes = 4096 - const maxLines = 256 - r.ndiffs++ - if r.nbytes < maxBytes && r.nlines < maxLines { - sx := value.Format(x, value.FormatConfig{UseStringer: true}) - sy := value.Format(y, value.FormatConfig{UseStringer: true}) - if sx == sy { - // Unhelpful output, so use more exact formatting. - sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true}) - sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true}) - } - s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) - r.diffs = append(r.diffs, s) - r.nbytes += len(s) - r.nlines += strings.Count(s, "\n") - } -} - -func (r *defaultReporter) String() string { - s := strings.Join(r.diffs, "") - if r.ndiffs == len(r.diffs) { - return s - } - return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs)) -} diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs new file mode 100644 index 0000000..be77b95 --- /dev/null +++ b/cmp/testdata/diffs @@ -0,0 +1,1854 @@ +<<< TestDiff/Comparer/StructInequal + struct{ A int; B int; C int }{ + A: 1, + B: 2, +- C: 3, ++ C: 4, + } +>>> TestDiff/Comparer/StructInequal +<<< TestDiff/Comparer/PointerStructInequal + &struct{ A *int }{ +- A: &4, ++ A: &5, + } +>>> TestDiff/Comparer/PointerStructInequal +<<< TestDiff/Comparer/StructNestedPointerInequal + &struct{ R *bytes.Buffer }{ +- R: s"", ++ R: nil, + } +>>> TestDiff/Comparer/StructNestedPointerInequal +<<< TestDiff/Comparer/RegexpInequal + []*regexp.Regexp{ + nil, +- s"a*b*c*", ++ s"a*b*d*", + } +>>> TestDiff/Comparer/RegexpInequal +<<< TestDiff/Comparer/TriplePointerInequal + &&&int( +- 0, ++ 1, + ) +>>> TestDiff/Comparer/TriplePointerInequal +<<< TestDiff/Comparer/StringerInequal + struct{ fmt.Stringer }( +- s"hello", ++ s"hello2", + ) +>>> TestDiff/Comparer/StringerInequal +<<< TestDiff/Comparer/DifferingHash + [32]uint8{ +- 0xca, 0x97, 0x81, 0x12, 0xca, 0x1b, 0xbd, 0xca, 0xfa, 0xc2, 0x31, 0xb3, 0x9a, 0x23, 0xdc, 0x4d, +- 0xa7, 0x86, 0xef, 0xf8, 0x14, 0x7c, 0x4e, 0x72, 0xb9, 0x80, 0x77, 0x85, 0xaf, 0xee, 0x48, 0xbb, ++ 0x3e, 0x23, 0xe8, 0x16, 0x00, 0x39, 0x59, 0x4a, 0x33, 0x89, 0x4f, 0x65, 0x64, 0xe1, 0xb1, 0x34, ++ 0x8b, 0xbd, 0x7a, 0x00, 0x88, 0xd4, 0x2c, 0x4a, 0xcb, 0x73, 0xee, 0xae, 0xd5, 0x9c, 0x00, 0x9d, + } +>>> TestDiff/Comparer/DifferingHash +<<< TestDiff/Comparer/NilStringer + any( +- &fmt.Stringer(nil), + ) +>>> TestDiff/Comparer/NilStringer +<<< TestDiff/Comparer/TarHeaders + []cmp_test.tarHeader{ + { + ... // 4 identical fields + Size: 1, + ModTime: s"2009-11-10 23:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 2, + ModTime: s"2009-11-11 00:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 4, + ModTime: s"2009-11-11 01:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 8, + ModTime: s"2009-11-11 02:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 16, + ModTime: s"2009-11-11 03:00:00 +0000 UTC", +- Typeflag: 48, ++ Typeflag: 0, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + } +>>> TestDiff/Comparer/TarHeaders +<<< TestDiff/Comparer/IrreflexiveComparison + []int{ +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), +- Inverse(λ, float64(NaN)), ++ Inverse(λ, float64(NaN)), + } +>>> TestDiff/Comparer/IrreflexiveComparison +<<< TestDiff/Comparer/StringerMapKey + map[*testprotos.Stringer]*testprotos.Stringer( +- {s"hello": s"world"}, ++ nil, + ) +>>> TestDiff/Comparer/StringerMapKey +<<< TestDiff/Comparer/StringerBacktick + any( +- []*testprotos.Stringer{s`multi\nline\nline\nline`}, + ) +>>> TestDiff/Comparer/StringerBacktick +<<< TestDiff/Comparer/DynamicMap + []any{ + map[string]any{ + "avg": float64(0.278), +- "hr": int(65), ++ "hr": float64(65), + "name": string("Mark McGwire"), + }, + map[string]any{ + "avg": float64(0.288), +- "hr": int(63), ++ "hr": float64(63), + "name": string("Sammy Sosa"), + }, + } +>>> TestDiff/Comparer/DynamicMap +<<< TestDiff/Comparer/MapKeyPointer + map[*int]string{ +- &⟪0xdeadf00f⟫0: "hello", ++ &⟪0xdeadf00f⟫0: "world", + } +>>> TestDiff/Comparer/MapKeyPointer +<<< TestDiff/Comparer/IgnoreSliceElements + [2][]int{ + {..., 1, 2, 3, ...}, + { + ... // 6 ignored and 1 identical elements +- 20, ++ 2, + ... // 3 ignored elements + }, + } +>>> TestDiff/Comparer/IgnoreSliceElements +<<< TestDiff/Comparer/IgnoreMapEntries + [2]map[string]int{ + {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, + { + ... // 2 ignored entries + "keep1": 1, ++ "keep2": 2, + }, + } +>>> TestDiff/Comparer/IgnoreMapEntries +<<< TestDiff/Transformer/Uints + uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( +- 0, ++ 1, + ))))))) +>>> TestDiff/Transformer/Uints +<<< TestDiff/Transformer/Filtered + []int{ + Inverse(λ, int64(0)), +- Inverse(λ, int64(-5)), ++ Inverse(λ, int64(3)), + Inverse(λ, int64(0)), +- Inverse(λ, int64(-1)), ++ Inverse(λ, int64(-5)), + } +>>> TestDiff/Transformer/Filtered +<<< TestDiff/Transformer/DisjointOutput + int(Inverse(λ, any( +- string("zero"), ++ float64(1), + ))) +>>> TestDiff/Transformer/DisjointOutput +<<< TestDiff/Transformer/JSON + string(Inverse(ParseJSON, map[string]any{ + "address": map[string]any{ +- "city": string("Los Angeles"), ++ "city": string("New York"), + "postalCode": string("10021-3100"), +- "state": string("CA"), ++ "state": string("NY"), + "streetAddress": string("21 2nd Street"), + }, + "age": float64(25), + "children": []any{}, + "firstName": string("John"), + "isAlive": bool(true), + "lastName": string("Smith"), + "phoneNumbers": []any{ + map[string]any{ +- "number": string("212 555-4321"), ++ "number": string("212 555-1234"), + "type": string("home"), + }, + map[string]any{"number": string("646 555-4567"), "type": string("office")}, + map[string]any{"number": string("123 456-7890"), "type": string("mobile")}, + }, ++ "spouse": nil, + })) +>>> TestDiff/Transformer/JSON +<<< TestDiff/Transformer/AcyclicString + cmp_test.StringBytes{ + String: Inverse(SplitString, []string{ + "some", + "multi", +- "Line", ++ "line", + "string", + }), + Bytes: []uint8(Inverse(SplitBytes, [][]uint8{ + "some", + "multi", + "line", + { +- 0x62, ++ 0x42, + 0x79, + 0x74, + ... // 2 identical elements + }, + })), + } +>>> TestDiff/Transformer/AcyclicString +<<< TestDiff/Reporter/PanicStringer + struct{ X fmt.Stringer }{ +- X: struct{ fmt.Stringer }{}, ++ X: s"", + } +>>> TestDiff/Reporter/PanicStringer +<<< TestDiff/Reporter/PanicError + struct{ X error }{ +- X: struct{ error }{}, ++ X: e"", + } +>>> TestDiff/Reporter/PanicError +<<< TestDiff/Reporter/AmbiguousType + any( +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{}, + ) +>>> TestDiff/Reporter/AmbiguousType +<<< TestDiff/Reporter/AmbiguousPointer + (*int)( +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + ) +>>> TestDiff/Reporter/AmbiguousPointer +<<< TestDiff/Reporter/AmbiguousPointerStruct + struct{ I *int }{ +- I: &⟪0xdeadf00f⟫0, ++ I: &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerStruct +<<< TestDiff/Reporter/AmbiguousPointerSlice + []*int{ +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerSlice +<<< TestDiff/Reporter/AmbiguousPointerMap + map[string]*int{ +- "zero": &⟪0xdeadf00f⟫0, ++ "zero": &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerMap +<<< TestDiff/Reporter/AmbiguousStringer + any( +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + ) +>>> TestDiff/Reporter/AmbiguousStringer +<<< TestDiff/Reporter/AmbiguousStringerStruct + struct{ S fmt.Stringer }{ +- S: cmp_test.Stringer("hello"), ++ S: &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerStruct +<<< TestDiff/Reporter/AmbiguousStringerSlice + []fmt.Stringer{ +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerSlice +<<< TestDiff/Reporter/AmbiguousStringerMap + map[string]fmt.Stringer{ +- "zero": cmp_test.Stringer("hello"), ++ "zero": &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerMap +<<< TestDiff/Reporter/AmbiguousSliceHeader + []int( +- ⟪ptr:0xdeadf00f, len:0, cap:5⟫{}, ++ ⟪ptr:0xdeadf00f, len:0, cap:1000⟫{}, + ) +>>> TestDiff/Reporter/AmbiguousSliceHeader +<<< TestDiff/Reporter/AmbiguousStringerMapKey + map[any]string{ +- nil: "nil", ++ &⟪0xdeadf00f⟫"github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{S: "fizz"}: "buzz", ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{S: "fizz"}: "buzz", + } +>>> TestDiff/Reporter/AmbiguousStringerMapKey +<<< TestDiff/Reporter/NonAmbiguousStringerMapKey + map[any]string{ ++ s"fizz": "buzz", +- s"hello": "goodbye", + } +>>> TestDiff/Reporter/NonAmbiguousStringerMapKey +<<< TestDiff/Reporter/InvalidUTF8 + any( +- cmp_test.MyString("\xed\xa0\x80"), + ) +>>> TestDiff/Reporter/InvalidUTF8 +<<< TestDiff/Reporter/UnbatchedSlice + cmp_test.MyComposite{ + ... // 3 identical fields + BytesB: nil, + BytesC: nil, + IntsA: []int8{ ++ 10, + 11, +- 12, ++ 21, + 13, + 14, + ... // 15 identical elements + }, + IntsB: nil, + IntsC: nil, + ... // 6 identical fields + } +>>> TestDiff/Reporter/UnbatchedSlice +<<< TestDiff/Reporter/BatchedSlice + cmp_test.MyComposite{ + ... // 3 identical fields + BytesB: nil, + BytesC: nil, + IntsA: []int8{ +- 10, 11, 12, 13, 14, 15, 16, ++ 12, 29, 13, 27, 22, 23, + 17, 18, 19, 20, 21, +- 22, 23, 24, 25, 26, 27, 28, 29, ++ 10, 26, 16, 25, 28, 11, 15, 24, 14, + }, + IntsB: nil, + IntsC: nil, + ... // 6 identical fields + } +>>> TestDiff/Reporter/BatchedSlice +<<< TestDiff/Reporter/BatchedWithComparer + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ +- 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, // -|.......| ++ 0x0c, 0x1d, 0x0d, 0x1b, 0x16, 0x17, // +|......| + 0x11, 0x12, 0x13, 0x14, 0x15, // |.....| +- 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // -|........| ++ 0x0a, 0x1a, 0x10, 0x19, 0x1c, 0x0b, 0x0f, 0x18, 0x0e, // +|.........| + }, + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BatchedWithComparer +<<< TestDiff/Reporter/BatchedLong + any( +- cmp_test.MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, ...}}, + ) +>>> TestDiff/Reporter/BatchedLong +<<< TestDiff/Reporter/BatchedNamedAndUnnamed + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ +- 0x01, 0x02, 0x03, // -|...| ++ 0x03, 0x02, 0x01, // +|...| + }, + BytesB: []cmp_test.MyByte{ +- 0x04, 0x05, 0x06, ++ 0x06, 0x05, 0x04, + }, + BytesC: cmp_test.MyBytes{ +- 0x07, 0x08, 0x09, // -|...| ++ 0x09, 0x08, 0x07, // +|...| + }, + IntsA: []int8{ +- -1, -2, -3, ++ -3, -2, -1, + }, + IntsB: []cmp_test.MyInt{ +- -4, -5, -6, ++ -6, -5, -4, + }, + IntsC: cmp_test.MyInts{ +- -7, -8, -9, ++ -9, -8, -7, + }, + UintsA: []uint16{ +- 1000, 2000, 3000, ++ 3000, 2000, 1000, + }, + UintsB: []cmp_test.MyUint{ +- 4000, 5000, 6000, ++ 6000, 5000, 4000, + }, + UintsC: cmp_test.MyUints{ +- 7000, 8000, 9000, ++ 9000, 8000, 7000, + }, + FloatsA: []float32{ +- 1.5, 2.5, 3.5, ++ 3.5, 2.5, 1.5, + }, + FloatsB: []cmp_test.MyFloat{ +- 4.5, 5.5, 6.5, ++ 6.5, 5.5, 4.5, + }, + FloatsC: cmp_test.MyFloats{ +- 7.5, 8.5, 9.5, ++ 9.5, 8.5, 7.5, + }, + } +>>> TestDiff/Reporter/BatchedNamedAndUnnamed +<<< TestDiff/Reporter/BinaryHexdump + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ + 0xf3, 0x0f, 0x8a, 0xa4, 0xd3, 0x12, 0x52, 0x09, 0x24, 0xbe, // |......R.$.| +- 0x58, 0x95, 0x41, 0xfd, 0x24, 0x66, 0x58, 0x8b, 0x79, // -|X.A.$fX.y| + 0x54, 0xac, 0x0d, 0xd8, 0x71, 0x77, 0x70, 0x20, 0x6a, 0x5c, 0x73, 0x7f, 0x8c, 0x17, 0x55, 0xc0, // |T...qwp j\s...U.| + 0x34, 0xce, 0x6e, 0xf7, 0xaa, 0x47, 0xee, 0x32, 0x9d, 0xc5, 0xca, 0x1e, 0x58, 0xaf, 0x8f, 0x27, // |4.n..G.2....X..'| + 0xf3, 0x02, 0x4a, 0x90, 0xed, 0x69, 0x2e, 0x70, 0x32, 0xb4, 0xab, 0x30, 0x20, 0xb6, 0xbd, 0x5c, // |..J..i.p2..0 ..\| + 0x62, 0x34, 0x17, 0xb0, 0x00, 0xbb, 0x4f, 0x7e, 0x27, 0x47, 0x06, 0xf4, 0x2e, 0x66, 0xfd, 0x63, // |b4....O~'G...f.c| + 0xd7, 0x04, 0xdd, 0xb7, 0x30, 0xb7, 0xd1, // |....0..| +- 0x55, 0x7e, 0x7b, 0xf6, 0xb3, 0x7e, 0x1d, 0x57, 0x69, // -|U~{..~.Wi| ++ 0x75, 0x2d, 0x5b, 0x5d, 0x5d, 0xf6, 0xb3, 0x68, 0x61, 0x68, 0x61, 0x7e, 0x1d, 0x57, 0x49, // +|u-[]]..haha~.WI| + 0x20, 0x9e, 0xbc, 0xdf, 0xe1, 0x4d, 0xa9, 0xef, 0xa2, 0xd2, 0xed, 0xb4, 0x47, 0x78, 0xc9, 0xc9, // | ....M......Gx..| + 0x27, 0xa4, 0xc6, 0xce, 0xec, 0x44, 0x70, 0x5d, // |'....Dp]| + }, + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BinaryHexdump +<<< TestDiff/Reporter/StringHexdump + cmp_test.MyComposite{ + StringA: "", + StringB: cmp_test.MyString{ +- 0x72, 0x65, 0x61, 0x64, 0x6d, 0x65, // -|readme| ++ 0x67, 0x6f, 0x70, 0x68, 0x65, 0x72, // +|gopher| + 0x2e, 0x74, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |.txt............| + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |................| + ... // 64 identical bytes + 0x30, 0x30, 0x36, 0x30, 0x30, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x30, // |00600.0000000.00| + 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x34, // |00000.0000000004| +- 0x36, // -|6| ++ 0x33, // +|3| + 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, 0x31, 0x31, // |.00000000000.011| +- 0x31, 0x37, 0x33, // -|173| ++ 0x32, 0x31, 0x37, // +|217| + 0x00, 0x20, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |. 0.............| + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // |................| + ... // 326 identical bytes + }, + BytesA: nil, + BytesB: nil, + ... // 10 identical fields + } +>>> TestDiff/Reporter/StringHexdump +<<< TestDiff/Reporter/BinaryString + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: bytes.Join({ + `{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"`, + `address":{"streetAddress":"`, +- "314 54th Avenue", ++ "21 2nd Street", + `","city":"New York","state":"NY","postalCode":"10021-3100"},"pho`, + `neNumbers":[{"type":"home","number":"212 555-1234"},{"type":"off`, + ... // 101 identical bytes + }, ""), + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BinaryString +<<< TestDiff/Reporter/TripleQuote + cmp_test.MyComposite{ + StringA: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 6 identical lines + """ + ), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter/TripleQuote +<<< TestDiff/Reporter/TripleQuoteSlice + []string{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., + } +>>> TestDiff/Reporter/TripleQuoteSlice +<<< TestDiff/Reporter/TripleQuoteNamedTypes + cmp_test.MyComposite{ + StringA: "", + StringB: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + BytesA: nil, + BytesB: nil, + BytesC: cmp_test.MyBytes( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + IntsA: nil, + IntsB: nil, + ... // 7 identical fields + } +>>> TestDiff/Reporter/TripleQuoteNamedTypes +<<< TestDiff/Reporter/TripleQuoteSliceNamedTypes + []cmp_test.MyString{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., + } +>>> TestDiff/Reporter/TripleQuoteSliceNamedTypes +<<< TestDiff/Reporter/TripleQuoteEndlines + ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR ++ rrr + sss + ttt + ... // 4 identical lines + yyy + zzz +- + """ + ) +>>> TestDiff/Reporter/TripleQuoteEndlines +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", +- "fff", ++ `"""`, + "ggg", + "hhh", + ... // 7 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis + strings.Join({ + "aaa", + "bbb", +- "ccc", +- "...", ++ "CCC", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis +<<< TestDiff/Reporter/AvoidTripleQuoteNonPrintable + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", + ... // 7 identical lines + "mmm", + "nnn", +- "ooo", ++ "o\roo", + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteNonPrintable +<<< TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace + strings.Join({ + "aaa", + "bbb", +- "ccc", +- " ddd", ++ "ccc ", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace +<<< TestDiff/Reporter/TripleQuoteStringer + []fmt.Stringer{ + s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"..., +- ( +- s""" +- package main +- +- import ( +- "fmt" +- "math/rand" +- ) +- +- func main() { +- fmt.Println("My favorite number is", rand.Intn(10)) +- } +- s""" +- ), ++ ( ++ s""" ++ package main ++ ++ import ( ++ "fmt" ++ "math" ++ ) ++ ++ func main() { ++ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7)) ++ } ++ s""" ++ ), + } +>>> TestDiff/Reporter/TripleQuoteStringer +<<< TestDiff/Reporter/LimitMaximumBytesDiffs + []uint8{ +- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| ++ 0x5c, 0x3d, 0x3d, 0x3d, 0x3d, 0x7c, 0x96, 0xe7, 0x53, 0x42, 0xa0, 0xab, // +|\====|..SB..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x1d, 0xdf, 0x61, 0xae, 0x98, 0x9f, 0x48, // -|..a...H| ++ 0xf0, 0xbd, 0xa5, 0x71, 0xab, 0x17, 0x3b, // +|...q..;| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0xc7, 0xb0, 0xb7, // -|...| ++ 0xab, 0x50, 0x00, // +|.P.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0xef, 0x3d, 0x3d, 0x3d, 0x3d, 0x3a, 0x5c, 0x94, 0xe6, 0x4a, 0xc7, // -|.====:\..J.| ++ 0xeb, 0x3d, 0x3d, 0x3d, 0x3d, 0xa5, 0x14, 0xe6, 0x4f, 0x28, 0xe4, // +|.====...O(.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0xb4, // -|.| ++ 0x28, // +|(| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x0a, 0x0a, 0xf7, 0x94, // -|....| ++ 0x2f, 0x63, 0x40, 0x3f, // +|/c@?| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xf2, 0x9c, 0xc0, 0x66, // -|...f| ++ 0xd9, 0x78, 0xed, 0x13, // +|.x..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x34, 0xf6, 0xf1, 0xc3, 0x17, 0x82, // -|4.....| ++ 0x4a, 0xfc, 0x91, 0x38, 0x42, 0x8d, // +|J..8B.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x6e, 0x16, 0x60, 0x91, 0x44, 0xc6, 0x06, // -|n.`.D..| ++ 0x61, 0x38, 0x41, 0xeb, 0x73, 0x04, 0xae, // +|a8A.s..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x1c, 0x45, 0x3d, 0x3d, 0x3d, 0x3d, 0x2e, // -|.E====.| ++ 0x07, 0x43, 0x3d, 0x3d, 0x3d, 0x3d, 0x1c, // +|.C====.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xc4, 0x18, // -|..| ++ 0x91, 0x22, // +|."| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x8a, 0x8d, 0x0e, 0x3d, 0x3d, 0x3d, 0x3d, 0x87, 0xb1, 0xa5, 0x8e, 0xc3, 0x3d, 0x3d, 0x3d, 0x3d, // -|...====.....====| +- 0x3d, 0x7a, 0x0f, 0x31, 0xae, 0x55, 0x3d, // -|=z.1.U=| ++ 0x75, 0xd8, 0xbe, 0x3d, 0x3d, 0x3d, 0x3d, 0x73, 0xec, 0x84, 0x35, 0x07, 0x3d, 0x3d, 0x3d, 0x3d, // +|u..====s..5.====| ++ 0x3d, 0x3b, 0xab, 0x53, 0x39, 0x74, // +|=;.S9t| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x47, 0x2c, 0x3d, // -|G,=| ++ 0x3d, 0x1f, 0x1b, // +|=..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x35, 0xe7, 0x35, 0xee, 0x82, 0xf4, 0xce, 0x3d, 0x3d, 0x3d, 0x3d, 0x11, 0x72, 0x3d, // -|5.5....====.r=| ++ 0x3d, 0x80, 0xab, 0x2f, 0xed, 0x2b, 0x3a, 0x3b, 0x3d, 0x3d, 0x3d, 0x3d, 0xea, 0x49, // +|=../.+:;====.I| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |==========| +- 0xaf, 0x5d, 0x3d, // -|.]=| ++ 0x3d, 0xab, 0x6c, // +|=.l| + ... // 51 identical, 34 removed, and 35 inserted bytes + } +>>> TestDiff/Reporter/LimitMaximumBytesDiffs +<<< TestDiff/Reporter/LimitMaximumStringDiffs + ( + """ +- a ++ aa + b +- c ++ cc + d +- e ++ ee + f +- g ++ gg + h +- i ++ ii + j +- k ++ kk + l +- m ++ mm + n +- o ++ oo + p +- q ++ qq + r +- s ++ ss + t +- u ++ uu + v +- w ++ ww + x +- y ++ yy + z +- A ++ AA + B +- C ++ CC + D +- E ++ EE + ... // 12 identical, 10 removed, and 10 inserted lines + """ + ) +>>> TestDiff/Reporter/LimitMaximumStringDiffs +<<< TestDiff/Reporter/LimitMaximumSliceDiffs + []struct{ S string }{ +- {S: "a"}, ++ {S: "aa"}, + {S: "b"}, +- {S: "c"}, ++ {S: "cc"}, + {S: "d"}, +- {S: "e"}, ++ {S: "ee"}, + {S: "f"}, +- {S: "g"}, ++ {S: "gg"}, + {S: "h"}, +- {S: "i"}, ++ {S: "ii"}, + {S: "j"}, +- {S: "k"}, ++ {S: "kk"}, + {S: "l"}, +- {S: "m"}, ++ {S: "mm"}, + {S: "n"}, +- {S: "o"}, ++ {S: "oo"}, + {S: "p"}, +- {S: "q"}, ++ {S: "qq"}, + {S: "r"}, +- {S: "s"}, ++ {S: "ss"}, + {S: "t"}, +- {S: "u"}, ++ {S: "uu"}, + {S: "v"}, +- {S: "w"}, ++ {S: "ww"}, + {S: "x"}, +- {S: "y"}, ++ {S: "yy"}, + {S: "z"}, +- {S: "A"}, ++ {S: "AA"}, + {S: "B"}, +- {S: "C"}, ++ {S: "CC"}, + {S: "D"}, +- {S: "E"}, ++ {S: "EE"}, + ... // 12 identical and 10 modified elements + } +>>> TestDiff/Reporter/LimitMaximumSliceDiffs +<<< TestDiff/Reporter/MultilineString + cmp_test.MyComposite{ + StringA: ( + """ +- Package cmp determines equality of values. ++ Package cmp determines equality of value. + + This package is intended to be a more powerful and safer alternative to + ... // 6 identical lines + For example, an equality function may report floats as equal so long as they + are within some tolerance of each other. +- +- • Types that have an Equal method may use that method to determine equality. +- This allows package authors to determine the equality operation for the types +- that they define. + + • If no custom equality functions are used and no Equal method is defined, + ... // 3 identical lines + by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared + using the AllowUnexported option. +- + """ + ), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter/MultilineString +<<< TestDiff/Reporter/Slices + cmp_test.MyComposite{ + StringA: "", + StringB: "", +- BytesA: []uint8{0x01, 0x02, 0x03}, ++ BytesA: nil, +- BytesB: []cmp_test.MyByte{0x04, 0x05, 0x06}, ++ BytesB: nil, +- BytesC: cmp_test.MyBytes{0x07, 0x08, 0x09}, ++ BytesC: nil, +- IntsA: []int8{-1, -2, -3}, ++ IntsA: nil, +- IntsB: []cmp_test.MyInt{-4, -5, -6}, ++ IntsB: nil, +- IntsC: cmp_test.MyInts{-7, -8, -9}, ++ IntsC: nil, +- UintsA: []uint16{1000, 2000, 3000}, ++ UintsA: nil, +- UintsB: []cmp_test.MyUint{4000, 5000, 6000}, ++ UintsB: nil, +- UintsC: cmp_test.MyUints{7000, 8000, 9000}, ++ UintsC: nil, +- FloatsA: []float32{1.5, 2.5, 3.5}, ++ FloatsA: nil, +- FloatsB: []cmp_test.MyFloat{4.5, 5.5, 6.5}, ++ FloatsB: nil, +- FloatsC: cmp_test.MyFloats{7.5, 8.5, 9.5}, ++ FloatsC: nil, + } +>>> TestDiff/Reporter/Slices +<<< TestDiff/Reporter/EmptySlices + cmp_test.MyComposite{ + StringA: "", + StringB: "", +- BytesA: []uint8{}, ++ BytesA: nil, +- BytesB: []cmp_test.MyByte{}, ++ BytesB: nil, +- BytesC: cmp_test.MyBytes{}, ++ BytesC: nil, +- IntsA: []int8{}, ++ IntsA: nil, +- IntsB: []cmp_test.MyInt{}, ++ IntsB: nil, +- IntsC: cmp_test.MyInts{}, ++ IntsC: nil, +- UintsA: []uint16{}, ++ UintsA: nil, +- UintsB: []cmp_test.MyUint{}, ++ UintsB: nil, +- UintsC: cmp_test.MyUints{}, ++ UintsC: nil, +- FloatsA: []float32{}, ++ FloatsA: nil, +- FloatsB: []cmp_test.MyFloat{}, ++ FloatsB: nil, +- FloatsC: cmp_test.MyFloats{}, ++ FloatsC: nil, + } +>>> TestDiff/Reporter/EmptySlices +<<< TestDiff/Reporter/LargeMapKey + map[*[]uint8]int{ +- &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, ++ &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, + } +>>> TestDiff/Reporter/LargeMapKey +<<< TestDiff/Reporter/LargeStringInInterface + struct{ X any }{ + X: strings.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeStringInInterface +<<< TestDiff/Reporter/LargeBytesInInterface + struct{ X any }{ + X: bytes.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeBytesInInterface +<<< TestDiff/Reporter/LargeStandaloneString + struct{ X any }{ +- X: [1]string{ +- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.", +- }, ++ X: [1]string{ ++ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,", ++ }, + } +>>> TestDiff/Reporter/LargeStandaloneString +<<< TestDiff/Reporter/SurroundingEqualElements + strings.Join({ + "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", +- ",#=_value", + ` _value=2 11 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=a,tag2=bb", +- ",#=_value", + ` _value=2 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=b,tag2=cc", +- ",#=_value", + ` _value=1 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=a,tag2=dd", +- ",#=_value", + ` _value=3 31 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=c", +- ",#=_value", + ` _value=4 41 `, + }, "") +>>> TestDiff/Reporter/SurroundingEqualElements +<<< TestDiff/Reporter/MostlyTextString + strings.Join({ + "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", +- ",\xff=_value", + " _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=a,tag2=bb", +- ",\xff=_value", + " _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=b,tag2=cc", +- ",\xff=_value", + " _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=a,tag2=dd", +- ",\xff=_value", + " _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=c", +- ",\xff=_value", + " _value=4 41\n", + }, "") +>>> TestDiff/Reporter/MostlyTextString +<<< TestDiff/Reporter/AllLinesDiffer + strings.Join({ ++ "X", + "d5c14bdf6bac81c27afc5429500ed750\n", ++ "X", + "25483503b557c606dad4f144d27ae10b\n", ++ "X", + "90bdbcdbb6ea7156068e3dcfb7459244\n", ++ "X", + "978f480a6e3cced51e297fbff9a506b7\n", + }, "") +>>> TestDiff/Reporter/AllLinesDiffer +<<< TestDiff/Reporter/StringifiedBytes + struct{ X []uint8 }{ +- X: []uint8("hello, world!"), ++ X: nil, + } +>>> TestDiff/Reporter/StringifiedBytes +<<< TestDiff/Reporter/NonStringifiedBytes + struct{ X []uint8 }{ +- X: []uint8{0xde, 0xad, 0xbe, 0xef}, ++ X: nil, + } +>>> TestDiff/Reporter/NonStringifiedBytes +<<< TestDiff/Reporter/StringifiedNamedBytes + struct{ X cmp_test.MyBytes }{ +- X: cmp_test.MyBytes("hello, world!"), ++ X: nil, + } +>>> TestDiff/Reporter/StringifiedNamedBytes +<<< TestDiff/Reporter/NonStringifiedNamedBytes + struct{ X cmp_test.MyBytes }{ +- X: cmp_test.MyBytes{0xde, 0xad, 0xbe, 0xef}, ++ X: nil, + } +>>> TestDiff/Reporter/NonStringifiedNamedBytes +<<< TestDiff/Reporter/ShortJSON + ( + """ + { +- "id": 1, ++ "id": 1434180, + "foo": true, + "bar": true, + } + """ + ) +>>> TestDiff/Reporter/ShortJSON +<<< TestDiff/Reporter/PointerToStringOrAny + any( +- &string("hello"), ++ &any(string("hello")), + ) +>>> TestDiff/Reporter/PointerToStringOrAny +<<< TestDiff/Reporter/NamedPointer + any( +- &string("hello"), ++ cmp_test.PointerString(&string("hello")), + ) +>>> TestDiff/Reporter/NamedPointer +<<< TestDiff/Reporter/MapStringAny + map[string]any{ +- "key": int(0), ++ "key": uint(0), + } +>>> TestDiff/Reporter/MapStringAny +<<< TestDiff/Reporter/StructFieldAny + struct{ X any }{ +- X: int(0), ++ X: uint(0), + } +>>> TestDiff/Reporter/StructFieldAny +<<< TestDiff/Reporter/SliceOfBytesText + [][]uint8{ +- "hello", + "foo", ++ "foo", + "barbaz", ++ "added", ++ "here", +- "blahdieblah", ++ "hrmph", + } +>>> TestDiff/Reporter/SliceOfBytesText +<<< TestDiff/Reporter/SliceOfBytesBinary + [][]uint8{ +- {0xde, 0xad, 0xbe, 0xef}, + {0xff, 0x6f, 0x6f}, ++ "foo", + "barbaz", ++ "added", ++ "here", +- "blahdieblah", ++ {0x68, 0x72, 0x6d, 0x70, 0x68, 0xff}, + } +>>> TestDiff/Reporter/SliceOfBytesBinary +<<< TestDiff/Reporter/ManyEscapeCharacters + ( + """ + [ +- {"Base32": "NA======"}, ++ {"Base32": "NB======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + ... // 3 identical lines + """ + ) +>>> TestDiff/Reporter/ManyEscapeCharacters +<<< TestDiff/EmbeddedStruct/ParentStructA/Inequal + teststructs.ParentStructA{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructA/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructB/Inequal + teststructs.ParentStructB{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructB/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructC/Inequal + teststructs.ParentStructC{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructC/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructD/Inequal + teststructs.ParentStructD{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructD/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructE/Inequal + teststructs.ParentStructE{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructE/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructF/Inequal + teststructs.ParentStructF{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, +- Public: 5, ++ Public: 6, +- private: 6, ++ private: 7, + } +>>> TestDiff/EmbeddedStruct/ParentStructF/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructG/Inequal + &teststructs.ParentStructG{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructG/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructH/Inequal + &teststructs.ParentStructH{ + PublicStruct: &teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructH/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructI/Inequal + &teststructs.ParentStructI{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: &teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructI/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructJ/Inequal + &teststructs.ParentStructJ{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: &teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + Public: teststructs.PublicStruct{ +- Public: 7, ++ Public: 8, +- private: 8, ++ private: 9, + }, + private: teststructs.privateStruct{ +- Public: 5, ++ Public: 6, +- private: 6, ++ private: 7, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructJ/Inequal +<<< TestDiff/EqualMethod/StructB/ValueInequal + teststructs.StructB{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB/ValueInequal +<<< TestDiff/EqualMethod/StructD/ValueInequal + teststructs.StructD{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD/ValueInequal +<<< TestDiff/EqualMethod/StructE/ValueInequal + teststructs.StructE{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE/ValueInequal +<<< TestDiff/EqualMethod/StructF/ValueInequal + teststructs.StructF{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF/ValueInequal +<<< TestDiff/EqualMethod/StructA1/ValueInequal + teststructs.StructA1{ + StructA: {X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1/ValueInequal +<<< TestDiff/EqualMethod/StructA1/PointerInequal + &teststructs.StructA1{ + StructA: {X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1/PointerInequal +<<< TestDiff/EqualMethod/StructB1/ValueInequal + teststructs.StructB1{ + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1/ValueInequal +<<< TestDiff/EqualMethod/StructB1/PointerInequal + &teststructs.StructB1{ + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1/PointerInequal +<<< TestDiff/EqualMethod/StructD1/ValueInequal + teststructs.StructD1{ +- StructD: teststructs.StructD{X: "NotEqual"}, ++ StructD: teststructs.StructD{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD1/ValueInequal +<<< TestDiff/EqualMethod/StructE1/ValueInequal + teststructs.StructE1{ +- StructE: teststructs.StructE{X: "NotEqual"}, ++ StructE: teststructs.StructE{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE1/ValueInequal +<<< TestDiff/EqualMethod/StructF1/ValueInequal + teststructs.StructF1{ +- StructF: teststructs.StructF{X: "NotEqual"}, ++ StructF: teststructs.StructF{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF1/ValueInequal +<<< TestDiff/EqualMethod/StructA2/ValueInequal + teststructs.StructA2{ + StructA: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2/ValueInequal +<<< TestDiff/EqualMethod/StructA2/PointerInequal + &teststructs.StructA2{ + StructA: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2/PointerInequal +<<< TestDiff/EqualMethod/StructB2/ValueInequal + teststructs.StructB2{ + StructB: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2/ValueInequal +<<< TestDiff/EqualMethod/StructB2/PointerInequal + &teststructs.StructB2{ + StructB: &{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2/PointerInequal +<<< TestDiff/EqualMethod/StructNo/Inequal + teststructs.StructNo{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructNo/Inequal +<<< TestDiff/Cycle/PointersInequal + &&⟪ref#0⟫cmp_test.P( +- &⟪ref#0⟫(...), ++ &&⟪ref#0⟫(...), + ) +>>> TestDiff/Cycle/PointersInequal +<<< TestDiff/Cycle/SlicesInequal + cmp_test.S{ +- ⟪ref#0⟫{⟪ref#0⟫(...)}, ++ ⟪ref#1⟫{{⟪ref#1⟫(...)}}, + } +>>> TestDiff/Cycle/SlicesInequal +<<< TestDiff/Cycle/MapsInequal + cmp_test.M⟪ref#0⟫{ +- 0: ⟪ref#0⟫(...), ++ 0: {0: ⟪ref#0⟫(...)}, + } +>>> TestDiff/Cycle/MapsInequal +<<< TestDiff/Cycle/GraphInequalZeroed + map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + }, + }, + }, + }, + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + "Buzz": &⟪ref#2⟫(...), + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + "Buzz": &⟪ref#2⟫(...), + }, + }, + }, + }, + "Foo": &⟪ref#4⟫{ + Name: "Foo", + Bravos: map[string]*cmp_test.CycleBravo{ + "FooBravo": &{ +- ID: 101, ++ ID: 0, + Name: "FooBravo", + Mods: 100, + Alphas: {"Foo": &⟪ref#4⟫(...)}, + }, + }, + }, + } +>>> TestDiff/Cycle/GraphInequalZeroed +<<< TestDiff/Cycle/GraphInequalStruct + map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ + ID: 102, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, + }, + }, + }, + }, + }, + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}, +- "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo"}, ++ "BuzzBarBravo": &⟪ref#3⟫(...), + }, + }, + }, + }, + }, + }, + "Buzz": &⟪ref#2⟫{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫{Name: "Bar", Bravos: {"BarBuzzBravo": &⟪ref#1⟫(...), "BuzzBarBravo": &⟪ref#3⟫{ID: 103, Name: "BuzzBarBravo", Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}}}, "Buzz": &⟪ref#2⟫(...)}}, + "BuzzBarBravo": &⟪ref#3⟫{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": &⟪ref#0⟫{ ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &⟪ref#1⟫{...}, "BuzzBarBravo": &⟪ref#3⟫(...)}, ++ }, ++ "Buzz": &⟪ref#2⟫(...), ++ }, + }, + }, + }, + "Foo": &⟪ref#4⟫{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &⟪ref#4⟫(...)}}}}, + } +>>> TestDiff/Cycle/GraphInequalStruct +<<< TestDiff/Project1/ProtoInequal + teststructs.Eagle{ + ... // 4 identical fields + Dreamers: nil, + Prong: 0, + Slaps: []teststructs.Slap{ + ... // 2 identical elements + {}, + {}, + { + Name: "", + Desc: "", + DescLong: "", +- Args: s"metadata", ++ Args: s"metadata2", + Tense: 0, + Interval: 0, + ... // 3 identical fields + }, + }, + StateGoverner: "", + PrankRating: "", + ... // 2 identical fields + } +>>> TestDiff/Project1/ProtoInequal +<<< TestDiff/Project1/Inequal + teststructs.Eagle{ + ... // 2 identical fields + Desc: "some description", + DescLong: "", + Dreamers: []teststructs.Dreamer{ + {}, + { + ... // 4 identical fields + ContSlaps: nil, + ContSlapsInterval: 0, + Animal: []any{ + teststructs.Goat{ + Target: "corporation", + Slaps: nil, + FunnyPrank: "", + Immutable: &teststructs.GoatImmutable{ +- ID: "southbay2", ++ ID: "southbay", +- State: &6, ++ State: &5, + Started: s"2009-11-10 23:00:00 +0000 UTC", + Stopped: s"0001-01-01 00:00:00 +0000 UTC", + ... // 1 ignored and 1 identical fields + }, + }, + teststructs.Donkey{}, + }, + Ornamental: false, + Amoeba: 53, + ... // 5 identical fields + }, + }, + Prong: 0, + Slaps: []teststructs.Slap{ + { + ... // 6 identical fields + Homeland: 0, + FunnyPrank: "", + Immutable: &teststructs.SlapImmutable{ + ID: "immutableSlap", + Out: nil, +- MildSlap: false, ++ MildSlap: true, + PrettyPrint: "", + State: nil, + Started: s"2009-11-10 23:00:00 +0000 UTC", + Stopped: s"0001-01-01 00:00:00 +0000 UTC", + LastUpdate: s"0001-01-01 00:00:00 +0000 UTC", + LoveRadius: &teststructs.LoveRadius{ + Summer: &teststructs.SummerLove{ + Summary: &teststructs.SummerLoveSummary{ + Devices: []string{ + "foo", +- "bar", +- "baz", + }, + ChangeType: {1, 2, 3}, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + }, + }, + StateGoverner: "", + PrankRating: "", + ... // 2 identical fields + } +>>> TestDiff/Project1/Inequal +<<< TestDiff/Project2/InequalOrder + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ + 17: {s"germ1"}, + 18: { +- s"germ2", + s"germ3", + s"germ4", ++ s"germ2", + }, + }, + CleanGerms: nil, + GermMap: {13: s"germ13", 21: s"germ21"}, + ... // 7 identical fields + } +>>> TestDiff/Project2/InequalOrder +<<< TestDiff/Project2/Inequal + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ ++ 17: {s"germ1"}, + 18: Inverse(Sort, []*testprotos.Germ{ + s"germ2", + s"germ3", +- s"germ4", + }), + }, + CleanGerms: nil, + GermMap: {13: s"germ13", 21: s"germ21"}, + DishMap: map[int32]*teststructs.Dish{ + 0: &{err: e"EOF"}, +- 1: nil, ++ 1: &{err: e"unexpected EOF"}, + 2: &{pb: s"dish"}, + }, + HasPreviousResult: true, + DirtyID: 10, + CleanID: 0, +- GermStrain: 421, ++ GermStrain: 22, + TotalDirtyGerms: 0, + InfectedAt: s"2009-11-10 23:00:00 +0000 UTC", + } +>>> TestDiff/Project2/Inequal +<<< TestDiff/Project3/Inequal + teststructs.Dirt{ +- table: &teststructs.MockTable{state: []string{"a", "c"}}, ++ table: &teststructs.MockTable{state: []string{"a", "b", "c"}}, + ts: 12345, +- Discord: 554, ++ Discord: 500, +- Proto: testprotos.Dirt(Inverse(λ, s"blah")), ++ Proto: testprotos.Dirt(Inverse(λ, s"proto")), + wizard: map[string]*testprotos.Wizard{ +- "albus": s"dumbledore", +- "harry": s"potter", ++ "harry": s"otter", + }, + sadistic: nil, + lastTime: 54321, + ... // 1 ignored field + } +>>> TestDiff/Project3/Inequal +<<< TestDiff/Project4/Inequal + teststructs.Cartel{ + Headquarter: teststructs.Headquarter{ + id: 5, + location: "moon", + subDivisions: []string{ +- "alpha", + "bravo", + "charlie", + }, + incorporatedDate: s"0001-01-01 00:00:00 +0000 UTC", + metaData: s"metadata", + privateMessage: nil, + publicMessage: []uint8{ + 0x01, + 0x02, +- 0x03, ++ 0x04, +- 0x04, ++ 0x03, + 0x05, + }, + horseBack: "abcdef", + rattle: "", + ... // 5 identical fields + }, + source: "mars", + creationDate: s"0001-01-01 00:00:00 +0000 UTC", + boss: "al capone", + lastCrimeDate: s"0001-01-01 00:00:00 +0000 UTC", + poisons: []*teststructs.Poison{ + &{ +- poisonType: 1, ++ poisonType: 5, + expiration: s"2009-11-10 23:00:00 +0000 UTC", + manufacturer: "acme", + potency: 0, + }, +- &{poisonType: 2, manufacturer: "acme2"}, + }, + } +>>> TestDiff/Project4/Inequal diff --git a/cmp/unsafe_panic.go b/cmp/unsafe_panic.go deleted file mode 100644 index 8193f08..0000000 --- a/cmp/unsafe_panic.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build purego - -package cmp - -import "reflect" - -const supportAllowUnexported = false - -func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value { - panic("unsafeRetrieveField is not implemented") -} diff --git a/cmp/unsafe_reflect.go b/cmp/unsafe_reflect.go deleted file mode 100644 index 926fd47..0000000 --- a/cmp/unsafe_reflect.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// +build !purego - -package cmp - -import ( - "reflect" - "unsafe" -) - -const supportAllowUnexported = true - -// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct -// such that the value has read-write permissions. -// -// The parent struct, v, must be addressable, while f must be a StructField -// describing the field to retrieve. -func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value { - return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() -} diff --git a/go.mod b/go.mod index aa2daba..f55cea6 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/google/go-cmp + +go 1.13