diff options
author | Sasha Smundak <asmundak@google.com> | 2021-05-26 00:59:42 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-05-26 00:59:42 +0000 |
commit | 21d1decefbae9e5ee9403be7ba87aae94ffcc60c (patch) | |
tree | 58f4f974df5ebdb73bd0aa992754d5de19affb51 | |
parent | 693d56c4b56ce9ebec79e8236f1080f6b70d5021 (diff) | |
parent | 1ea8835d09a3ac3f7ce66367848dcf2444619882 (diff) | |
download | go-cmp-21d1decefbae9e5ee9403be7ba87aae94ffcc60c.tar.gz |
Merge sso://github/google/go-cmp, add mandatory files am: 1ea8835d09
Original change: https://android-review.googlesource.com/c/platform/external/go-cmp/+/1327475
Change-Id: I9bb13705cff688a5f4b6ecdf12f2a9a374880b7d
51 files changed, 12127 insertions, 0 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..efb4782 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +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: + - go test -v -race ./... + - go: 1.12.x + script: + - go test -v -race ./... + - go: 1.13.x + script: + - go test -v -race ./... + - go: 1.14.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/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ae319c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/METADATA b/METADATA new file mode 100644 index 0000000..58d714f --- /dev/null +++ b/METADATA @@ -0,0 +1,18 @@ +name: "go-cmp" +description: + "This package is intended to be a more powerful and safer alternative to " + "reflect.DeepEqual for comparing whether two values are semantically equal." + +third_party { + url { + type: HOMEPAGE + value: "https://github.com/google/go-cmp" + } + url { + type: GIT + value: "https://github.com/google/go-cmp.git" + } + version: "v0.4.1" + last_upgrade_date { year: 2020 month: 5 day: 14 } + license_type: NOTICE +} diff --git a/MODULE_LICENSE_BSD b/MODULE_LICENSE_BSD new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_BSD diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed0eb9b --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Package for equality of Go values + +[![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)][godev] +[![Build Status](https://travis-ci.org/google/go-cmp.svg?branch=master)][travis] + +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. + +See the [documentation][godev] for more information. + +This is not an official Google product. + +[godev]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[travis]: https://travis-ci.org/google/go-cmp + +## Install + +``` +go get -u github.com/google/go-cmp/cmp +``` + +## License + +BSD - See [LICENSE][license] file + +[license]: https://github.com/google/go-cmp/blob/master/LICENSE diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go new file mode 100644 index 0000000..e102849 --- /dev/null +++ b/cmp/cmpopts/equate.go @@ -0,0 +1,156 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "math" + "reflect" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a Comparer option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjunction with SortSlices and SortMaps. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a Comparer option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// 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. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a Comparer option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjunction with EquateApprox. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +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 representible 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) + // TODO: Use errors.Is when go1.13 is the minimally supported version of Go. + return xerrors.Is(xe, ye) || xerrors.Is(ye, xe) +} diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go new file mode 100644 index 0000000..ff8e785 --- /dev/null +++ b/cmp/cmpopts/ignore.go @@ -0,0 +1,207 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "unicode" + "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. +// 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()) +} + +// IgnoreTypes returns an Option that ignores all values assignable to +// certain types, which are specified by passing in a value of each type. +func IgnoreTypes(typs ...interface{}) cmp.Option { + tf := newTypeFilter(typs...) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type typeFilter []reflect.Type + +func newTypeFilter(typs ...interface{}) (tf typeFilter) { + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil { + // This occurs if someone tries to pass in sync.Locker(nil) + panic("cannot determine type; consider using IgnoreInterfaces") + } + tf = append(tf, t) + } + return tf +} +func (tf typeFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreInterfaces returns an Option that ignores all values or references of +// values assignable to certain interface types. These interfaces are specified +// by passing in an anonymous struct with the interface types embedded in it. +// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}. +func IgnoreInterfaces(ifaces interface{}) cmp.Option { + tf := newIfaceFilter(ifaces) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type ifaceFilter []reflect.Type + +func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { + t := reflect.TypeOf(ifaces) + if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { + panic("input must be an anonymous struct") + } + for i := 0; i < t.NumField(); i++ { + fi := t.Field(i) + switch { + case !fi.Anonymous: + panic("struct cannot have named fields") + case fi.Type.Kind() != reflect.Interface: + panic("embedded field must be an interface type") + case fi.Type.NumMethod() == 0: + // This matches everything; why would you ever want this? + panic("cannot ignore empty interface") + default: + tf = append(tf, fi.Type) + } + } + return tf +} +func (tf ifaceFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p.Last().Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreUnexported returns an Option that only ignores the immediate unexported +// fields of a struct, including anonymous fields of unexported types. +// 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()) +} + +type unexportedFilter struct{ m map[reflect.Type]bool } + +func newUnexportedFilter(typs ...interface{}) unexportedFilter { + ux := unexportedFilter{m: make(map[reflect.Type]bool)} + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + ux.m[t] = true + } + return ux +} +func (xf unexportedFilter) filter(p cmp.Path) bool { + sf, ok := p.Index(-1).(cmp.StructField) + if !ok { + return false + } + return xf.m[p.Index(-2).Type()] && !isExported(sf.Name()) +} + +// isExported reports whether the identifier is exported. +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 new file mode 100644 index 0000000..3a48046 --- /dev/null +++ b/cmp/cmpopts/sort.go @@ -0,0 +1,147 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "sort" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// SortSlices returns a Transformer option that sorts all []V. +// The less function must be of the form "func(T, T) bool" which is used to +// 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) +// +// 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(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + 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)) +} + +type sliceSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ss sliceSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + if !(x != nil && y != nil && vx.Type() == vy.Type()) || + !(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) || + (vx.Len() <= 1 && vy.Len() <= 1) { + return false + } + // Check whether the slices are already sorted to avoid an infinite + // recursion cycle applying the same transform to itself. + ok1 := sort.SliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) }) + ok2 := sort.SliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) }) + return !ok1 || !ok2 +} +func (ss sliceSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + dst.Index(i).Set(src.Index(i)) + } + sort.SliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) }) + ss.checkSort(dst) + return dst.Interface() +} +func (ss sliceSorter) checkSort(v reflect.Value) { + start := -1 // Start of a sequence of equal elements. + for i := 1; i < v.Len(); i++ { + if ss.less(v, i-1, i) { + // Check that first and last elements in v[start:i] are equal. + if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} + +// SortMaps returns a Transformer option that flattens map[K]V types to be a +// sorted []struct{K, V}. The less function must be of the form +// "func(T, T) bool" which is used to sort any map with key K that is +// assignable to T. +// +// Flattening the map into a slice has the property that cmp.Equal is able to +// 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) +// +// SortMaps can be used in conjunction with EquateEmpty. +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", lessFunc)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: src.Type().Key()}, + {Name: "V", Type: src.Type().Elem()}, + }) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sort.Slice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go new file mode 100644 index 0000000..dae7ced --- /dev/null +++ b/cmp/cmpopts/struct_filter.go @@ -0,0 +1,187 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// 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 select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// 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: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // 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) { + // 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) + } + } + } + if sf.Name == "" { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go new file mode 100644 index 0000000..37704c8 --- /dev/null +++ b/cmp/cmpopts/util_test.go @@ -0,0 +1,1371 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" +) + +type ( + MyInt int + MyInts []int + MyFloat float32 + MyString string + MyTime struct{ time.Time } + MyStruct struct { + A, B []int + C, D map[time.Time]string + } + + Foo1 struct{ Alpha, Bravo, Charlie int } + Foo2 struct{ *Foo1 } + Foo3 struct{ *Foo2 } + Bar1 struct{ Foo3 } + Bar2 struct { + Bar1 + *Foo3 + Bravo float32 + } + Bar3 struct { + Bar1 + Bravo *Bar2 + Delta struct{ Echo Foo1 } + *Foo3 + Alpha string + } + + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStruct struct { + *privateStruct + *PublicStruct + Public int + private int + } + + Everything struct { + MyInt + MyFloat + MyTime + MyStruct + Bar3 + ParentStruct + } + + EmptyInterface interface{} +) + +func TestOptions(t *testing.T) { + createBar3X := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}}, + Bravo: 4, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}}, + Alpha: "alpha", + } + } + createBar3Y := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}}, + Bravo: 5, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}}, + Alpha: "ALPHA", + } + } + + tests := []struct { + label string // Test name + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + wantEqual bool // Whether the inputs are equal + wantPanic bool // Whether Equal should panic + reason string // The reason for the expected outcome + }{{ + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + wantEqual: false, + reason: "not equal because empty non-nil and nil slice differ", + }, { + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + opts: []cmp.Option{EquateEmpty()}, + wantEqual: true, + reason: "equal because EquateEmpty equates empty slices", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + wantEqual: false, + reason: "not equal because element order differs", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: true, + reason: "equal because SortSlices sorts the slices", + }, { + label: "SortSlices", + x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: false, + reason: "not equal because MyInt is not the same type as int", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2}, + y: []float64{2, 0, 2, 1, 2, 1}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantEqual: true, + reason: "equal even when sorted with duplicate elements", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantPanic: true, + reason: "panics because SortSlices used with non-transitive less function", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + })}, + wantEqual: false, + reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN", + }, { + label: "SortSlices+EquateNaNs", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2}, + opts: []cmp.Option{ + EquateNaNs(), + SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + }), + }, + wantEqual: true, + reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + wantEqual: false, + reason: "not equal because timezones differ", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: true, + reason: "equal because SortMaps flattens to a slice where Time.Equal can be used", + }, { + label: "SortMaps", + x: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday", + }, + y: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: false, + reason: "not equal because MyTime is not assignable to time.Time", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{SortMaps(func(a, b int) bool { + if -10 < a && a <= 0 { + a *= -100 + } + if -10 < b && b <= 0 { + b *= -100 + } + return a < b + })}, + wantEqual: false, + reason: "not equal because values differ even though SortMap provides valid ordering", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{ + SortMaps(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x < y + }), + cmp.Comparer(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x == y + }), + }, + wantEqual: true, + reason: "equal because Comparer used to equate differences", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return x < y && x >= 0 && y >= 0 + })}, + wantPanic: true, + reason: "panics because SortMaps used with non-transitive less function", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return math.Abs(float64(x)) < math.Abs(float64(y)) + })}, + wantPanic: true, + reason: "panics because SortMaps used with partial less function", + }, { + label: "EquateEmpty+SortSlices+SortMaps", + x: MyStruct{ + A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + }, + D: map[time.Time]string{}, + }, + y: MyStruct{ + A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + B: []int{}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + }, + }, + opts: []cmp.Option{ + EquateEmpty(), + SortSlices(func(x, y int) bool { return x < y }), + SortMaps(func(x, y time.Time) bool { return x.Before(y) }), + }, + wantEqual: true, + reason: "no panics because EquateEmpty should compose with the sort options", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + wantEqual: false, + reason: "not equal because floats do not exactly matches", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.003, 0.009)}, + wantEqual: false, + reason: "not equal because EquateApprox is too strict", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0.011)}, + wantEqual: true, + reason: "equal because margin is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because fraction is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0.011)}, + wantEqual: true, + reason: "equal because both the margin and fraction are loose enough to match", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float64(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: false, + reason: "not equal because the types differ", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float32(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because EquateApprox also applies on float32s", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), math.Inf(-1)}, + y: []float64{math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: true, + reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), -1e100}, + y: []float64{+1e100, math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: false, + reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)}, + wantEqual: true, + reason: "equal because infinite fraction matches everything", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(0, math.Inf(+1))}, + wantEqual: true, + reason: "equal because infinite margin matches everything", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Pi, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: true, + reason: "equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Nextafter(math.Pi, math.Inf(+1)), + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + wantEqual: false, + reason: "not equal because NaN != NaN", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs allows NaN == NaN", + }, { + label: "EquateNaNs", + x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs operates on float32", + }, { + label: "EquateApprox+EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because EquateNaNs and EquateApprox compose together", + }, { + label: "EquateApprox+EquateNaNs", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: false, + reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type", + }, { + label: "EquateApprox+EquateNaNs+Transform", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }), + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because named type is transformed to float64", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(0)}, + wantEqual: true, + reason: "equal because times are identical", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because time is outside allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + wantEqual: false, + reason: "not equal because time is outside allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because both times are zero", + }, { + label: "EquateApproxTime", + x: time.Time{}, + y: time.Time{}.Add(1), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Time{}.Add(1), + y: time.Time{}, + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "not equal because zero time is always not equal not non-zero", + }, { + label: "EquateApproxTime", + x: time.Date(2409, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2000, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: false, + reason: "time difference overflows time.Duration", + }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{nil}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{errors.New("EOF")}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + wantEqual: false, + reason: "not equal because values do not match in deeply embedded field", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + wantEqual: false, + reason: "not equal because many deeply nested or embedded fields differ", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields at the highest levels", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields using fully-qualified field", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: false, + reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")}, + wantEqual: false, + reason: "not equal because highest-level field is not ignored: Foo3", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{})}, + wantEqual: false, + reason: "not equal because unexported fields mismatch", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{ + cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{}), + IgnoreFields(ParentStruct{}, "PublicStruct.private", "privateStruct.private", "private"), + }, + wantEqual: true, + reason: "equal because mismatching unexported fields are ignored", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + wantEqual: false, + reason: "not equal because 5 != 6", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantEqual: true, + reason: "equal because ints are ignored", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantPanic: true, + reason: "panics because bytes.Buffer has unexported fields", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "diff", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct{ io.Reader }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on interface type", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct { + io.Reader + io.Writer + fmt.Stringer + }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on multiple interface types", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + wantPanic: true, + reason: "panics because sync.Mutex has unexported fields", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)", + }, { + label: "IgnoreInterfaces", + x: struct{ mu *sync.Mutex }{}, + y: struct{ mu *sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on pointers", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})}, + wantEqual: false, + reason: "not equal because ParentStruct.private differs with AllowUnexported", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{IgnoreUnexported(ParentStruct{})}, + wantEqual: true, + reason: "equal because IgnoreUnexported ignored ParentStruct.private", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.private is ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: false, + reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + IgnoreUnexported(ParentStruct{}, PublicStruct{}), + }, + wantEqual: true, + reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}), + }, + wantEqual: false, + reason: "not equal since ParentStruct.privateStruct differs", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: true, + reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: false, + reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreFields+IgnoreTypes+IgnoreUnexported", + x: &Everything{ + MyInt: 5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3X(), + ParentStruct: ParentStruct{ + Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}, + }, + }, + y: &Everything{ + MyInt: -5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3Y(), + ParentStruct: ParentStruct{ + Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4}, + }, + }, + opts: []cmp.Option{ + IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"), + IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"), + IgnoreTypes(MyInt(0), PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because all Ignore options can be composed together", + }, { + label: "IgnoreSliceElements", + x: []int{1, 0, 2, 3, 0, 4, 0, 0}, + y: []int{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because zero elements are ignored", + }, { + label: "IgnoreSliceElements", + x: []MyInt{1, 0, 2, 3, 0, 4, 0, 0}, + y: []MyInt{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreSliceElements", + x: MyInts{1, 0, 2, 3, 0, 4, 0, 0}, + y: MyInts{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because the element type of MyInts is assignable to int", + }, { + label: "IgnoreSliceElements+EquateEmpty", + x: []MyInt{}, + y: []MyInt{0, 0, 0, 0}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored elements does not imply empty slice", + }, { + label: "IgnoreMapEntries", + x: map[string]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: true, + reason: "equal because uppercase keys are ignored", + }, { + label: "IgnoreMapEntries", + x: map[MyString]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[MyString]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyString is not assignable to string", + }, { + label: "IgnoreMapEntries", + x: map[string]MyInt{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]MyInt{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreMapEntries+EquateEmpty", + x: map[string]MyInt{"ONE": 1, "TWO": 2, "THREE": 3}, + y: nil, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored entries does not imply empty map", + }, { + label: "AcyclicTransformer", + x: "a\nb\nc\nd", + y: "a\nb\nd\nd", + opts: []cmp.Option{ + AcyclicTransformer("", func(s string) []string { return strings.Split(s, "\n") }), + }, + wantEqual: false, + reason: "not equal because 3rd line differs, but should not recurse infinitely", + }, { + label: "AcyclicTransformer", + x: []string{"foo", "Bar", "BAZ"}, + y: []string{"Foo", "BAR", "baz"}, + opts: []cmp.Option{ + AcyclicTransformer("", strings.ToUpper), + }, + wantEqual: true, + reason: "equal because of strings.ToUpper; AcyclicTransformer unnecessary, but check this still works", + }, { + label: "AcyclicTransformer", + x: "this is a sentence", + y: "this is a sentence", + opts: []cmp.Option{ + AcyclicTransformer("", strings.Fields), + }, + wantEqual: true, + reason: "equal because acyclic transformer splits on any contiguous whitespace", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + var gotEqual bool + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + gotPanic = fmt.Sprint(ex) + } + }() + gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...) + }() + switch { + case tt.reason == "": + t.Errorf("reason must be provided") + case gotPanic == "" && tt.wantPanic: + t.Errorf("expected Equal panic\nreason: %s", tt.reason) + case gotPanic != "" && !tt.wantPanic: + t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason) + case gotEqual != tt.wantEqual: + t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) + } + }) + } +} + +func TestPanic(t *testing.T) { + args := func(x ...interface{}) []interface{} { return x } + tests := []struct { + label string // Test name + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + reason string // The reason for the expected outcome + }{{ + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, 0.0), + reason: "zero margin and fraction is equivalent to exact equality", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(-0.1, 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, -0.1), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(math.NaN(), 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "NaN inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(1.0, 0.0), + reason: "fraction of 1.0 or greater is valid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, math.Inf(+1)), + reason: "margin of infinity is valid", + }, { + label: "EquateApproxTime", + fnc: EquateApproxTime, + args: args(time.Duration(-1)), + wantPanic: "margin must be a non-negative number", + reason: "negative duration is invalid", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ""), + wantPanic: "name must not be empty", + reason: "empty selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "."), + wantPanic: "name must not be empty", + reason: "single dot selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ".Alpha"), + reason: "dot-prefix is okay since Foo1.Alpha reads naturally", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha."), + wantPanic: "name must not be empty", + reason: "dot-suffix is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha "), + wantPanic: "does not exist", + reason: "identifiers must not have spaces", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Zulu"), + wantPanic: "does not exist", + reason: "name of non-existent field is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha.NoExist"), + wantPanic: "must be a struct", + reason: "cannot select into a non-struct", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(&Foo1{}, "Alpha"), + wantPanic: "must be a struct", + reason: "the type must be a struct (not pointer to a struct)", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "privateStruct"), + reason: "privateStruct field permitted since it is the default name of the embedded type", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "Public"), + reason: "Public field permitted since it is a forwarded field that is exported", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(struct{ privateStruct }{}, "private"), + wantPanic: "does not exist", + reason: "private field not permitted since it is a forwarded field that is unexported", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + reason: "empty input is valid", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(nil), + wantPanic: "cannot determine type", + reason: "input must not be nil value", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(0, 0, 0), + reason: "duplicate inputs of the same type is valid", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(nil), + wantPanic: "input must be an anonymous struct", + reason: "input must not be nil value", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(Foo1{}), + wantPanic: "input must be an anonymous struct", + reason: "input must not be a named struct type", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ _ io.Reader }{}), + wantPanic: "struct cannot have named fields", + reason: "input must not have named fields", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ Foo1 }{}), + wantPanic: "embedded field must be an interface type", + reason: "field types must be interfaces", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ EmptyInterface }{}), + wantPanic: "cannot ignore empty interface", + reason: "field types must not be the empty interface", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct { + io.Reader + io.Writer + io.Closer + io.ReadWriteCloser + }{}), + reason: "multiple interfaces may be specified, even if they overlap", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + reason: "empty input is valid", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(nil), + wantPanic: "invalid struct type", + reason: "input must not be nil value", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(&Foo1{}), + wantPanic: "invalid struct type", + reason: "input must be a struct type (not a pointer to a struct)", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(Foo1{}, struct{ x, X int }{}), + reason: "input may be named or unnamed structs", + }, { + label: "AcyclicTransformer", + fnc: AcyclicTransformer, + args: args("", "not a func"), + wantPanic: "invalid transformer function", + reason: "AcyclicTransformer has same input requirements as Transformer", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + // Prepare function arguments. + vf := reflect.ValueOf(tt.fnc) + var vargs []reflect.Value + for i, arg := range tt.args { + if arg == nil { + tf := vf.Type() + if i == tf.NumIn()-1 && tf.IsVariadic() { + vargs = append(vargs, reflect.Zero(tf.In(i).Elem())) + } else { + vargs = append(vargs, reflect.Zero(tf.In(i))) + } + } else { + vargs = append(vargs, reflect.ValueOf(arg)) + } + } + + // Call the function and capture any panics. + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + vf.Call(vargs) + }() + + switch { + case tt.reason == "": + t.Errorf("reason must be provided") + case tt.wantPanic == "" && gotPanic != "": + t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason) + case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic): + t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason) + } + }) + } +} diff --git a/cmp/cmpopts/xform.go b/cmp/cmpopts/xform.go new file mode 100644 index 0000000..9d65155 --- /dev/null +++ b/cmp/cmpopts/xform.go @@ -0,0 +1,35 @@ +// 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. + +package cmpopts + +import ( + "github.com/google/go-cmp/cmp" +) + +type xformFilter struct{ xform cmp.Option } + +func (xf xformFilter) filter(p cmp.Path) bool { + for _, ps := range p { + if t, ok := ps.(cmp.Transform); ok && t.Option() == xf.xform { + return false + } + } + return true +} + +// AcyclicTransformer returns a Transformer with a filter applied that ensures +// 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, 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 new file mode 100644 index 0000000..c82c062 --- /dev/null +++ b/cmp/compare.go @@ -0,0 +1,682 @@ +// 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 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. +// +// • 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 Exporter option. +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" +) + +// 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: +// +// • 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. +// +// • 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. +// +// 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. +// +// 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. +// +// 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(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 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 to 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) + s.reporters = append(s.reporters, reporter{r}) + s.compareAny(rootStep(x, y)) + d := r.String() + 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 = reflect.TypeOf((*interface{})(nil)).Elem() + 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 + 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. + recChecker recChecker + + // dynChecker triggers pseudo-random checks for option correctness. + // It is safe for statelessCompare to mutate this value. + dynChecker dynChecker + + // These fields, once set by processOption, will not change. + exporters []exporter // List of exporters for structs with unexported fields + opts Options // List of all fundamental and filter options +} + +func newState(opts []Option) *state { + // Always ensure a validator option exists to validate the inputs. + s := &state{opts: Options{validator{}}} + s.curPtrs.Init() + s.processOption(Options(opts)) + return s +} + +func (s *state) processOption(opt Option) { + switch opt := opt.(type) { + case nil: + case Options: + for _, o := range opt { + s.processOption(o) + } + case coreOption: + type filtered interface { + isFiltered() bool + } + if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { + panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) + } + s.opts = append(s.opts, opt) + case exporter: + s.exporters = append(s.exporters, opt) + case reporter: + s.reporters = append(s.reporters, opt) + default: + panic(fmt.Sprintf("unknown option %T", opt)) + } +} + +// 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(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, oldReporters := s.result, s.reporters + s.result = diff.Result{} // Reset result + s.reporters = nil // Remove reporters to avoid spurious printouts + s.compareAny(step) + res := s.result + s.result, s.reporters = oldResult, oldReporters + return res +} + +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) + + // 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) + } + + // Rule 1: Check whether an option applies on this node in the value tree. + if s.tryOptions(t, vx, vy) { + return + } + + // Rule 2: Check whether the type has a valid Equal method. + if s.tryMethod(t, vx, vy) { + return + } + + // Rule 3: Compare based on the underlying kind. + switch t.Kind() { + case reflect.Bool: + s.report(vx.Bool() == vy.Bool(), 0) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + 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(), 0) + case reflect.Float32, reflect.Float64: + s.report(vx.Float() == vy.Float(), 0) + case reflect.Complex64, reflect.Complex128: + s.report(vx.Complex() == vy.Complex(), 0) + case reflect.String: + s.report(vx.String() == vy.String(), 0) + case reflect.Chan, reflect.UnsafePointer: + s.report(vx.Pointer() == vy.Pointer(), 0) + case reflect.Func: + s.report(vx.IsNil() && vy.IsNil(), 0) + case reflect.Struct: + s.compareStruct(t, vx, vy) + case reflect.Slice, reflect.Array: + s.compareSlice(t, vx, vy) + case reflect.Map: + s.compareMap(t, vx, vy) + case reflect.Ptr: + s.comparePtr(t, vx, vy) + case reflect.Interface: + s.compareInterface(t, vx, vy) + default: + panic(fmt.Sprintf("%v kind not handled", t.Kind())) + } +} + +func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { + // Evaluate all filters and apply the remaining options. + if opt := s.opts.filter(s, t, vx, vy); opt != nil { + opt.apply(s, vx, vy) + return true + } + return false +} + +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) { + return false + } + + eq := s.callTTBFunc(m.Func, vx, vy) + s.report(eq, reportByMethod) + return true +} + +func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { + v = sanitizeValue(v, f.Type().In(0)) + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{v})[0] + } + + // Run the function twice and ensure that we get the same results back. + // We run in goroutines so that the race detector (if enabled) can detect + // 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 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 step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { + return want + } + panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) + } + return want +} + +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() + } + + // Swapping the input arguments is sufficient to check that + // f is symmetric and deterministic. + // We run in goroutines so that the race detector (if enabled) can detect + // 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.IsValid() || got.Bool() != want { + panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) + } + return want +} + +func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { + var ret reflect.Value + defer func() { + recover() // Ignore panics, let the other call to f panic instead + c <- ret + }() + 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). + if !flags.AtLeastGo110 { + if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { + return reflect.New(t).Elem() + } + } + return v +} + +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 + + var mayForce, mayForceInit bool + step := StructField{&structField{}} + for i := 0; i < t.NumField(); 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) + if step.unexported { + if step.name == "_" { + continue + } + // Defer checking of unexported fields until later to give an + // Ignore a chance to ignore the field. + if !vax.IsValid() || !vay.IsValid() { + // 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) + } + 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(step) + } +} + +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 + } + + // 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 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: + s.compareAny(withIndexes(ix, -1)) + ix++ + case diff.UniqueY: + s.compareAny(withIndexes(-1, iy)) + iy++ + default: + s.compareAny(withIndexes(ix, iy)) + ix++ + iy++ + } + } +} + +func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + 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{&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 + 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, + // there still isn't a sensible way to compare the values since + // a NaN key may map to multiple unordered values. + // The most reasonable way to compare NaNs would be to compare the + // set of values. However, this is impossible to do efficiently + // since set equality is provably an O(n^2) operation given only + // an Equal function. If we had a Less function or Hash function, + // this could be done in O(n*log(n)) or O(n), respectively. + // + // Rather than adding complex logic to deal with NaNs, make it + // the user's responsibility to compare such obscure maps. + 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) + } +} + +func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // 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}) + } +} + +// recChecker tracks the state needed to periodically perform checks that +// user provided transformers are not stuck in an infinitely recursive cycle. +type recChecker struct{ next int } + +// Check scans the Path for any recursive transformers and panics when any +// recursive transformers are detected. Note that the presence of a +// recursive Transformer does not necessarily imply an infinite cycle. +// As such, this check only activates after some minimal number of path steps. +func (rc *recChecker) Check(p Path) { + const minLen = 1 << 16 + if rc.next == 0 { + rc.next = minLen + } + if len(p) < rc.next { + return + } + rc.next <<= 1 + + // Check whether the same transformer has appeared at least twice. + var ss []string + m := map[Option]int{} + for _, ps := range p { + if t, ok := ps.(Transform); ok { + t := t.Option() + if m[t] == 1 { // Transformer was used exactly once before + tf := t.(*transformer).fnc.Type() + ss = append(ss, fmt.Sprintf("%v: %v => %v", t, tf.In(0), tf.Out(0))) + } + m[t]++ + } + } + if len(ss) > 0 { + const warning = "recursive set of Transformers detected" + const help = "consider using cmpopts.AcyclicTransformer" + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s:\n\t%s\n%s", warning, set, help)) + } +} + +// dynChecker tracks the state needed to periodically perform checks that +// user provided functions are symmetric and deterministic. +// The zero value is safe for immediate use. +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 +// the number of functions calls grows larger. +func (dc *dynChecker) Next() bool { + ok := dc.curr == dc.next + if ok { + dc.curr = 0 + dc.next++ + } + dc.curr++ + return ok +} + +// makeAddressable returns a value that is always addressable. +// It returns the input verbatim if it is already addressable, +// otherwise it creates a new value and returns an addressable copy. +func makeAddressable(v reflect.Value) reflect.Value { + if v.CanAddr() { + return v + } + vc := reflect.New(v.Type()).Elem() + vc.Set(v) + return vc +} diff --git a/cmp/compare_test.go b/cmp/compare_test.go new file mode 100644 index 0000000..4ffa0eb --- /dev/null +++ b/cmp/compare_test.go @@ -0,0 +1,2423 @@ +// 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_test + +import ( + "bytes" + "crypto/md5" + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "math" + "math/rand" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "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" +) + +func init() { + flags.Deterministic = true +} + +var update = flag.Bool("update", false, "update golden test files") + +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) + +func intPtr(n int) *int { return &n } + +type test struct { + label string // Test name + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + 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) { + if !*update { + t.Parallel() + } + var gotDiff, gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + gotDiff = cmp.Diff(tt.x, tt.y, tt.opts...) + }() + + // TODO: Require every test case to provide a reason. + if tt.wantPanic == "" { + if 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 gotDiff != wantDiff { + t.Fatalf("Diff:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, wantDiff, tt.reason) + } + } + gotEqual := gotDiff == "" + if gotEqual != tt.wantEqual { + t.Fatalf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) + } + } else { + if !strings.Contains(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 { + const label = "Comparer" + + type Iface1 interface { + Method() + } + type Iface2 interface { + Method() + } + + type tarHeader struct { + Name string + Mode int64 + Uid int + Gid int + Size int64 + ModTime time.Time + Typeflag byte + Linkname string + Uname string + Gname string + Devmajor int64 + Devminor int64 + AccessTime time.Time + ChangeTime time.Time + 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{ + Name: fmt.Sprintf("some/dummy/test/file%d", i), + Mode: 0664, Uid: i * 1000, Gid: i * 1000, Size: 1 << uint(i), + ModTime: now.Add(time.Duration(i) * time.Hour), + Uname: "user", Gname: "group", + Typeflag: tf, + }) + } + return hs + } + + return []test{{ + label: label, + x: nil, + y: nil, + wantEqual: true, + }, { + label: label, + x: 1, + y: 1, + wantEqual: true, + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Ignore()}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Comparer(func(_, _ interface{}) bool { return true })}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Transformer("λ", func(x interface{}) interface{} { return x })}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("λ", func(x int) float64 { return float64(x) }), + }, + wantPanic: "ambiguous set of applicable options", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + return len(p) > 0 && p[len(p)-1].Type().Kind() == reflect.Int + }, cmp.Options{cmp.Ignore(), cmp.Ignore(), cmp.Ignore()}), + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("λ", func(x int) float64 { return float64(x) }), + }, + wantEqual: true, + }, { + label: label, + opts: []cmp.Option{struct{ cmp.Option }{}}, + wantPanic: "unknown option", + }, { + label: label, + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 3}, + wantEqual: true, + }, { + label: label, + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 4}, + wantEqual: false, + }, { + label: label, + x: struct{ a, b, c int }{1, 2, 3}, + y: struct{ a, b, c int }{1, 2, 4}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(4)}, + wantEqual: true, + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + wantEqual: false, + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + }, + wantEqual: true, + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), + }, + wantEqual: true, + }, { + label: label, + x: &struct{ R *bytes.Buffer }{}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: true, + }, { + label: label, + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + wantEqual: false, + }, { + label: label, + 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, + }, { + label: label, + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + 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", + }, { + label: label, + 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, + }, { + label: label, + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + 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 { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + wantEqual: true, + }, { + label: label, + 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 { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + wantEqual: false, + }, { + label: label, + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + wantEqual: true, + }, { + label: label, + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 1 + b := &a + c := &b + return &c + }(), + wantEqual: false, + }, { + label: label, + x: []int{1, 2, 3, 4, 5}[:3], + y: []int{1, 2, 3}, + wantEqual: true, + }, { + 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() })}, + wantEqual: true, + }, { + 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() })}, + wantEqual: false, + }, { + label: label, + x: md5.Sum([]byte{'a'}), + y: md5.Sum([]byte{'b'}), + wantEqual: false, + }, { + label: label, + x: new(fmt.Stringer), + y: nil, + wantEqual: false, + }, { + label: label, + x: makeTarHeaders('0'), + y: makeTarHeaders('\x00'), + wantEqual: false, + }, { + label: label, + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(_, _ int) bool { + return rand.Intn(2) == 0 + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.FilterValues(func(_, _ int) bool { + return rand.Intn(2) == 0 + }, cmp.Ignore()), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + 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{ + cmp.Comparer(func(x, y int) bool { + return x < y + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + x: make([]string, 1000), + y: make([]string, 1000), + opts: []cmp.Option{ + cmp.Transformer("λ", func(x string) int { + return rand.Int() + }), + }, + wantPanic: "non-deterministic function detected", + }, { + // Make sure the dynamic checks don't raise a false positive for + // non-reflexive comparisons. + label: label, + x: make([]int, 10), + y: make([]int, 10), + opts: []cmp.Option{ + cmp.Transformer("λ", func(x int) float64 { + return math.NaN() + }), + }, + wantEqual: false, + }, { + // Ensure reasonable Stringer formatting of map keys. + label: label, + x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, + y: map[*pb.Stringer]*pb.Stringer(nil), + wantEqual: false, + }, { + // Ensure Stringer avoids double-quote escaping if possible. + label: label, + x: []*pb.Stringer{{`multi\nline\nline\nline`}}, + wantEqual: false, + }, { + label: label, + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y Iface1) bool { + return x == nil && y == nil + }), + }, + wantEqual: true, + }, { + label: label, + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.Transformer("λ", func(v Iface1) bool { + return v == nil + }), + }, + wantEqual: true, + }, { + label: label, + x: struct{ I Iface2 }{}, + y: struct{ I Iface2 }{}, + opts: []cmp.Option{ + cmp.FilterValues(func(x, y Iface1) bool { + return x == nil && y == nil + }, cmp.Ignore()), + }, + wantEqual: true, + }, { + 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"}}, + wantEqual: false, + }, { + label: label, + x: map[*int]string{ + new(int): "hello", + }, + y: map[*int]string{ + new(int): "world", + }, + wantEqual: false, + }, { + label: label, + x: intPtr(0), + y: intPtr(0), + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + // TODO: This diff output is unhelpful and should show the address. + wantEqual: false, + }, { + label: label, + 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, + 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, + x: namedWithUnexported{}, + y: namedWithUnexported{}, + wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".namedWithUnexported", + reason: "panic on named struct type with unexported field", + }, { + label: label, + 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, + 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", + }} +} + +func transformerTests() []test { + type StringBytes struct { + String string + Bytes []byte + } + + const label = "Transformer" + + transformOnce := func(name string, f interface{}) cmp.Option { + xform := cmp.Transformer(name, f) + return cmp.FilterPath(func(p cmp.Path) bool { + for _, ps := range p { + if tr, ok := ps.(cmp.Transform); ok && tr.Option() == xform { + return false + } + } + return true + }, xform) + } + + return []test{{ + label: label, + x: uint8(0), + y: uint8(1), + opts: []cmp.Option{ + cmp.Transformer("λ", func(in uint8) uint16 { return uint16(in) }), + cmp.Transformer("λ", func(in uint16) uint32 { return uint32(in) }), + cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), + }, + wantEqual: false, + }, { + label: label, + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("λ", func(in int) int { return in / 2 }), + cmp.Transformer("λ", func(in int) int { return in }), + }, + wantPanic: "ambiguous set of applicable options", + }, { + label: label, + x: []int{0, -5, 0, -1}, + y: []int{1, 3, 0, -5}, + opts: []cmp.Option{ + cmp.FilterValues( + func(x, y int) bool { return x+y >= 0 }, + cmp.Transformer("λ", func(in int) int64 { return int64(in / 2) }), + ), + cmp.FilterValues( + func(x, y int) bool { return x+y < 0 }, + cmp.Transformer("λ", func(in int) int64 { return int64(in) }), + ), + }, + wantEqual: false, + }, { + label: label, + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("λ", func(in int) interface{} { + if in == 0 { + return "zero" + } + return float64(in) + }), + }, + wantEqual: false, + }, { + label: label, + x: `{ + "firstName": "John", + "lastName": "Smith", + "age": 25, + "isAlive": true, + "address": { + "city": "Los Angeles", + "postalCode": "10021-3100", + "state": "CA", + "streetAddress": "21 2nd Street" + }, + "phoneNumbers": [{ + "type": "home", + "number": "212 555-4321" + },{ + "type": "office", + "number": "646 555-4567" + },{ + "number": "123 456-7890", + "type": "mobile" + }], + "children": [] + }`, + y: `{"firstName":"John","lastName":"Smith","isAlive":true,"age":25, + "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}`, + opts: []cmp.Option{ + transformOnce("ParseJSON", func(s string) (m map[string]interface{}) { + if err := json.Unmarshal([]byte(s), &m); err != nil { + panic(err) + } + return m + }), + }, + wantEqual: false, + }, { + label: label, + 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")) }), + }, + wantEqual: false, + }, { + 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", + }, { + 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", + }} +} + +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 + } + ) + + return []test{{ + label: label, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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", + }} +} + +func embeddedTests() []test { + const label = "EmbeddedStruct/" + + privateStruct := *new(ts.ParentStructA).PrivateStruct() + + createStructA := func(i int) ts.ParentStructA { + s := ts.ParentStructA{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructB := func(i int) ts.ParentStructB { + s := ts.ParentStructB{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructC := func(i int) ts.ParentStructC { + s := ts.ParentStructC{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructD := func(i int) ts.ParentStructD { + s := ts.ParentStructD{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructE := func(i int) ts.ParentStructE { + s := ts.ParentStructE{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructF := func(i int) ts.ParentStructF { + s := ts.ParentStructF{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Public = 5 + i + s.SetPrivate(6 + i) + return s + } + + createStructG := func(i int) *ts.ParentStructG { + s := ts.NewParentStructG() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructH := func(i int) *ts.ParentStructH { + s := ts.NewParentStructH() + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructI := func(i int) *ts.ParentStructI { + s := ts.NewParentStructI() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructJ := func(i int) *ts.ParentStructJ { + s := ts.NewParentStructJ() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Private().Public = 5 + i + s.Private().SetPrivate(6 + i) + s.Public.Public = 7 + i + s.Public.SetPrivate(8 + i) + return s + } + + // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122). + wantPanicNotGo110 := func(s string) string { + if !flags.AtLeastGo110 { + return "" + } + return s + } + + return []test{{ + label: label + "ParentStructA", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructA", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructA{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructB", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructB", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + wantEqual: false, + }, { + label: label + "ParentStructC", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructC", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructC{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructD", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructD", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + wantEqual: false, + }, { + label: label + "ParentStructE", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructF", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructG", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + wantPanic: wantPanicNotGo110("cannot handle unexported field"), + wantEqual: !flags.AtLeastGo110, + }, { + label: label + "ParentStructG", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructG{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructH", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + wantEqual: true, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructH", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructH{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + wantEqual: false, + }, { + label: label + "ParentStructI", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + wantPanic: wantPanicNotGo110("cannot handle unexported field"), + wantEqual: !flags.AtLeastGo110, + }, { + label: label + "ParentStructI", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + wantEqual: true, + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: true, + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + wantEqual: false, + }} +} + +func methodTests() []test { + 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 { + if len(p) == 0 { + return false + } + t := p[len(p)-1].Type() + if _, ok := t.MethodByName("Equal"); ok || t.Kind() == reflect.Ptr { + return false + } + if m, ok := reflect.PtrTo(t).MethodByName("Equal"); ok { + tf := m.Func.Type() + return !tf.IsVariadic() && tf.NumIn() == 2 && tf.NumOut() == 1 && + tf.In(0).AssignableTo(tf.In(1)) && tf.Out(0) == reflect.TypeOf(true) + } + return false + }, cmp.Transformer("Ref", func(x interface{}) interface{} { + v := reflect.ValueOf(x) + vp := reflect.New(v.Type()) + vp.Elem().Set(v) + return vp.Interface() + })) + + // For each of these types, there is an Equal method defined, which always + // 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"}, + wantEqual: true, + }, { + label: label + "StructA", + x: &ts.StructA{X: "NotEqual"}, + y: &ts.StructA{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructB", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + wantEqual: false, + }, { + label: label + "StructB", + x: ts.StructB{X: "NotEqual"}, + y: ts.StructB{X: "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantEqual: true, + }, { + label: label + "StructB", + x: &ts.StructB{X: "NotEqual"}, + y: &ts.StructB{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructC", + x: ts.StructC{X: "NotEqual"}, + y: ts.StructC{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructC", + x: &ts.StructC{X: "NotEqual"}, + y: &ts.StructC{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructD", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + wantEqual: false, + }, { + label: label + "StructD", + x: ts.StructD{X: "NotEqual"}, + y: ts.StructD{X: "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantEqual: true, + }, { + label: label + "StructD", + x: &ts.StructD{X: "NotEqual"}, + y: &ts.StructD{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructE", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + wantEqual: false, + }, { + label: label + "StructE", + x: ts.StructE{X: "NotEqual"}, + y: ts.StructE{X: "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantEqual: true, + }, { + label: label + "StructE", + x: &ts.StructE{X: "NotEqual"}, + y: &ts.StructE{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructF", + x: ts.StructF{X: "NotEqual"}, + y: ts.StructF{X: "not_equal"}, + wantEqual: false, + }, { + label: label + "StructF", + x: &ts.StructF{X: "NotEqual"}, + y: &ts.StructF{X: "not_equal"}, + wantEqual: true, + }, { + label: label + "StructA1", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + label: label + "StructA1", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + 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}, + wantEqual: true, + }, { + 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}, + wantEqual: false, + }, { + 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}, + wantEqual: true, + }, { + 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}, + wantEqual: false, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + 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}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + 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}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + 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"}, + wantEqual: true, + }, { + label: label + "StructA2", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + label: label + "StructA2", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + label: label + "StructB2", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + label: label + "StructB2", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, + wantEqual: true, + }, { + 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"}, + wantEqual: false, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + 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"}, + wantEqual: true, + }, { + label: label + "StructNo", + x: ts.StructNo{X: "NotEqual"}, + y: ts.StructNo{X: "not_equal"}, + wantEqual: false, + }, { + label: label + "AssignA", + x: ts.AssignA(func() int { return 0 }), + y: ts.AssignA(func() int { return 1 }), + wantEqual: true, + }, { + label: label + "AssignB", + x: ts.AssignB(struct{ A int }{0}), + y: ts.AssignB(struct{ A int }{1}), + wantEqual: true, + }, { + label: label + "AssignC", + x: ts.AssignC(make(chan bool)), + y: ts.AssignC(make(chan bool)), + wantEqual: true, + }, { + label: label + "AssignD", + x: ts.AssignD(make(chan bool)), + y: ts.AssignD(make(chan bool)), + wantEqual: true, + }} +} + +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 { + in XY + wantEqual bool + reason string + }{{ + in: func() XY { + x := new(P) + *x = x + y := new(P) + *y = y + return XY{x, y} + }(), + wantEqual: true, + }, { + in: func() XY { + x := new(P) + *x = x + y1, y2 := new(P), new(P) + *y1 = y2 + *y2 = y1 + return XY{x, y1} + }(), + wantEqual: false, + }, { + in: func() XY { + x := S{nil} + x[0] = x + y := S{nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + }, { + 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, + }, { + in: func() XY { + x := M{0: nil} + x[0] = x + y := M{0: nil} + y[0] = y + return XY{x, y} + }(), + wantEqual: true, + }, { + 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, + }, { + in: XY{makeGraph(), makeGraph()}, + wantEqual: true, + }, { + 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, + }, { + in: func() XY { + x := makeGraph() + y := makeGraph() + x["Buzz"].Bravos["BuzzBarBravo"] = &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + } + return XY{x, y} + }(), + wantEqual: false, + }} { + tests = append(tests, test{ + label: label, + x: tt.in.x, + y: tt.in.y, + wantEqual: tt.wantEqual, + reason: tt.reason, + }) + } + return tests +} + +func project1Tests() []test { + const label = "Project1" + + ignoreUnexported := cmpopts.IgnoreUnexported( + ts.EagleImmutable{}, + ts.DreamerImmutable{}, + ts.SlapImmutable{}, + ts.GoatImmutable{}, + ts.DonkeyImmutable{}, + ts.LoveRadius{}, + ts.SummerLove{}, + ts.SummerLoveSummary{}, + ) + + createEagle := func() ts.Eagle { + return ts.Eagle{ + Name: "eagle", + Hounds: []string{"buford", "tannen"}, + Desc: "some description", + Dreamers: []ts.Dreamer{{}, { + Name: "dreamer2", + Animal: []interface{}{ + ts.Goat{ + Target: "corporation", + Immutable: &ts.GoatImmutable{ + ID: "southbay", + State: (*pb.Goat_States)(intPtr(5)), + Started: now, + }, + }, + ts.Donkey{}, + }, + Amoeba: 53, + }}, + Slaps: []ts.Slap{{ + Name: "slapID", + Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, + Immutable: &ts.SlapImmutable{ + ID: "immutableSlap", + MildSlap: true, + Started: now, + LoveRadius: &ts.LoveRadius{ + Summer: &ts.SummerLove{ + Summary: &ts.SummerLoveSummary{ + Devices: []string{"foo", "bar", "baz"}, + ChangeType: []pb.SummerType{1, 2, 3}, + }, + }, + }, + }, + }}, + Immutable: &ts.EagleImmutable{ + ID: "eagleID", + Birthday: now, + MissingCall: (*pb.Eagle_MissingCalls)(intPtr(55)), + }, + } + } + + return []test{{ + label: label, + 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"}}, + }}}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + 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)}, + wantEqual: true, + }, { + label: label, + 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)}, + wantEqual: false, + }, { + label: label, + x: createEagle(), + y: createEagle(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: true, + }, { + label: label, + 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.Slaps[0].Immutable.MildSlap = false + return eg + }(), + y: func() ts.Eagle { + eg := createEagle() + devs := eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices + eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices = devs[:1] + return eg + }(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: false, + }} +} + +type germSorter []*pb.Germ + +func (gs germSorter) Len() int { return len(gs) } +func (gs germSorter) Less(i, j int) bool { return gs[i].String() < gs[j].String() } +func (gs germSorter) Swap(i, j int) { gs[i], gs[j] = gs[j], gs[i] } + +func project2Tests() []test { + const label = "Project2" + + sortGerms := cmp.Transformer("Sort", func(in []*pb.Germ) []*pb.Germ { + out := append([]*pb.Germ(nil), in...) // Make copy + sort.Sort(germSorter(out)) + return out + }) + + equalDish := cmp.Comparer(func(x, y *ts.Dish) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + px, err1 := x.Proto() + py, err2 := y.Proto() + if err1 != nil || err2 != nil { + return err1 == err2 + } + return pb.Equal(px, py) + }) + + createBatch := func() ts.GermBatch { + return ts.GermBatch{ + DirtyGerms: map[int32][]*pb.Germ{ + 17: { + {Stringer: pb.Stringer{X: "germ1"}}, + }, + 18: { + {Stringer: pb.Stringer{X: "germ2"}}, + {Stringer: pb.Stringer{X: "germ3"}}, + {Stringer: pb.Stringer{X: "germ4"}}, + }, + }, + GermMap: map[int32]*pb.Germ{ + 13: {Stringer: pb.Stringer{X: "germ13"}}, + 21: {Stringer: pb.Stringer{X: "germ21"}}, + }, + DishMap: map[int32]*ts.Dish{ + 0: ts.CreateDish(nil, io.EOF), + 1: ts.CreateDish(nil, io.ErrUnexpectedEOF), + 2: ts.CreateDish(&pb.Dish{Stringer: pb.Stringer{X: "dish"}}, nil), + }, + HasPreviousResult: true, + DirtyID: 10, + GermStrain: 421, + InfectedAt: now, + } + } + + return []test{{ + label: label, + x: createBatch(), + y: createBatch(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createBatch(), + y: createBatch(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + }, { + label: label, + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, + wantEqual: false, + }, { + label: label, + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, + }, { + label: label, + x: func() ts.GermBatch { + gb := createBatch() + delete(gb.DirtyGerms, 17) + gb.DishMap[1] = nil + return gb + }(), + y: func() ts.GermBatch { + gb := createBatch() + gb.DirtyGerms[18] = gb.DirtyGerms[18][:2] + gb.GermStrain = 22 + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: false, + }} +} + +func project3Tests() []test { + const label = "Project3" + + allowVisibility := cmp.AllowUnexported(ts.Dirt{}) + + ignoreLocker := cmpopts.IgnoreInterfaces(struct{ sync.Locker }{}) + + transformProtos := cmp.Transformer("λ", func(x pb.Dirt) *pb.Dirt { + return &x + }) + + equalTable := cmp.Comparer(func(x, y ts.Table) bool { + tx, ok1 := x.(*ts.MockTable) + ty, ok2 := y.(*ts.MockTable) + if !ok1 || !ok2 { + panic("table type must be MockTable") + } + return cmp.Equal(tx.State(), ty.State()) + }) + + createDirt := func() (d ts.Dirt) { + d.SetTable(ts.CreateMockTable([]string{"a", "b", "c"})) + d.SetTimestamp(12345) + d.Discord = 554 + d.Proto = pb.Dirt{Stringer: pb.Stringer{X: "proto"}} + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{X: "potter"}}, + "albus": {Stringer: pb.Stringer{X: "dumbledore"}}, + }) + d.SetLastTime(54321) + return d + } + + return []test{{ + label: label, + x: createDirt(), + y: createDirt(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: true, + }, { + label: label, + x: func() ts.Dirt { + d := createDirt() + d.SetTable(ts.CreateMockTable([]string{"a", "c"})) + d.Proto = pb.Dirt{Stringer: pb.Stringer{X: "blah"}} + return d + }(), + y: func() ts.Dirt { + d := createDirt() + d.Discord = 500 + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{X: "otter"}}, + }) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: false, + }} +} + +func project4Tests() []test { + const label = "Project4" + + allowVisibility := cmp.AllowUnexported( + ts.Cartel{}, + ts.Headquarter{}, + ts.Poison{}, + ) + + transformProtos := cmp.Transformer("λ", func(x pb.Restrictions) *pb.Restrictions { + return &x + }) + + createCartel := func() ts.Cartel { + var p ts.Poison + p.SetPoisonType(5) + p.SetExpiration(now) + p.SetManufacturer("acme") + + var hq ts.Headquarter + hq.SetID(5) + hq.SetLocation("moon") + hq.SetSubDivisions([]string{"alpha", "bravo", "charlie"}) + hq.SetMetaData(&pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}) + hq.SetPublicMessage([]byte{1, 2, 3, 4, 5}) + hq.SetHorseBack("abcdef") + hq.SetStatus(44) + + var c ts.Cartel + c.Headquarter = hq + c.SetSource("mars") + c.SetCreationTime(now) + c.SetBoss("al capone") + c.SetPoisons([]*ts.Poison{&p}) + + return c + } + + return []test{{ + label: label, + x: createCartel(), + y: createCartel(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, cmp.Comparer(pb.Equal)}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: true, + }, { + label: label, + x: func() ts.Cartel { + d := createCartel() + var p1, p2 ts.Poison + p1.SetPoisonType(1) + p1.SetExpiration(now) + p1.SetManufacturer("acme") + p2.SetPoisonType(2) + p2.SetManufacturer("acme2") + d.SetPoisons([]*ts.Poison{&p1, &p2}) + return d + }(), + y: func() ts.Cartel { + d := createCartel() + d.SetSubDivisions([]string{"bravo", "charlie"}) + d.SetPublicMessage([]byte{1, 2, 4, 3, 5}) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: false, + }} +} + +// 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..bc1932e --- /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.md 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} + // +: <invalid reflect.Value> +} diff --git a/cmp/example_test.go b/cmp/example_test.go new file mode 100644 index 0000000..2689efb --- /dev/null +++ b/cmp/example_test.go @@ -0,0 +1,376 @@ +// 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_test + +import ( + "fmt" + "math" + "net" + "reflect" + "sort" + "strings" + "time" + + "github.com/google/go-cmp/cmp" +) + +// TODO: Re-write these examples in terms of how you actually use the +// 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 to print out a human-readable report of differences for tests +// comparing nested or structured data. +func ExampleDiff_testing() { + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) + } + + // Output: + // MakeGatewayInfo() mismatch (-want +got): + // cmp_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: net.IPMask{0xff, 0xff, 0x00, 0x00}, + // 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 +// comparer on floats that determines two values to be equal if they are within +// some range of each other. +// +// This example is for demonstrative purposes; use cmpopts.EquateApprox instead. +func ExampleOption_approximateFloats() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + }) + + x := []float64{1.0, 1.1, 1.2, math.Pi} + y := []float64{1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Normal floating-point arithmetic defines == to be false when comparing +// NaN with itself. In certain cases, this is not the desired property. +// +// This example is for demonstrative purposes; use cmpopts.EquateNaNs instead. +func ExampleOption_equalNaNs() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + 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 + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// To have floating-point comparisons combine both properties of NaN being +// equal to itself and also approximate equality of values, filters are needed +// to restrict the scope of the comparison so that they are composable. +// +// This example is for demonstrative purposes; +// use cmpopts.EquateNaNs and cmpopts.EquateApprox instead. +func ExampleOption_equalNaNsAndApproximateFloats() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + opts := cmp.Options{ + // This option declares that a float64 comparison is equal only if + // both inputs are NaN. + cmp.FilterValues(func(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) + }, alwaysEqual), + + // This option declares approximate equality on float64s only if + // both inputs are not NaN. + cmp.FilterValues(func(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) + }, cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + })), + } + + x := []float64{math.NaN(), 1.0, 1.1, 1.2, math.Pi} + y := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opts)) + fmt.Println(cmp.Equal(y, z, opts)) + fmt.Println(cmp.Equal(z, x, opts)) + + // Output: + // true + // false + // false +} + +// Sometimes, an empty map or slice is considered equal to an allocated one +// of zero length. +// +// This example is for demonstrative purposes; use cmpopts.EquateEmpty instead. +func ExampleOption_equalEmpty() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + // This option handles slices and maps of any type. + opt := cmp.FilterValues(func(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (vx.IsValid() && vy.IsValid() && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) + }, alwaysEqual) + + type S struct { + A []int + B map[string]bool + } + x := S{nil, make(map[string]bool, 100)} + y := S{make([]int, 0, 200), nil} + z := S{[]int{0}, nil} // []int has a single element (i.e., not empty) + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Two slices may be considered equal if they have the same elements, +// regardless of the order that they appear in. Transformations can be used +// to sort the slice. +// +// This example is for demonstrative purposes; use cmpopts.SortSlices instead. +func ExampleOption_sortedSlice() { + // This Transformer sorts a []int. + trans := cmp.Transformer("Sort", func(in []int) []int { + out := append([]int(nil), in...) // Copy input to avoid mutating it + sort.Ints(out) + return out + }) + + x := struct{ Ints []int }{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} + y := struct{ Ints []int }{[]int{2, 8, 0, 9, 6, 1, 4, 7, 3, 5}} + z := struct{ Ints []int }{[]int{0, 0, 1, 2, 3, 4, 5, 6, 7, 8}} + + fmt.Println(cmp.Equal(x, y, trans)) + fmt.Println(cmp.Equal(y, z, trans)) + fmt.Println(cmp.Equal(z, x, trans)) + + // Output: + // true + // false + // false +} + +type otherString string + +func (x otherString) Equal(y otherString) bool { + return strings.ToLower(string(x)) == strings.ToLower(string(y)) +} + +// 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() { + // Suppose otherString.Equal performs a case-insensitive equality, + // which is too loose for our needs. + // We can avoid the methods of otherString by declaring a new type. + type myString otherString + + // This transformer converts otherString to myString, allowing Equal to use + // other Options to determine equality. + trans := cmp.Transformer("", func(in otherString) myString { + return myString(in) + }) + + x := []otherString{"foo", "bar", "baz"} + y := []otherString{"fOO", "bAr", "Baz"} // Same as before, but with different case + + fmt.Println(cmp.Equal(x, y)) // Equal because of case-insensitivity + fmt.Println(cmp.Equal(x, y, trans)) // Not equal because of more exact equality + + // Output: + // true + // false +} + +func roundF64(z float64) float64 { + if z < 0 { + return math.Ceil(z - 0.5) + } + return math.Floor(z + 0.5) +} + +// The complex numbers complex64 and complex128 can really just be decomposed +// into a pair of float32 or float64 values. It would be convenient to be able +// define only a single comparator on float64 and have float32, complex64, and +// complex128 all be able to use that comparator. Transformations can be used +// to handle this. +func ExampleOption_transformComplex() { + opts := []cmp.Option{ + // This transformer decomposes complex128 into a pair of float64s. + cmp.Transformer("T1", func(in complex128) (out struct{ Real, Imag float64 }) { + out.Real, out.Imag = real(in), imag(in) + return out + }), + // This transformer converts complex64 to complex128 to allow the + // above transform to take effect. + cmp.Transformer("T2", func(in complex64) complex128 { + return complex128(in) + }), + // This transformer converts float32 to float64. + cmp.Transformer("T3", func(in float32) float64 { + return float64(in) + }), + // This equality function compares float64s as rounded integers. + cmp.Comparer(func(x, y float64) bool { + return roundF64(x) == roundF64(y) + }), + } + + x := []interface{}{ + complex128(3.0), complex64(5.1 + 2.9i), float32(-1.2), float64(12.3), + } + y := []interface{}{ + complex128(3.1), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + z := []interface{}{ + complex128(3.8), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + + fmt.Println(cmp.Equal(x, y, opts...)) + fmt.Println(cmp.Equal(y, z, opts...)) + fmt.Println(cmp.Equal(z, x, opts...)) + + // Output: + // true + // false + // 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..dfa5d21 --- /dev/null +++ b/cmp/export_panic.go @@ -0,0 +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. + +// +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..351f1a3 --- /dev/null +++ b/cmp/export_unsafe.go @@ -0,0 +1,35 @@ +// 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 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 new file mode 100644 index 0000000..fe98dcc --- /dev/null +++ b/cmp/internal/diff/debug_disable.go @@ -0,0 +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. + +// +build !cmp_debug + +package diff + +var debug debugger + +type debugger struct{} + +func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { + return f +} +func (debugger) Update() {} +func (debugger) Finish() {} diff --git a/cmp/internal/diff/debug_enable.go b/cmp/internal/diff/debug_enable.go new file mode 100644 index 0000000..597b6ae --- /dev/null +++ b/cmp/internal/diff/debug_enable.go @@ -0,0 +1,122 @@ +// 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 cmp_debug + +package diff + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// The algorithm can be seen running in real-time by enabling debugging: +// go test -tags=cmp_debug -v +// +// Example output: +// === RUN TestDifference/#34 +// ┌───────────────────────────────┐ +// │ \ · · · · · · · · · · · · · · │ +// │ · # · · · · · · · · · · · · · │ +// │ · \ · · · · · · · · · · · · · │ +// │ · · \ · · · · · · · · · · · · │ +// │ · · · X # · · · · · · · · · · │ +// │ · · · # \ · · · · · · · · · · │ +// │ · · · · · # # · · · · · · · · │ +// │ · · · · · # \ · · · · · · · · │ +// │ · · · · · · · \ · · · · · · · │ +// │ · · · · · · · · \ · · · · · · │ +// │ · · · · · · · · · \ · · · · · │ +// │ · · · · · · · · · · \ · · # · │ +// │ · · · · · · · · · · · \ # # · │ +// │ · · · · · · · · · · · # # # · │ +// │ · · · · · · · · · · # # # # · │ +// │ · · · · · · · · · # # # # # · │ +// │ · · · · · · · · · · · · · · \ │ +// └───────────────────────────────┘ +// [.Y..M.XY......YXYXY.|] +// +// The grid represents the edit-graph where the horizontal axis represents +// list X and the vertical axis represents list Y. The start of the two lists +// is the top-left, while the ends are the bottom-right. The '·' represents +// an unexplored node in the graph. The '\' indicates that the two symbols +// from list X and Y are equal. The 'X' indicates that two symbols are similar +// (but not exactly equal) to each other. The '#' indicates that the two symbols +// are different (and not similar). The algorithm traverses this graph trying to +// make the paths starting in the top-left and the bottom-right connect. +// +// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents +// the currently established path from the forward and reverse searches, +// separated by a '|' character. + +const ( + updateDelay = 100 * time.Millisecond + finishDelay = 500 * time.Millisecond + ansiTerminal = true // ANSI escape codes used to move terminal cursor +) + +var debug debugger + +type debugger struct { + sync.Mutex + p1, p2 EditScript + fwdPath, revPath *EditScript + grid []byte + lines int +} + +func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { + dbg.Lock() + dbg.fwdPath, dbg.revPath = p1, p2 + top := "┌─" + strings.Repeat("──", nx) + "┐\n" + row := "│ " + strings.Repeat("· ", nx) + "│\n" + btm := "└─" + strings.Repeat("──", nx) + "┘\n" + dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) + dbg.lines = strings.Count(dbg.String(), "\n") + fmt.Print(dbg) + + // Wrap the EqualFunc so that we can intercept each result. + return func(ix, iy int) (r Result) { + cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] + for i := range cell { + cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot + } + switch r = f(ix, iy); { + case r.Equal(): + cell[0] = '\\' + case r.Similar(): + cell[0] = 'X' + default: + cell[0] = '#' + } + return + } +} + +func (dbg *debugger) Update() { + dbg.print(updateDelay) +} + +func (dbg *debugger) Finish() { + dbg.print(finishDelay) + dbg.Unlock() +} + +func (dbg *debugger) String() string { + dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] + for i := len(*dbg.revPath) - 1; i >= 0; i-- { + dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) + } + return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) +} + +func (dbg *debugger) print(d time.Duration) { + if ansiTerminal { + fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor + } + fmt.Print(dbg) + time.Sleep(d) +} diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go new file mode 100644 index 0000000..3d2e426 --- /dev/null +++ b/cmp/internal/diff/diff.go @@ -0,0 +1,372 @@ +// 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 diff implements an algorithm for producing edit-scripts. +// The edit-script is a sequence of operations needed to transform one list +// of symbols into another (or vice-versa). The edits allowed are insertions, +// deletions, and modifications. The summation of all edits is called the +// Levenshtein distance as this problem is well-known in computer science. +// +// This package prioritizes performance over accuracy. That is, the run time +// is more important than obtaining a minimal Levenshtein distance. +package diff + +// EditType represents a single operation within an edit-script. +type EditType uint8 + +const ( + // Identity indicates that a symbol pair is identical in both list X and Y. + Identity EditType = iota + // UniqueX indicates that a symbol only exists in X and not Y. + UniqueX + // UniqueY indicates that a symbol only exists in Y and not X. + UniqueY + // Modified indicates that a symbol pair is a modification of each other. + Modified +) + +// EditScript represents the series of differences between two lists. +type EditScript []EditType + +// String returns a human-readable string representing the edit-script where +// Identity, UniqueX, UniqueY, and Modified are represented by the +// '.', 'X', 'Y', and 'M' characters, respectively. +func (es EditScript) String() string { + b := make([]byte, len(es)) + for i, e := range es { + switch e { + case Identity: + b[i] = '.' + case UniqueX: + b[i] = 'X' + case UniqueY: + b[i] = 'Y' + case Modified: + b[i] = 'M' + default: + panic("invalid edit-type") + } + } + return string(b) +} + +// stats returns a histogram of the number of each type of edit operation. +func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { + for _, e := range es { + switch e { + case Identity: + s.NI++ + case UniqueX: + s.NX++ + case UniqueY: + s.NY++ + case Modified: + s.NM++ + default: + panic("invalid edit-type") + } + } + return +} + +// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if +// lists X and Y are equal. +func (es EditScript) Dist() int { return len(es) - es.stats().NI } + +// LenX is the length of the X list. +func (es EditScript) LenX() int { return len(es) - es.stats().NY } + +// LenY is the length of the Y list. +func (es EditScript) LenY() int { return len(es) - es.stats().NX } + +// EqualFunc reports whether the symbols at indexes ix and iy are equal. +// When called by Difference, the index is guaranteed to be within nx and ny. +type EqualFunc func(ix int, iy int) Result + +// Result is the result of comparison. +// 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 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 NumSame to NumDiff to determine similarity may change. +func (r Result) Similar() bool { + // Use NumSame+1 to offset NumSame so that binary comparisons are similar. + return r.NumSame+1 >= r.NumDiff +} + +// 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() +// +// 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 +// favors performance over optimality. The exact output is not guaranteed to +// be stable and may change over time. +func Difference(nx, ny int, f EqualFunc) (es EditScript) { + // This algorithm is based on traversing what is known as an "edit-graph". + // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" + // by Eugene W. Myers. Since D can be as large as N itself, this is + // effectively O(N^2). Unlike the algorithm from that paper, we are not + // interested in the optimal path, but at least some "decent" path. + // + // For example, let X and Y be lists of symbols: + // X = [A B C A B B A] + // Y = [C B A B A C] + // + // The edit-graph can be drawn as the following: + // A B C A B B A + // ┌─────────────┐ + // C │_|_|\|_|_|_|_│ 0 + // B │_|\|_|_|\|\|_│ 1 + // A │\|_|_|\|_|_|\│ 2 + // B │_|\|_|_|\|\|_│ 3 + // A │\|_|_|\|_|_|\│ 4 + // C │ | |\| | | | │ 5 + // └─────────────┘ 6 + // 0 1 2 3 4 5 6 7 + // + // List X is written along the horizontal axis, while list Y is written + // along the vertical axis. At any point on this grid, if the symbol in + // list X matches the corresponding symbol in list Y, then a '\' is drawn. + // The goal of any minimal edit-script algorithm is to find a path from the + // top-left corner to the bottom-right corner, while traveling through the + // fewest horizontal or vertical edges. + // A horizontal edge is equivalent to inserting a symbol from list X. + // A vertical edge is equivalent to inserting a symbol from list Y. + // 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 + // + // In general: + // • 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)} + fwdFrontier := fwdPath.point // Forward search frontier + revFrontier := revPath.point // Reverse search frontier + + // Search budget bounds the cost of searching for better paths. + // The longest sequence of non-matching symbols that can be tolerated is + // approximately the square-root of the search budget. + searchBudget := 4 * (nx + ny) // O(n) + + // 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. + // + // 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 { + // Forward search from the beginning. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{fwdFrontier.X + z, fwdFrontier.Y - z} + switch { + case p.X >= revPath.X || p.Y < fwdPath.Y: + stop1 = true // Hit top-right corner + case p.Y >= revPath.Y || p.X < fwdPath.X: + stop2 = true // Hit bottom-left corner + case f(p.X, p.Y).Equal(): + // Match found, so connect the path to this point. + fwdPath.connect(p, f) + fwdPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(fwdPath.X, fwdPath.Y).Equal() { + break + } + fwdPath.append(Identity) + } + fwdFrontier = fwdPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards reverse point. + if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { + fwdFrontier.X++ + } else { + fwdFrontier.Y++ + } + + // Reverse search from the end. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{revFrontier.X - z, revFrontier.Y + z} + switch { + case fwdPath.X >= p.X || revPath.Y < p.Y: + stop1 = true // Hit bottom-left corner + case fwdPath.Y >= p.Y || revPath.X < p.X: + stop2 = true // Hit top-right corner + case f(p.X-1, p.Y-1).Equal(): + // Match found, so connect the path to this point. + revPath.connect(p, f) + revPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(revPath.X-1, revPath.Y-1).Equal() { + break + } + revPath.append(Identity) + } + revFrontier = revPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards forward point. + if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { + revFrontier.X-- + } else { + revFrontier.Y-- + } + } + + // 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-- { + t := revPath.es[i] + revPath.es = revPath.es[:i] + fwdPath.append(t) + } + debug.Finish() + return fwdPath.es +} + +type path struct { + dir int // +1 if forward, -1 if reverse + point // Leading point of the EditScript path + es EditScript +} + +// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types +// to the edit-script to connect p.point to dst. +func (p *path) connect(dst point, f EqualFunc) { + if p.dir > 0 { + // Connect in forward direction. + for dst.X > p.X && dst.Y > p.Y { + switch r := f(p.X, p.Y); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case dst.X-p.X >= dst.Y-p.Y: + p.append(UniqueX) + default: + p.append(UniqueY) + } + } + for dst.X > p.X { + p.append(UniqueX) + } + for dst.Y > p.Y { + p.append(UniqueY) + } + } else { + // Connect in reverse direction. + for p.X > dst.X && p.Y > dst.Y { + switch r := f(p.X-1, p.Y-1); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case p.Y-dst.Y >= p.X-dst.X: + p.append(UniqueY) + default: + p.append(UniqueX) + } + } + for p.X > dst.X { + p.append(UniqueX) + } + for p.Y > dst.Y { + p.append(UniqueY) + } + } +} + +func (p *path) append(t EditType) { + p.es = append(p.es, t) + switch t { + case Identity, Modified: + p.add(p.dir, p.dir) + case UniqueX: + p.add(p.dir, 0) + case UniqueY: + p.add(0, p.dir) + } + debug.Update() +} + +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 { + x = ^x + } + return x >> 1 +} diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go new file mode 100644 index 0000000..ef39077 --- /dev/null +++ b/cmp/internal/diff/diff_test.go @@ -0,0 +1,444 @@ +// 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 diff + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "unicode" +) + +func TestDifference(t *testing.T) { + tests := []struct { + // Before passing x and y to Difference, we strip all spaces so that + // they can be used by the test author to indicate a missing symbol + // in one of the lists. + x, y string + want string + }{{ + x: "", + y: "", + want: "", + }, { + x: "#", + y: "#", + want: ".", + }, { + x: "##", + y: "# ", + want: ".X", + }, { + x: "a#", + y: "A ", + want: "MX", + }, { + x: "#a", + y: " A", + want: "XM", + }, { + x: "# ", + y: "##", + want: ".Y", + }, { + x: " #", + y: "@#", + want: "Y.", + }, { + x: "@#", + y: " #", + want: "X.", + }, { + x: "##########0123456789", + y: " 0123456789", + want: "XXXXXXXXXX..........", + }, { + x: " 0123456789", + y: "##########0123456789", + want: "YYYYYYYYYY..........", + }, { + x: "#####0123456789#####", + y: " 0123456789 ", + want: "XXXXX..........XXXXX", + }, { + x: " 0123456789 ", + y: "#####0123456789#####", + want: "YYYYY..........YYYYY", + }, { + x: "01234##########56789", + y: "01234 56789", + want: ".....XXXXXXXXXX.....", + }, { + x: "01234 56789", + y: "01234##########56789", + want: ".....YYYYYYYYYY.....", + }, { + x: "0123456789##########", + y: "0123456789 ", + want: "..........XXXXXXXXXX", + }, { + x: "0123456789 ", + y: "0123456789##########", + want: "..........YYYYYYYYYY", + }, { + x: "abcdefghij0123456789", + y: "ABCDEFGHIJ0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "ABCDEFGHIJ0123456789", + y: "abcdefghij0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "01234abcdefghij56789", + y: "01234ABCDEFGHIJ56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "01234ABCDEFGHIJ56789", + y: "01234abcdefghij56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "0123456789abcdefghij", + y: "0123456789ABCDEFGHIJ", + want: "..........MMMMMMMMMM", + }, { + x: "0123456789ABCDEFGHIJ", + y: "0123456789abcdefghij", + want: "..........MMMMMMMMMM", + }, { + x: "ABCDEFGHIJ0123456789 ", + y: " 0123456789abcdefghij", + want: "XXXXXXXXXX..........YYYYYYYYYY", + }, { + x: " 0123456789abcdefghij", + y: "ABCDEFGHIJ0123456789 ", + want: "YYYYYYYYYY..........XXXXXXXXXX", + }, { + x: "ABCDE0123456789 FGHIJ", + y: " 0123456789abcdefghij", + want: "XXXXX..........YYYYYMMMMM", + }, { + x: " 0123456789abcdefghij", + y: "ABCDE0123456789 FGHIJ", + want: "YYYYY..........XXXXXMMMMM", + }, { + x: "ABCDE01234F G H I J 56789 ", + y: " 01234 a b c d e56789fghij", + want: "XXXXX.....XYXYXYXYXY.....YYYYY", + }, { + x: " 01234a b c d e 56789fghij", + y: "ABCDE01234 F G H I J56789 ", + want: "YYYYY.....XYXYXYXYXY.....XXXXX", + }, { + x: "FGHIJ01234ABCDE56789 ", + y: " 01234abcde56789fghij", + want: "XXXXX.....MMMMM.....YYYYY", + }, { + x: " 01234abcde56789fghij", + y: "FGHIJ01234ABCDE56789 ", + want: "YYYYY.....MMMMM.....XXXXX", + }, { + x: "ABCAB BA ", + y: " C BABAC", + want: "XX.X.Y..Y", + }, { + x: "# #### ###", + y: "#y####yy###", + want: ".Y....YY...", + }, { + x: "# #### # ##x#x", + y: "#y####y y## # ", + want: ".Y....YXY..X.X", + }, { + x: "###z#z###### x #", + y: "#y##Z#Z###### yy#", + want: ".Y..M.M......XYY.", + }, { + x: "0 12z3x 456789 x x 0", + y: "0y12Z3 y456789y y y0", + want: ".Y..M.XY......YXYXY.", + }, { + x: "0 2 4 6 8 ..................abXXcdEXF.ghXi", + y: " 1 3 5 7 9..................AB CDE F.GH I", + want: "XYXYXYXYXY..................MMXXMM.X..MMXM", + }, { + x: "I HG.F EDC BA..................9 7 5 3 1 ", + y: "iXhg.FXEdcXXba.................. 8 6 4 2 0", + want: "MYMM..Y.MMYYMM..................XYXYXYXYXY", + }, { + x: "x1234", + y: " 1234", + want: "X....", + }, { + x: "x123x4", + y: " 123 4", + want: "X...X.", + }, { + x: "x1234x56", + y: " 1234 ", + want: "X....XXX", + }, { + x: "x1234xxx56", + y: " 1234 56", + want: "X....XXX..", + }, { + x: ".1234...ab", + y: " 1234 AB", + want: "X....XXXMM", + }, { + x: "x1234xxab.", + y: " 1234 AB ", + want: "X....XXMMX", + }, { + x: " 0123456789", + y: "9012345678 ", + want: "Y.........X", + }, { + x: " 0123456789", + y: "8901234567 ", + want: "YY........XX", + }, { + x: " 0123456789", + y: "7890123456 ", + want: "YYY.......XXX", + }, { + x: " 0123456789", + y: "6789012345 ", + want: "YYYY......XXXX", + }, { + x: "0123456789 ", + y: " 5678901234", + want: "XXXXX.....YYYYY", + }, { + x: "0123456789 ", + y: " 4567890123", + want: "XXXX......YYYY", + }, { + x: "0123456789 ", + y: " 3456789012", + want: "XXX.......YYY", + }, { + x: "0123456789 ", + y: " 2345678901", + want: "XX........YY", + }, { + x: "0123456789 ", + y: " 1234567890", + want: "X.........Y", + }, { + x: "0 1 2 3 45 6 7 8 9 ", + y: " 9 8 7 6 54 3 2 1 0", + want: "XYXYXYXYX.YXYXYXYXY", + }, { + x: "0 1 2345678 9 ", + y: " 6 72 5 819034", + want: "XYXY.XX.XX.Y.YYY", + }, { + x: "F B Q M O I G T L N72X90 E 4S P 651HKRJU DA 83CVZW", + y: " 5 W H XO10R9IV K ZLCTAJ8P3N SEQM4 7 2G6 UBD F ", + want: "XYXYXYXY.YYYY.YXYXY.YYYYYYY.XXXXXY.YY.XYXYY.XXXXXX.Y.XYXXXXXX", + }} + + for _, tt := range tests { + t.Run("", func(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) + } + }) + } +} + +func TestDifferenceFuzz(t *testing.T) { + tests := []struct{ px, py, pm float32 }{ + {px: 0.0, py: 0.0, pm: 0.1}, + {px: 0.0, py: 0.1, pm: 0.0}, + {px: 0.1, py: 0.0, pm: 0.0}, + {px: 0.0, py: 0.1, pm: 0.1}, + {px: 0.1, py: 0.0, pm: 0.1}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.1, pm: 0.2}, + {px: 0.1, py: 0.3, pm: 0.2}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.3, pm: 0.3}, + {px: 0.1, py: 0.1, pm: 0.5}, + {px: 0.4, py: 0.1, pm: 0.5}, + {px: 0.3, py: 0.2, pm: 0.5}, + {px: 0.2, py: 0.3, pm: 0.5}, + {px: 0.1, py: 0.4, pm: 0.5}, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("P%d", i), func(t *testing.T) { + // Sweep from 1B to 1KiB. + for n := 1; n <= 1024; n <<= 1 { + t.Run(fmt.Sprintf("N%d", n), func(t *testing.T) { + for j := 0; j < 10; j++ { + x, y := generateStrings(n, tt.px, tt.py, tt.pm, int64(j)) + testStrings(t, x, y) + } + }) + } + }) + } +} + +func BenchmarkDifference(b *testing.B) { + for n := 1 << 10; n <= 1<<20; n <<= 2 { + b.Run(fmt.Sprintf("N%d", n), func(b *testing.B) { + x, y := generateStrings(n, 0.05, 0.05, 0.10, 0) + b.ReportAllocs() + b.SetBytes(int64(len(x) + len(y))) + for i := 0; i < b.N; i++ { + Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + } + }) + } +} + +func generateStrings(n int, px, py, pm float32, seed int64) (string, string) { + if px+py+pm > 1.0 { + panic("invalid probabilities") + } + py += px + pm += py + + b := make([]byte, n) + r := rand.New(rand.NewSource(seed)) + r.Read(b) + + var x, y []byte + for len(b) > 0 { + switch p := r.Float32(); { + case p < px: // UniqueX + x = append(x, b[0]) + case p < py: // UniqueY + y = append(y, b[0]) + case p < pm: // Modified + x = append(x, 'A'+(b[0]%26)) + y = append(y, 'a'+(b[0]%26)) + default: // Identity + x = append(x, b[0]) + y = append(y, b[0]) + } + b = b[1:] + } + return string(x), string(y) +} + +func testStrings(t *testing.T, x, y string) EditScript { + es := Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + if es.LenX() != len(x) { + t.Errorf("es.LenX = %d, want %d", es.LenX(), len(x)) + } + if es.LenY() != len(y) { + t.Errorf("es.LenY = %d, want %d", es.LenY(), len(y)) + } + if !validateScript(x, y, es) { + t.Errorf("invalid edit script: %v", es) + } + return es +} + +func validateScript(x, y string, es EditScript) bool { + var bx, by []byte + for _, e := range es { + switch e { + case Identity: + if !compareByte(x[len(bx)], y[len(by)]).Equal() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + case UniqueX: + bx = append(bx, x[len(bx)]) + case UniqueY: + by = append(by, y[len(by)]) + case Modified: + if !compareByte(x[len(bx)], y[len(by)]).Similar() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + } + } + return string(bx) == x && string(by) == y +} + +// compareByte returns a Result where the result is Equal if x == y, +// similar if x and y differ only in casing, and different otherwise. +func compareByte(x, y byte) (r Result) { + switch { + case x == y: + return equalResult // Identity + case unicode.ToUpper(rune(x)) == unicode.ToUpper(rune(y)): + return similarResult // Modified + default: + return differentResult // UniqueX or UniqueY + } +} + +var ( + equalResult = Result{NumDiff: 0} + similarResult = Result{NumDiff: 1} + differentResult = Result{NumDiff: 2} +) + +func TestResult(t *testing.T) { + tests := []struct { + result Result + wantEqual bool + wantSimilar bool + }{ + // 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 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{NumSame: 0, NumDiff: 0}, true, 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{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 { + if got := tt.result.Equal(); got != tt.wantEqual { + t.Errorf("%#v.Equal() = %v, want %v", tt.result, got, tt.wantEqual) + } + if got := tt.result.Similar(); got != tt.wantSimilar { + t.Errorf("%#v.Similar() = %v, want %v", tt.result, got, tt.wantSimilar) + } + } +} diff --git a/cmp/internal/flags/flags.go b/cmp/internal/flags/flags.go new file mode 100644 index 0000000..a9e7fc0 --- /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.md 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/flags/toolchain_legacy.go b/cmp/internal/flags/toolchain_legacy.go new file mode 100644 index 0000000..01aed0a --- /dev/null +++ b/cmp/internal/flags/toolchain_legacy.go @@ -0,0 +1,10 @@ +// 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. + +// +build !go1.10 + +package flags + +// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10. +const AtLeastGo110 = false diff --git a/cmp/internal/flags/toolchain_recent.go b/cmp/internal/flags/toolchain_recent.go new file mode 100644 index 0000000..c0b667f --- /dev/null +++ b/cmp/internal/flags/toolchain_recent.go @@ -0,0 +1,10 @@ +// 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. + +// +build go1.10 + +package flags + +// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10. +const AtLeastGo110 = true diff --git a/cmp/internal/function/func.go b/cmp/internal/function/func.go new file mode 100644 index 0000000..ace1dbe --- /dev/null +++ b/cmp/internal/function/func.go @@ -0,0 +1,99 @@ +// 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 function provides functionality for identifying function types. +package function + +import ( + "reflect" + "regexp" + "runtime" + "strings" +) + +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 + ValuePredicate = tbFunc // func(T) bool + KeyValuePredicate = trbFunc // func(T, R) bool +) + +var boolType = reflect.TypeOf(true) + +// IsType reports whether the reflect.Type is of the specified function type. +func IsType(t reflect.Type, ft funcType) bool { + if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { + return false + } + 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 + } + case trFunc: // func(T) R + if ni == 1 && no == 1 { + return true + } + } + return false +} + +var lastIdentRx = regexp.MustCompile(`[_\p{L}][_\p{L}\p{N}]*$`) + +// NameOf returns the name of the function value. +func NameOf(v reflect.Value) string { + fnc := runtime.FuncForPC(v.Pointer()) + if fnc == nil { + return "<unknown>" + } + fullName := fnc.Name() // e.g., "long/path/name/mypkg.(*MyType).(long/path/name/mypkg.myMethod)-fm" + + // Method closures have a "-fm" suffix. + fullName = strings.TrimSuffix(fullName, "-fm") + + var name string + for len(fullName) > 0 { + inParen := strings.HasSuffix(fullName, ")") + fullName = strings.TrimSuffix(fullName, ")") + + s := lastIdentRx.FindString(fullName) + if s == "" { + break + } + name = s + "." + name + fullName = strings.TrimSuffix(fullName, s) + + if i := strings.LastIndexByte(fullName, '('); inParen && i >= 0 { + fullName = fullName[:i] + } + fullName = strings.TrimSuffix(fullName, ".") + } + return strings.TrimSuffix(name, ".") +} diff --git a/cmp/internal/function/func_test.go b/cmp/internal/function/func_test.go new file mode 100644 index 0000000..61eeccd --- /dev/null +++ b/cmp/internal/function/func_test.go @@ -0,0 +1,51 @@ +// 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. + +package function + +import ( + "bytes" + "reflect" + "testing" +) + +type myType struct{ bytes.Buffer } + +func (myType) valueMethod() {} +func (myType) ValueMethod() {} + +func (*myType) pointerMethod() {} +func (*myType) PointerMethod() {} + +func TestNameOf(t *testing.T) { + tests := []struct { + fnc interface{} + want string + }{ + {TestNameOf, "function.TestNameOf"}, + {func() {}, "function.TestNameOf.func1"}, + {(myType).valueMethod, "function.myType.valueMethod"}, + {(myType).ValueMethod, "function.myType.ValueMethod"}, + {(myType{}).valueMethod, "function.myType.valueMethod"}, + {(myType{}).ValueMethod, "function.myType.ValueMethod"}, + {(*myType).valueMethod, "function.myType.valueMethod"}, + {(*myType).ValueMethod, "function.myType.ValueMethod"}, + {(&myType{}).valueMethod, "function.myType.valueMethod"}, + {(&myType{}).ValueMethod, "function.myType.ValueMethod"}, + {(*myType).pointerMethod, "function.myType.pointerMethod"}, + {(*myType).PointerMethod, "function.myType.PointerMethod"}, + {(&myType{}).pointerMethod, "function.myType.pointerMethod"}, + {(&myType{}).PointerMethod, "function.myType.PointerMethod"}, + {(*myType).Write, "function.myType.Write"}, + {(&myType{}).Write, "bytes.Buffer.Write"}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := NameOf(reflect.ValueOf(tt.fnc)) + if got != tt.want { + t.Errorf("NameOf() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmp/internal/testprotos/protos.go b/cmp/internal/testprotos/protos.go new file mode 100644 index 0000000..120c8b0 --- /dev/null +++ b/cmp/internal/testprotos/protos.go @@ -0,0 +1,116 @@ +// 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 testprotos + +func Equal(x, y Message) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() +} + +type Message interface { + Proto() + String() string +} + +type proto interface { + Proto() +} + +type notComparable struct { + unexportedField func() +} + +type Stringer struct{ X string } + +func (s *Stringer) String() string { return s.X } + +// Project1 protocol buffers +type ( + Eagle_States int + Eagle_MissingCalls int + Dreamer_States int + Dreamer_MissingCalls int + Slap_States int + Goat_States int + Donkey_States int + SummerType int + + Eagle struct { + proto + notComparable + Stringer + } + Dreamer struct { + proto + notComparable + Stringer + } + Slap struct { + proto + notComparable + Stringer + } + Goat struct { + proto + notComparable + Stringer + } + Donkey struct { + proto + notComparable + Stringer + } +) + +// Project2 protocol buffers +type ( + Germ struct { + proto + notComparable + Stringer + } + Dish struct { + proto + notComparable + Stringer + } +) + +// Project3 protocol buffers +type ( + Dirt struct { + proto + notComparable + Stringer + } + Wizard struct { + proto + notComparable + Stringer + } + Sadistic struct { + proto + notComparable + Stringer + } +) + +// Project4 protocol buffers +type ( + HoneyStatus int + PoisonType int + MetaData struct { + proto + notComparable + Stringer + } + Restrictions struct { + proto + notComparable + Stringer + } +) diff --git a/cmp/internal/teststructs/project1.go b/cmp/internal/teststructs/project1.go new file mode 100644 index 0000000..1999e38 --- /dev/null +++ b/cmp/internal/teststructs/project1.go @@ -0,0 +1,267 @@ +// 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 teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalEagle(x, y Eagle) bool { + if x.Name != y.Name && + !reflect.DeepEqual(x.Hounds, y.Hounds) && + x.Desc != y.Desc && + x.DescLong != y.DescLong && + x.Prong != y.Prong && + x.StateGoverner != y.StateGoverner && + x.PrankRating != y.PrankRating && + x.FunnyPrank != y.FunnyPrank && + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + + if len(x.Dreamers) != len(y.Dreamers) { + return false + } + for i := range x.Dreamers { + if !equalDreamer(x.Dreamers[i], y.Dreamers[i]) { + return false + } + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDreamer(x, y Dreamer) bool { + if x.Name != y.Name || + x.Desc != y.Desc || + x.DescLong != y.DescLong || + x.ContSlapsInterval != y.ContSlapsInterval || + x.Ornamental != y.Ornamental || + x.Amoeba != y.Amoeba || + x.Heroes != y.Heroes || + x.FloppyDisk != y.FloppyDisk || + x.MightiestDuck != y.MightiestDuck || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + + return false + } + if len(x.Animal) != len(y.Animal) { + return false + } + for i := range x.Animal { + vx := x.Animal[i] + vy := y.Animal[i] + if reflect.TypeOf(x.Animal) != reflect.TypeOf(y.Animal) { + return false + } + switch vx.(type) { + case Goat: + if !equalGoat(vx.(Goat), vy.(Goat)) { + return false + } + case Donkey: + if !equalDonkey(vx.(Donkey), vy.(Donkey)) { + return false + } + default: + panic(fmt.Sprintf("unknown type: %T", vx)) + } + } + if len(x.PreSlaps) != len(y.PreSlaps) { + return false + } + for i := range x.PreSlaps { + if !equalSlap(x.PreSlaps[i], y.PreSlaps[i]) { + return false + } + } + if len(x.ContSlaps) != len(y.ContSlaps) { + return false + } + for i := range x.ContSlaps { + if !equalSlap(x.ContSlaps[i], y.ContSlaps[i]) { + return false + } + } + return true +} +func equalSlap(x, y Slap) bool { + return x.Name == y.Name && + x.Desc == y.Desc && + x.DescLong == y.DescLong && + pb.Equal(x.Args, y.Args) && + x.Tense == y.Tense && + x.Interval == y.Interval && + x.Homeland == y.Homeland && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +func equalGoat(x, y Goat) bool { + if x.Target != y.Target || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDonkey(x, y Donkey) bool { + return x.Pause == y.Pause && + x.Sleep == y.Sleep && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +*/ + +type Eagle struct { + Name string + Hounds []string + Desc string + DescLong string + Dreamers []Dreamer + Prong int64 + Slaps []Slap + StateGoverner string + PrankRating string + FunnyPrank string + Immutable *EagleImmutable +} + +type EagleImmutable struct { + ID string + State *pb.Eagle_States + MissingCall *pb.Eagle_MissingCalls + Birthday time.Time + Death time.Time + Started time.Time + LastUpdate time.Time + Creator string + empty bool +} + +type Dreamer struct { + Name string + Desc string + DescLong string + PreSlaps []Slap + ContSlaps []Slap + ContSlapsInterval int32 + Animal []interface{} // Could be either Goat or Donkey + Ornamental bool + Amoeba int64 + Heroes int32 + FloppyDisk int32 + MightiestDuck bool + FunnyPrank string + Immutable *DreamerImmutable +} + +type DreamerImmutable struct { + ID string + State *pb.Dreamer_States + MissingCall *pb.Dreamer_MissingCalls + Calls int32 + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type Slap struct { + Name string + Desc string + DescLong string + Args pb.Message + Tense int32 + Interval int32 + Homeland uint32 + FunnyPrank string + Immutable *SlapImmutable +} + +type SlapImmutable struct { + ID string + Out pb.Message + MildSlap bool + PrettyPrint string + State *pb.Slap_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + LoveRadius *LoveRadius + empty bool +} + +type Goat struct { + Target string + Slaps []Slap + FunnyPrank string + Immutable *GoatImmutable +} + +type GoatImmutable struct { + ID string + State *pb.Goat_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} +type Donkey struct { + Pause bool + Sleep int32 + FunnyPrank string + Immutable *DonkeyImmutable +} + +type DonkeyImmutable struct { + ID string + State *pb.Donkey_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type LoveRadius struct { + Summer *SummerLove + empty bool +} + +type SummerLove struct { + Summary *SummerLoveSummary + empty bool +} + +type SummerLoveSummary struct { + Devices []string + ChangeType []pb.SummerType + empty bool +} + +func (EagleImmutable) Proto() *pb.Eagle { return nil } +func (DreamerImmutable) Proto() *pb.Dreamer { return nil } +func (SlapImmutable) Proto() *pb.Slap { return nil } +func (GoatImmutable) Proto() *pb.Goat { return nil } +func (DonkeyImmutable) Proto() *pb.Donkey { return nil } diff --git a/cmp/internal/teststructs/project2.go b/cmp/internal/teststructs/project2.go new file mode 100644 index 0000000..536592b --- /dev/null +++ b/cmp/internal/teststructs/project2.go @@ -0,0 +1,74 @@ +// 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 teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalBatch(b1, b2 *GermBatch) bool { + for _, b := range []*GermBatch{b1, b2} { + for _, l := range b.DirtyGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + for _, l := range b.CleanGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + } + if !pb.DeepEqual(b1.DirtyGerms, b2.DirtyGerms) || + !pb.DeepEqual(b1.CleanGerms, b2.CleanGerms) || + !pb.DeepEqual(b1.GermMap, b2.GermMap) { + return false + } + if len(b1.DishMap) != len(b2.DishMap) { + return false + } + for id := range b1.DishMap { + kpb1, err1 := b1.DishMap[id].Proto() + kpb2, err2 := b2.DishMap[id].Proto() + if !pb.Equal(kpb1, kpb2) || !reflect.DeepEqual(err1, err2) { + return false + } + } + return b1.HasPreviousResult == b2.HasPreviousResult && + b1.DirtyID == b2.DirtyID && + b1.CleanID == b2.CleanID && + b1.GermStrain == b2.GermStrain && + b1.TotalDirtyGerms == b2.TotalDirtyGerms && + b1.InfectedAt.Equal(b2.InfectedAt) +} +*/ + +type GermBatch struct { + DirtyGerms, CleanGerms map[int32][]*pb.Germ + GermMap map[int32]*pb.Germ + DishMap map[int32]*Dish + HasPreviousResult bool + DirtyID, CleanID int32 + GermStrain int32 + TotalDirtyGerms int + InfectedAt time.Time +} + +type Dish struct { + pb *pb.Dish + err error +} + +func CreateDish(m *pb.Dish, err error) *Dish { + return &Dish{pb: m, err: err} +} + +func (d *Dish) Proto() (*pb.Dish, error) { + if d.err != nil { + return nil, d.err + } + return d.pb, nil +} diff --git a/cmp/internal/teststructs/project3.go b/cmp/internal/teststructs/project3.go new file mode 100644 index 0000000..957d093 --- /dev/null +++ b/cmp/internal/teststructs/project3.go @@ -0,0 +1,82 @@ +// 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 teststructs + +import ( + "sync" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalDirt(x, y *Dirt) bool { + if !reflect.DeepEqual(x.table, y.table) || + !reflect.DeepEqual(x.ts, y.ts) || + x.Discord != y.Discord || + !pb.Equal(&x.Proto, &y.Proto) || + len(x.wizard) != len(y.wizard) || + len(x.sadistic) != len(y.sadistic) || + x.lastTime != y.lastTime { + return false + } + for k, vx := range x.wizard { + vy, ok := y.wizard[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + for k, vx := range x.sadistic { + vy, ok := y.sadistic[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + return true +} +*/ + +type FakeMutex struct { + sync.Locker + x struct{} +} + +type Dirt struct { + table Table // Always concrete type of MockTable + ts Timestamp + Discord DiscordState + Proto pb.Dirt + wizard map[string]*pb.Wizard + sadistic map[string]*pb.Sadistic + lastTime int64 + mu FakeMutex +} + +type DiscordState int + +type Timestamp int64 + +func (d *Dirt) SetTable(t Table) { d.table = t } +func (d *Dirt) SetTimestamp(t Timestamp) { d.ts = t } +func (d *Dirt) SetWizard(m map[string]*pb.Wizard) { d.wizard = m } +func (d *Dirt) SetSadistic(m map[string]*pb.Sadistic) { d.sadistic = m } +func (d *Dirt) SetLastTime(t int64) { d.lastTime = t } + +type Table interface { + Operation1() error + Operation2() error + Operation3() error +} + +type MockTable struct { + state []string +} + +func CreateMockTable(s []string) *MockTable { return &MockTable{s} } +func (mt *MockTable) Operation1() error { return nil } +func (mt *MockTable) Operation2() error { return nil } +func (mt *MockTable) Operation3() error { return nil } +func (mt *MockTable) State() []string { return mt.state } diff --git a/cmp/internal/teststructs/project4.go b/cmp/internal/teststructs/project4.go new file mode 100644 index 0000000..49920f2 --- /dev/null +++ b/cmp/internal/teststructs/project4.go @@ -0,0 +1,142 @@ +// 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 teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalCartel(x, y Cartel) bool { + if !(equalHeadquarter(x.Headquarter, y.Headquarter) && + x.Source() == y.Source() && + x.CreationDate().Equal(y.CreationDate()) && + x.Boss() == y.Boss() && + x.LastCrimeDate().Equal(y.LastCrimeDate())) { + return false + } + if len(x.Poisons()) != len(y.Poisons()) { + return false + } + for i := range x.Poisons() { + if !equalPoison(*x.Poisons()[i], *y.Poisons()[i]) { + return false + } + } + return true +} +func equalHeadquarter(x, y Headquarter) bool { + xr, yr := x.Restrictions(), y.Restrictions() + return x.ID() == y.ID() && + x.Location() == y.Location() && + reflect.DeepEqual(x.SubDivisions(), y.SubDivisions()) && + x.IncorporatedDate().Equal(y.IncorporatedDate()) && + pb.Equal(x.MetaData(), y.MetaData()) && + bytes.Equal(x.PrivateMessage(), y.PrivateMessage()) && + bytes.Equal(x.PublicMessage(), y.PublicMessage()) && + x.HorseBack() == y.HorseBack() && + x.Rattle() == y.Rattle() && + x.Convulsion() == y.Convulsion() && + x.Expansion() == y.Expansion() && + x.Status() == y.Status() && + pb.Equal(&xr, &yr) && + x.CreationTime().Equal(y.CreationTime()) +} +func equalPoison(x, y Poison) bool { + return x.PoisonType() == y.PoisonType() && + x.Expiration().Equal(y.Expiration()) && + x.Manufacturer() == y.Manufacturer() && + x.Potency() == y.Potency() +} +*/ + +type Cartel struct { + Headquarter + source string + creationDate time.Time + boss string + lastCrimeDate time.Time + poisons []*Poison +} + +func (p Cartel) Source() string { return p.source } +func (p Cartel) CreationDate() time.Time { return p.creationDate } +func (p Cartel) Boss() string { return p.boss } +func (p Cartel) LastCrimeDate() time.Time { return p.lastCrimeDate } +func (p Cartel) Poisons() []*Poison { return p.poisons } + +func (p *Cartel) SetSource(x string) { p.source = x } +func (p *Cartel) SetCreationDate(x time.Time) { p.creationDate = x } +func (p *Cartel) SetBoss(x string) { p.boss = x } +func (p *Cartel) SetLastCrimeDate(x time.Time) { p.lastCrimeDate = x } +func (p *Cartel) SetPoisons(x []*Poison) { p.poisons = x } + +type Headquarter struct { + id uint64 + location string + subDivisions []string + incorporatedDate time.Time + metaData *pb.MetaData + privateMessage []byte + publicMessage []byte + horseBack string + rattle string + convulsion bool + expansion uint64 + status pb.HoneyStatus + restrictions pb.Restrictions + creationTime time.Time +} + +func (hq Headquarter) ID() uint64 { return hq.id } +func (hq Headquarter) Location() string { return hq.location } +func (hq Headquarter) SubDivisions() []string { return hq.subDivisions } +func (hq Headquarter) IncorporatedDate() time.Time { return hq.incorporatedDate } +func (hq Headquarter) MetaData() *pb.MetaData { return hq.metaData } +func (hq Headquarter) PrivateMessage() []byte { return hq.privateMessage } +func (hq Headquarter) PublicMessage() []byte { return hq.publicMessage } +func (hq Headquarter) HorseBack() string { return hq.horseBack } +func (hq Headquarter) Rattle() string { return hq.rattle } +func (hq Headquarter) Convulsion() bool { return hq.convulsion } +func (hq Headquarter) Expansion() uint64 { return hq.expansion } +func (hq Headquarter) Status() pb.HoneyStatus { return hq.status } +func (hq Headquarter) Restrictions() pb.Restrictions { return hq.restrictions } +func (hq Headquarter) CreationTime() time.Time { return hq.creationTime } + +func (hq *Headquarter) SetID(x uint64) { hq.id = x } +func (hq *Headquarter) SetLocation(x string) { hq.location = x } +func (hq *Headquarter) SetSubDivisions(x []string) { hq.subDivisions = x } +func (hq *Headquarter) SetIncorporatedDate(x time.Time) { hq.incorporatedDate = x } +func (hq *Headquarter) SetMetaData(x *pb.MetaData) { hq.metaData = x } +func (hq *Headquarter) SetPrivateMessage(x []byte) { hq.privateMessage = x } +func (hq *Headquarter) SetPublicMessage(x []byte) { hq.publicMessage = x } +func (hq *Headquarter) SetHorseBack(x string) { hq.horseBack = x } +func (hq *Headquarter) SetRattle(x string) { hq.rattle = x } +func (hq *Headquarter) SetConvulsion(x bool) { hq.convulsion = x } +func (hq *Headquarter) SetExpansion(x uint64) { hq.expansion = x } +func (hq *Headquarter) SetStatus(x pb.HoneyStatus) { hq.status = x } +func (hq *Headquarter) SetRestrictions(x pb.Restrictions) { hq.restrictions = x } +func (hq *Headquarter) SetCreationTime(x time.Time) { hq.creationTime = x } + +type Poison struct { + poisonType pb.PoisonType + expiration time.Time + manufacturer string + potency int +} + +func (p Poison) PoisonType() pb.PoisonType { return p.poisonType } +func (p Poison) Expiration() time.Time { return p.expiration } +func (p Poison) Manufacturer() string { return p.manufacturer } +func (p Poison) Potency() int { return p.potency } + +func (p *Poison) SetPoisonType(x pb.PoisonType) { p.poisonType = x } +func (p *Poison) SetExpiration(x time.Time) { p.expiration = x } +func (p *Poison) SetManufacturer(x string) { p.manufacturer = x } +func (p *Poison) SetPotency(x int) { p.potency = x } diff --git a/cmp/internal/teststructs/structs.go b/cmp/internal/teststructs/structs.go new file mode 100644 index 0000000..6b4d2a7 --- /dev/null +++ b/cmp/internal/teststructs/structs.go @@ -0,0 +1,197 @@ +// 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 teststructs + +type InterfaceA interface { + InterfaceA() +} + +type ( + StructA struct{ X string } // Equal method on value receiver + StructB struct{ X string } // Equal method on pointer receiver + StructC struct{ X string } // Equal method (with interface argument) on value receiver + StructD struct{ X string } // Equal method (with interface argument) on pointer receiver + StructE struct{ X string } // Equal method (with interface argument on value receiver) on pointer receiver + StructF struct{ X string } // Equal method (with interface argument on pointer receiver) on value receiver + + // These embed the above types as a value. + StructA1 struct { + StructA + X string + } + StructB1 struct { + StructB + X string + } + StructC1 struct { + StructC + X string + } + StructD1 struct { + StructD + X string + } + StructE1 struct { + StructE + X string + } + StructF1 struct { + StructF + X string + } + + // These embed the above types as a pointer. + StructA2 struct { + *StructA + X string + } + StructB2 struct { + *StructB + X string + } + StructC2 struct { + *StructC + X string + } + StructD2 struct { + *StructD + X string + } + StructE2 struct { + *StructE + X string + } + StructF2 struct { + *StructF + X string + } + + StructNo struct{ X string } // Equal method (with interface argument) on non-satisfying receiver + + AssignA func() int + AssignB struct{ A int } + AssignC chan bool + AssignD <-chan bool +) + +func (x StructA) Equal(y StructA) bool { return true } +func (x *StructB) Equal(y *StructB) bool { return true } +func (x StructC) Equal(y InterfaceA) bool { return true } +func (x StructC) InterfaceA() {} +func (x *StructD) Equal(y InterfaceA) bool { return true } +func (x *StructD) InterfaceA() {} +func (x *StructE) Equal(y InterfaceA) bool { return true } +func (x StructE) InterfaceA() {} +func (x StructF) Equal(y InterfaceA) bool { return true } +func (x *StructF) InterfaceA() {} +func (x StructNo) Equal(y InterfaceA) bool { return true } + +func (x AssignA) Equal(y func() int) bool { return true } +func (x AssignB) Equal(y struct{ A int }) bool { return true } +func (x AssignC) Equal(y chan bool) bool { return true } +func (x AssignD) Equal(y <-chan bool) bool { return true } + +var _ = func( + a StructA, b StructB, c StructC, d StructD, e StructE, f StructF, + ap *StructA, bp *StructB, cp *StructC, dp *StructD, ep *StructE, fp *StructF, + a1 StructA1, b1 StructB1, c1 StructC1, d1 StructD1, e1 StructE1, f1 StructF1, + a2 StructA2, b2 StructB2, c2 StructC2, d2 StructD2, e2 StructE2, f2 StructF1, +) { + a.Equal(a) + b.Equal(&b) + c.Equal(c) + d.Equal(&d) + e.Equal(e) + f.Equal(&f) + + ap.Equal(*ap) + bp.Equal(bp) + cp.Equal(*cp) + dp.Equal(dp) + ep.Equal(*ep) + fp.Equal(fp) + + a1.Equal(a1.StructA) + b1.Equal(&b1.StructB) + c1.Equal(c1) + d1.Equal(&d1) + e1.Equal(e1) + f1.Equal(&f1) + + a2.Equal(*a2.StructA) + b2.Equal(b2.StructB) + c2.Equal(c2) + d2.Equal(&d2) + e2.Equal(e2) + f2.Equal(&f2) +} + +type ( + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStructA struct{ privateStruct } + ParentStructB struct{ PublicStruct } + ParentStructC struct { + privateStruct + Public, private int + } + ParentStructD struct { + PublicStruct + Public, private int + } + ParentStructE struct { + privateStruct + PublicStruct + } + ParentStructF struct { + privateStruct + PublicStruct + Public, private int + } + ParentStructG struct { + *privateStruct + } + ParentStructH struct { + *PublicStruct + } + ParentStructI struct { + *privateStruct + *PublicStruct + } + ParentStructJ struct { + *privateStruct + *PublicStruct + Public PublicStruct + private privateStruct + } +) + +func NewParentStructG() *ParentStructG { + return &ParentStructG{new(privateStruct)} +} +func NewParentStructH() *ParentStructH { + return &ParentStructH{new(PublicStruct)} +} +func NewParentStructI() *ParentStructI { + return &ParentStructI{new(privateStruct), new(PublicStruct)} +} +func NewParentStructJ() *ParentStructJ { + return &ParentStructJ{ + privateStruct: new(privateStruct), PublicStruct: new(PublicStruct), + } +} +func (s *privateStruct) SetPrivate(i int) { s.private = i } +func (s *PublicStruct) SetPrivate(i int) { s.private = i } +func (s *ParentStructC) SetPrivate(i int) { s.private = i } +func (s *ParentStructD) SetPrivate(i int) { s.private = i } +func (s *ParentStructF) SetPrivate(i int) { s.private = i } +func (s *ParentStructA) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructC) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructE) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructF) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructG) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructI) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) Private() *privateStruct { return &s.private } diff --git a/cmp/internal/value/pointer_purego.go b/cmp/internal/value/pointer_purego.go new file mode 100644 index 0000000..0a01c47 --- /dev/null +++ b/cmp/internal/value/pointer_purego.go @@ -0,0 +1,23 @@ +// 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. + +// +build purego + +package value + +import "reflect" + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p uintptr + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // NOTE: Storing a pointer as an uintptr is technically incorrect as it + // assumes that the GC implementation does not use a moving collector. + return Pointer{v.Pointer(), v.Type()} +} diff --git a/cmp/internal/value/pointer_unsafe.go b/cmp/internal/value/pointer_unsafe.go new file mode 100644 index 0000000..da134ae --- /dev/null +++ b/cmp/internal/value/pointer_unsafe.go @@ -0,0 +1,26 @@ +// 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. + +// +build !purego + +package value + +import ( + "reflect" + "unsafe" +) + +// Pointer is an opaque typed pointer and is guaranteed to be comparable. +type Pointer struct { + p unsafe.Pointer + t reflect.Type +} + +// PointerOf returns a Pointer from v, which must be a +// reflect.Ptr, reflect.Slice, or reflect.Map. +func PointerOf(v reflect.Value) Pointer { + // The proper representation of a pointer is unsafe.Pointer, + // which is necessary if the GC ever uses a moving collector. + return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} +} diff --git a/cmp/internal/value/sort.go b/cmp/internal/value/sort.go new file mode 100644 index 0000000..24fbae6 --- /dev/null +++ b/cmp/internal/value/sort.go @@ -0,0 +1,106 @@ +// 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 ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKeys sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if isLess(vs2[len(vs2)-1], v) { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + 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: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/cmp/internal/value/sort_test.go b/cmp/internal/value/sort_test.go new file mode 100644 index 0000000..fb86fce --- /dev/null +++ b/cmp/internal/value/sort_test.go @@ -0,0 +1,159 @@ +// 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_test + +import ( + "math" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/value" +) + +func TestSortKeys(t *testing.T) { + type ( + MyString string + MyArray [2]int + MyStruct struct { + A MyString + B MyArray + C chan float64 + } + EmptyStruct struct{} + ) + + opts := []cmp.Option{ + cmp.Comparer(func(x, y float64) bool { + if math.IsNaN(x) && math.IsNaN(y) { + return true + } + return x == y + }), + cmp.Comparer(func(x, y complex128) bool { + rx, ix, ry, iy := real(x), imag(x), real(y), imag(y) + if math.IsNaN(rx) && math.IsNaN(ry) { + rx, ry = 0, 0 + } + if math.IsNaN(ix) && math.IsNaN(iy) { + ix, iy = 0, 0 + } + return rx == ry && ix == iy + }), + cmp.Comparer(func(x, y chan bool) bool { return true }), + cmp.Comparer(func(x, y chan int) bool { return true }), + cmp.Comparer(func(x, y chan float64) bool { return true }), + cmp.Comparer(func(x, y chan interface{}) bool { return true }), + cmp.Comparer(func(x, y *int) bool { return true }), + } + + tests := []struct { + in map[interface{}]bool // Set of keys to sort + want []interface{} + }{{ + in: map[interface{}]bool{1: true, 2: true, 3: true}, + want: []interface{}{1, 2, 3}, + }, { + in: map[interface{}]bool{ + nil: true, + true: true, + false: true, + -5: true, + -55: true, + -555: true, + uint(1): true, + uint(11): true, + uint(111): true, + "abc": true, + "abcd": true, + "abcde": true, + "foo": true, + "bar": true, + MyString("abc"): true, + MyString("abcd"): true, + MyString("abcde"): true, + new(int): true, + new(int): true, + make(chan bool): true, + make(chan bool): true, + make(chan int): true, + make(chan interface{}): true, + math.Inf(+1): true, + math.Inf(-1): true, + 1.2345: true, + 12.345: true, + 123.45: true, + 1234.5: true, + 0 + 0i: true, + 1 + 0i: true, + 2 + 0i: true, + 0 + 1i: true, + 0 + 2i: true, + 0 + 3i: true, + [2]int{2, 3}: true, + [2]int{4, 0}: true, + [2]int{2, 4}: true, + MyArray([2]int{2, 4}): true, + EmptyStruct{}: true, + MyStruct{ + "bravo", [2]int{2, 3}, make(chan float64), + }: true, + MyStruct{ + "alpha", [2]int{3, 3}, make(chan float64), + }: true, + }, + want: []interface{}{ + nil, false, true, + -555, -55, -5, uint(1), uint(11), uint(111), + math.Inf(-1), 1.2345, 12.345, 123.45, 1234.5, math.Inf(+1), + (0 + 0i), (0 + 1i), (0 + 2i), (0 + 3i), (1 + 0i), (2 + 0i), + [2]int{2, 3}, [2]int{2, 4}, [2]int{4, 0}, MyArray([2]int{2, 4}), + make(chan bool), make(chan bool), make(chan int), make(chan interface{}), + new(int), new(int), + "abc", "abcd", "abcde", "bar", "foo", + MyString("abc"), MyString("abcd"), MyString("abcde"), + EmptyStruct{}, + MyStruct{"alpha", [2]int{3, 3}, make(chan float64)}, + MyStruct{"bravo", [2]int{2, 3}, make(chan float64)}, + }, + }, { + // NaN values cannot be properly deduplicated. + // This is okay since map entries with NaN in the keys cannot be + // retrieved anyways. + in: map[interface{}]bool{ + math.NaN(): true, + math.NaN(): true, + complex(0, math.NaN()): true, + complex(0, math.NaN()): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), math.NaN()): true, + }, + want: []interface{}{ + math.NaN(), + complex(math.NaN(), math.NaN()), + complex(math.NaN(), 0), + complex(0, math.NaN()), + }, + }} + + for i, tt := range tests { + // Intentionally pass the map via an unexported field to detect panics. + // Unfortunately, we cannot actually test the keys without using unsafe. + v := reflect.ValueOf(struct{ x map[interface{}]bool }{tt.in}).Field(0) + value.SortKeys(append(v.MapKeys(), v.MapKeys()...)) + + // Try again, with keys that have read-write access in reflect. + v = reflect.ValueOf(tt.in) + keys := append(v.MapKeys(), v.MapKeys()...) + var got []interface{} + for _, k := range value.SortKeys(keys) { + got = append(got, k.Interface()) + } + if d := cmp.Diff(got, tt.want, opts...); d != "" { + t.Errorf("test %d, Sort() mismatch (-got +want):\n%s", i, d) + } + } +} diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go new file mode 100644 index 0000000..06a8ffd --- /dev/null +++ b/cmp/internal/value/zero.go @@ -0,0 +1,48 @@ +// 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 ( + "math" + "reflect" +) + +// 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 math.Float64bits(v.Float()) == 0 + case reflect.Complex64, reflect.Complex128: + return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(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 +} diff --git a/cmp/internal/value/zero_test.go b/cmp/internal/value/zero_test.go new file mode 100644 index 0000000..1d6c434 --- /dev/null +++ b/cmp/internal/value/zero_test.go @@ -0,0 +1,52 @@ +// 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. + +package value + +import ( + "archive/tar" + "math" + "reflect" + "testing" +) + +func TestIsZero(t *testing.T) { + tests := []struct { + in interface{} + want bool + }{ + {0, true}, + {1, false}, + {"", true}, + {"foo", false}, + {[]byte(nil), true}, + {[]byte{}, false}, + {map[string]bool(nil), true}, + {map[string]bool{}, false}, + {tar.Header{}, true}, + {&tar.Header{}, false}, + {tar.Header{Name: "foo"}, false}, + {(chan bool)(nil), true}, + {make(chan bool), false}, + {(func(*testing.T))(nil), true}, + {TestIsZero, false}, + {[...]int{0, 0, 0}, true}, + {[...]int{0, 1, 0}, false}, + {math.Copysign(0, +1), true}, + {math.Copysign(0, -1), false}, + {complex(math.Copysign(0, +1), math.Copysign(0, +1)), true}, + {complex(math.Copysign(0, -1), math.Copysign(0, +1)), false}, + {complex(math.Copysign(0, +1), math.Copysign(0, -1)), false}, + {complex(math.Copysign(0, -1), math.Copysign(0, -1)), false}, + } + + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := IsZero(reflect.ValueOf(tt.in)) + if got != tt.want { + t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want) + } + }) + } +} diff --git a/cmp/options.go b/cmp/options.go new file mode 100644 index 0000000..abbd2a6 --- /dev/null +++ b/cmp/options.go @@ -0,0 +1,549 @@ +// 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" + "regexp" + "strings" + + "github.com/google/go-cmp/cmp/internal/function" +) + +// Option configures for specific behavior of Equal and Diff. In particular, +// the fundamental Option functions (Ignore, Transformer, and Comparer), +// configure how equality is determined. +// +// The fundamental options may be composed with filters (FilterPath and +// FilterValues) to control the scope over which they are applied. +// +// The cmp/cmpopts package provides helper functions for creating options that +// may be used with Equal and Diff. +type Option interface { + // filter applies all filters and returns the option that remains. + // Each option may only read s.curPath and call s.callTTBFunc. + // + // 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, t reflect.Type, vx, vy reflect.Value) applicableOption +} + +// applicableOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Grouping: Options +type applicableOption interface { + Option + + // apply executes the option, which may mutate s or panic. + apply(s *state, vx, vy reflect.Value) +} + +// coreOption represents the following types: +// Fundamental: ignore | validator | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter +type coreOption interface { + Option + isCore() +} + +type core struct{} + +func (core) isCore() {} + +// Options is a list of Option values that also satisfies the Option interface. +// Helper comparison packages may return an Options value when packing multiple +// Option values into a single Option. When this package processes an Options, +// it will be implicitly expanded into a flat list. +// +// Applying a filter on an Options is equivalent to applying that same filter +// on all individual options held within. +type Options []Option + +func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { + for _, opt := range opts { + switch opt := opt.filter(s, t, vx, vy); opt.(type) { + case ignore: + return ignore{} // Only ignore can short-circuit evaluation + case validator: + out = validator{} // Takes precedence over comparer or transformer + case *comparer, *transformer, Options: + switch out.(type) { + case nil: + out = opt + case validator: + // Keep validator + case *comparer, *transformer, Options: + out = Options{out, opt} // Conflicting comparers or transformers + } + } + } + return out +} + +func (opts Options) apply(s *state, _, _ reflect.Value) { + const warning = "ambiguous set of applicable options" + const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + var ss []string + for _, opt := range flattenOptions(nil, opts) { + ss = append(ss, fmt.Sprint(opt)) + } + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) +} + +func (opts Options) String() string { + var ss []string + for _, opt := range opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) +} + +// 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 { + if f == nil { + panic("invalid path filter function") + } + if opt := normalizeOption(opt); opt != nil { + return &pathFilter{fnc: f, opt: opt} + } + return nil +} + +type pathFilter struct { + core + fnc func(Path) bool + opt Option +} + +func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if f.fnc(s.curPath) { + return f.opt.filter(s, t, vx, vy) + } + return nil +} + +func (f pathFilter) String() string { + return fmt.Sprintf("FilterPath(%s, %v)", function.NameOf(reflect.ValueOf(f.fnc)), f.opt) +} + +// 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 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 +// deterministic (i.e., produces the same result when given the same inputs). +// If T is an interface, it is possible that f is called with two values with +// different concrete types that both implement T. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterValues(f interface{}, opt Option) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { + panic(fmt.Sprintf("invalid values filter function: %T", f)) + } + if opt := normalizeOption(opt); opt != nil { + vf := &valuesFilter{fnc: v, opt: opt} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + vf.typ = ti + } + return vf + } + return nil +} + +type valuesFilter struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool + opt Option +} + +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, t, vx, vy) + } + return nil +} + +func (f valuesFilter) String() string { + return fmt.Sprintf("FilterValues(%s, %v)", function.NameOf(f.fnc), f.opt) +} + +// Ignore is an Option that causes all comparisons to be ignored. +// This value is intended to be combined with FilterPath or FilterValues. +// It is an error to pass an unfiltered Ignore option to Equal. +func Ignore() Option { return ignore{} } + +type ignore struct{ core } + +func (ignore) isFiltered() bool { return false } +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()" } + +// 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() { + const 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 + } 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)) + } + + panic("not reachable") +} + +// identRx represents a valid identifier according to the Go specification. +const identRx = `[_\p{L}][_\p{L}\p{N}]*` + +var identsRx = regexp.MustCompile(`^` + identRx + `(\.` + identRx + `)*$`) + +// Transformer returns an Option that applies a transformation function that +// converts values of a certain type into that of another. +// +// The transformer f must be a function "func(T) R" that converts values of +// type T to those of type R and is implicitly filtered to input values +// assignable to T. The transformer must not mutate T in any way. +// +// To help prevent some cases of infinite recursive cycles applying the +// same transform to the output of itself (e.g., in the case where the +// input and output types are the same), an implicit filter is added such that +// a transformer is applicable only if that exact transformer is not already +// in the tail of the Path since the last non-Transform step. +// For situations where the implicit filter is still insufficient, +// consider using cmpopts.AcyclicTransformer, which adds a filter +// to prevent the transformer from being recursively applied upon itself. +// +// The name is a user provided label that is used as the Transform.Name in the +// transformation PathStep (and eventually shown in the Diff output). +// The name must be a valid identifier or qualified identifier in Go syntax. +// If empty, an arbitrary name is used. +func Transformer(name string, f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { + panic(fmt.Sprintf("invalid transformer function: %T", f)) + } + if name == "" { + name = function.NameOf(v) + if !identsRx.MatchString(name) { + name = "λ" // Lambda-symbol as placeholder name + } + } else if !identsRx.MatchString(name) { + panic(fmt.Sprintf("invalid name: %q", name)) + } + tr := &transformer{name: name, fnc: reflect.ValueOf(f)} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + tr.typ = ti + } + return tr +} + +type transformer struct { + core + name string + typ reflect.Type // T + fnc reflect.Value // func(T) R +} + +func (tr *transformer) isFiltered() bool { return tr.typ != nil } + +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 { + break // Hit most recent non-Transform step + } else if tr == t.trans { + return nil // Cannot directly use same Transform + } + } + if tr.typ == nil || t.AssignableTo(tr.typ) { + return tr + } + return nil +} + +func (tr *transformer) apply(s *state, vx, vy reflect.Value) { + 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 { + return fmt.Sprintf("Transformer(%s, %s)", tr.name, function.NameOf(tr.fnc)) +} + +// Comparer returns an Option that determines whether two values are equal +// to each other. +// +// The comparer f must be a function "func(T, T) bool" and is implicitly +// filtered to input values assignable to T. If T is an interface, it is +// possible that f is called with two values of different concrete types that +// 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 +func Comparer(f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Equal) || v.IsNil() { + panic(fmt.Sprintf("invalid comparer function: %T", f)) + } + cm := &comparer{fnc: v} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + cm.typ = ti + } + return cm +} + +type comparer struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (cm *comparer) isFiltered() bool { return cm.typ != nil } + +func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { + if cm.typ == nil || t.AssignableTo(cm.typ) { + return cm + } + return nil +} + +func (cm *comparer) apply(s *state, vx, vy reflect.Value) { + eq := s.callTTBFunc(cm.fnc, vx, vy) + s.report(eq, reportByFunc) +} + +func (cm comparer) String() string { + return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) +} + +// 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 +// implementation of some external package may cause the result of Equal +// to unexpectedly change. However, it may be valid to use this option on types +// defined in an internal package where the semantic meaning of an unexported +// field is in the control of the user. +// +// 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. +// +// For example, the reflect.Type documentation defines equality to be determined +// by the == operator on the interface (essentially performing a shallow pointer +// comparison) and most attempts to compare *regexp.Regexp types are interested +// in only checking that the regular expression strings are equal. +// Both of these are accomplished using Comparers: +// +// Comparer(func(x, y reflect.Type) bool { return x == y }) +// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) +// +// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore +// all unexported fields on specified struct types. +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) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + m[t] = true + } + return exporter(func(t reflect.Type) bool { return m[t] }) +} + +// Result represents the comparison result for a single node and +// is provided by cmp when calling Result (see Reporter). +type Result struct { + _ [0]func() // Make Result incomparable + flags resultFlags +} + +// 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 +} + +// 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. + // + // 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} +} + +type reporter struct{ reporterIface } +type reporterIface interface { + PushStep(PathStep) + Report(Result) + PopStep() +} + +func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// normalizeOption normalizes the input options such that all Options groups +// are flattened and groups with a single element are reduced to that element. +// Only coreOptions and Options containing coreOptions are allowed. +func normalizeOption(src Option) Option { + switch opts := flattenOptions(nil, Options{src}); len(opts) { + case 0: + return nil + case 1: + return opts[0] + default: + return opts + } +} + +// flattenOptions copies all options in src to dst as a flat list. +// Only coreOptions and Options containing coreOptions are allowed. +func flattenOptions(dst, src Options) Options { + for _, opt := range src { + switch opt := opt.(type) { + case nil: + continue + case Options: + dst = flattenOptions(dst, opt) + case coreOption: + dst = append(dst, opt) + default: + panic(fmt.Sprintf("invalid option type: %T", opt)) + } + } + return dst +} diff --git a/cmp/options_test.go b/cmp/options_test.go new file mode 100644 index 0000000..f8066c7 --- /dev/null +++ b/cmp/options_test.go @@ -0,0 +1,216 @@ +// 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 ( + "io" + "reflect" + "strings" + "testing" + + ts "github.com/google/go-cmp/cmp/internal/teststructs" +) + +// Test that the creation of Option values with non-sensible inputs produces +// a run-time panic with a decent error message +func TestOptionPanic(t *testing.T) { + type myBool bool + tests := []struct { + label string // Test description + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + }{{ + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{1}, + wantPanic: "invalid struct type", + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, ts.StructB{}, ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, &ts.StructB{}, ts.StructA{}}, + wantPanic: "invalid struct type", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{5}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y interface{}) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) myBool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x string, y interface{}) bool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{(func(int, int) bool)(nil)}, + wantPanic: "invalid comparer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", 0}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) int { return 0 }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(bool) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int, int) bool { return true }}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", (func(int) uint)(nil)}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"Func", func(Path) Path { return nil }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"世界", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"/*", func(int) bool { return true }}, + wantPanic: "invalid name", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"_", func(int) bool { return true }}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{(func(Path) bool)(nil), Ignore()}, + wantPanic: "invalid path filter function", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Ignore()}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Reporter(&defaultReporter{})}, + wantPanic: "invalid option type", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{0, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y int) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) myBool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x io.Reader, y interface{}) bool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{(func(int, int) bool)(nil), Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Reporter(&defaultReporter{})}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, + wantPanic: "invalid option type", + }} + + for _, tt := range tests { + t.Run(tt.label, func(t *testing.T) { + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + var vargs []reflect.Value + for _, arg := range tt.args { + vargs = append(vargs, reflect.ValueOf(arg)) + } + reflect.ValueOf(tt.fnc).Call(vargs) + }() + if tt.wantPanic == "" { + if gotPanic != "" { + t.Fatalf("unexpected panic message: %s", gotPanic) + } + } else { + if !strings.Contains(gotPanic, tt.wantPanic) { + t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + } + } + }) + } +} diff --git a/cmp/path.go b/cmp/path.go new file mode 100644 index 0000000..603dbb0 --- /dev/null +++ b/cmp/path.go @@ -0,0 +1,378 @@ +// 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" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// 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 + +// 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 + + // Type is the resulting type after performing the path step. + Type() reflect.Type + + // 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) { + *pa = append(*pa, s) +} + +func (pa *Path) pop() { + *pa = (*pa)[:len(*pa)-1] +} + +// Last returns the last PathStep in the Path. +// If the path is empty, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Last() PathStep { + return pa.Index(-1) +} + +// Index returns the ith step in the Path and supports negative indexing. +// A negative index starts counting from the tail of the Path such that -1 +// refers to the last step, -2 refers to the second-to-last step, and so on. +// If index is invalid, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Index(i int) PathStep { + if i < 0 { + i = len(pa) + i + } + if i < 0 || i >= len(pa) { + return pathStep{} + } + return pa[i] +} + +// String returns the simplified path to a node. +// 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 { + ss = append(ss, s.String()) + } + } + return strings.TrimPrefix(strings.Join(ss, ""), ".") +} + +// 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 + var numIndirect int + for i, s := range pa { + var nextStep PathStep + if i+1 < len(pa) { + nextStep = pa[i+1] + } + switch s := s.(type) { + case Indirect: + numIndirect++ + pPre, pPost := "(", ")" + switch nextStep.(type) { + case Indirect: + continue // Next step is indirection, so let them batch up + case StructField: + numIndirect-- // Automatic indirection on struct fields + case nil: + pPre, pPost = "", "" // Last step; no need for parenthesis + } + if numIndirect > 0 { + ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) + ssPost = append(ssPost, pPost) + } + numIndirect = 0 + continue + case Transform: + ssPre = append(ssPre, s.trans.name+"(") + ssPost = append(ssPost, ")") + continue + } + ssPost = append(ssPost, s.String()) + } + for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { + ssPre[i], ssPre[j] = ssPre[j], ssPre[i] + } + return strings.Join(ssPre, "") + strings.Join(ssPost, "") +} + +type pathStep struct { + typ reflect.Type + vx, vy reflect.Value +} + +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 "<nil>" + } + s := ps.typ.String() + if s == "" || strings.ContainsAny(s, "{}\n") { + return "root" // Type too simple or complex to print + } + return fmt.Sprintf("{%s}", s) +} + +// 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 addressible) + 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) 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) + case si.ykey == -1: + // [5->?] means "I don't know where X[5] went" + return fmt.Sprintf("[%d->?]", si.xkey) + case si.xkey == -1: + // [?->3] means "I don't know where Y[3] came from" + return fmt.Sprintf("[?->%d]", si.ykey) + default: + // [5->3] means "X[5] moved to Y[3]" + return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) + } +} + +// 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 +} + +// 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 } + +// MapIndex is an index operation on a map at some index Key. +type MapIndex struct{ *mapIndex } +type mapIndex struct { + pathStep + key reflect.Value +} + +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) } + +// Key is the value of the map key. +func (mi MapIndex) Key() reflect.Value { return mi.key } + +// 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)", ta.typ) } + +// 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 seperate 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 { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/cmp/report.go b/cmp/report.go new file mode 100644 index 0000000..6ddf299 --- /dev/null +++ b/cmp/report.go @@ -0,0 +1,51 @@ +// 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 + +// 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 "" + } + return formatOptions{}.FormatDiff(r.root).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..d3fa154 --- /dev/null +++ b/cmp/report_compare.go @@ -0,0 +1,301 @@ +// 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. + +package cmp + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp/internal/value" +) + +// TODO: Enforce limits? +// * Enforce maximum number of records to print per node? +// * Enforce maximum size in bytes allowed? +// * As a heuristic, use less verbosity for equal nodes than unequal nodes. +// TODO: Enforce unique outputs? +// * Avoid Stringer methods if it results in same output? +// * Print pointer address if outputs still equal? + +// 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 +} + +// 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) textNode { + // 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 withinSlice bool + if v.parent != nil && (v.parent.Type.Kind() == reflect.Slice || v.parent.Type.Kind() == reflect.Array) { + withinSlice = true + } + + // For leaf nodes, format the value based on the reflect.Values alone. + if v.MaxDepth == 0 { + switch opts.DiffMode { + case diffUnknown, diffIdentical: + // Format Equal. + if v.NumDiff == 0 { + outx := opts.FormatValue(v.ValueX, withinSlice, visitedPointers{}) + outy := opts.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + 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, withinSlice, visitedPointers{}) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, withinSlice, visitedPointers{}) + 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, withinSlice, visitedPointers{}) + case diffInserted: + return opts.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + default: + panic("invalid diff mode") + } + } + + // Descend into the child value node. + if v.TransformerName != "" { + out := opts.WithTypeMode(emitType).FormatDiff(v.Value) + out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"} + return opts.FormatType(v.Type, out) + } else { + switch k := v.Type.Kind(); k { + case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map: + return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k)) + case reflect.Ptr: + return textWrap{"&", opts.FormatDiff(v.Value), ""} + case reflect.Interface: + return opts.WithTypeMode(emitType).FormatDiff(v.Value) + default: + panic(fmt.Sprintf("%v cannot have children", k)) + } + } +} + +func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) 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 = formatMapKey + } + + // 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 { + // Elide struct fields that are zero value. + if k == reflect.Struct { + var isZero bool + switch opts.DiffMode { + case diffIdentical: + isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY) + case diffRemoved: + isZero = value.IsZero(r.Value.ValueX) + case diffInserted: + isZero = value.IsZero(r.Value.ValueY) + } + 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); out != nil { + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + } + if deferredEllipsis { + list.AppendEllipsis(diffStats{}) + } + return textWrap{"{", list, "}"} + case diffUnknown: + default: + panic("invalid diff mode") + } + + // Handle differencing. + var list textList + groups := coalesceAdjacentRecords(name, recs) + for i, ds := range groups { + // 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) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + if numEqual > numLo+numHi { + ds.NumIdentical -= numLo + numHi + list.AppendEllipsis(ds) + } + for _, r := range recs[numEqual-numHi : numEqual] { + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + 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}) + case r.Value.NumChildren == r.Value.MaxDepth: + outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value) + outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value) + if outx != nil { + list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) + } + if outy != nil { + list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) + } + default: + out := opts.FormatDiff(r.Value) + list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + } + } + recs = recs[ds.NumDiff():] + } + assert(len(recs) == 0) + return textWrap{"{", list, "}"} +} + +// 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_reflect.go b/cmp/report_reflect.go new file mode 100644 index 0000000..642622e --- /dev/null +++ b/cmp/report_reflect.go @@ -0,0 +1,286 @@ +// 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. + +package cmp + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "unicode" + + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/value" +) + +type formatValueOptions struct { + // AvoidStringer controls whether to avoid calling custom stringer + // methods like error.Error or fmt.Stringer.String. + AvoidStringer bool + + // ShallowPointers controls whether to avoid descending into pointers. + // Useful when printing map keys, where pointer comparison is performed + // on the pointer address rather than the pointed-at value. + ShallowPointers bool + + // PrintAddresses controls whether to print the address of all pointers, + // slice elements, and maps. + PrintAddresses 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 + } + case elideType: + return s + } + + // Determine the type label, applying special handling for unnamed types. + typeName := t.String() + 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 + ")" + } + typeName = strings.Replace(typeName, "struct {", "struct{", -1) + typeName = strings.Replace(typeName, "interface {", "interface{", -1) + } + + // Avoid wrap the value in parenthesis if unnecessary. + if s, ok := s.(textWrap); ok { + hasParens := strings.HasPrefix(s.Prefix, "(") && strings.HasSuffix(s.Suffix, ")") + hasBraces := strings.HasPrefix(s.Prefix, "{") && strings.HasSuffix(s.Suffix, "}") + if hasParens || hasBraces { + return textWrap{typeName, s, ""} + } + } + return textWrap{typeName + "(", s, ")"} +} + +// FormatValue prints the reflect.Value, taking extra care to avoid descending +// into pointers already in m. As pointers are visited, m is also updated. +func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visitedPointers) (out textNode) { + if !v.IsValid() { + return nil + } + t := v.Type() + + // 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() { + switch v := v.Interface().(type) { + case error: + return textLine("e" + formatString(v.Error())) + case fmt.Stringer: + return textLine("s" + formatString(v.String())) + } + } + } + + // Check whether to explicitly wrap the result with the type. + var skipType bool + defer func() { + if !skipType { + out = opts.FormatType(t, out) + } + }() + + var ptr string + 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 withinSlice { + 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 textLine(formatString(v.String())) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return textLine(formatPointer(v)) + case reflect.Struct: + var list textList + v := makeAddressable(v) // needed for retrieveUnexportedField + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if value.IsZero(vv) { + continue // Elide fields with zero values + } + sf := t.Field(i) + if supportExporters && !isExported(sf.Name) { + vv = retrieveUnexportedField(v, sf, true) + } + s := opts.WithTypeMode(autoType).FormatValue(vv, false, m) + list = append(list, textRecord{Key: sf.Name, Value: s}) + } + return textWrap{"{", list, "}"} + case reflect.Slice: + if v.IsNil() { + return textNil + } + if opts.PrintAddresses { + ptr = formatPointer(v) + } + fallthrough + case reflect.Array: + var list textList + for i := 0; i < v.Len(); i++ { + vi := v.Index(i) + if vi.CanAddr() { // Check for cyclic elements + p := vi.Addr() + if m.Visit(p) { + var out textNode + out = textLine(formatPointer(p)) + out = opts.WithTypeMode(emitType).FormatType(p.Type(), out) + out = textWrap{"*", out, ""} + list = append(list, textRecord{Value: out}) + continue + } + } + s := opts.WithTypeMode(elideType).FormatValue(vi, true, m) + list = append(list, textRecord{Value: s}) + } + return textWrap{ptr + "{", list, "}"} + case reflect.Map: + if v.IsNil() { + return textNil + } + if m.Visit(v) { + return textLine(formatPointer(v)) + } + + var list textList + for _, k := range value.SortKeys(v.MapKeys()) { + sk := formatMapKey(k) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m) + list = append(list, textRecord{Key: sk, Value: sv}) + } + if opts.PrintAddresses { + ptr = formatPointer(v) + } + return textWrap{ptr + "{", list, "}"} + case reflect.Ptr: + if v.IsNil() { + return textNil + } + if m.Visit(v) || opts.ShallowPointers { + return textLine(formatPointer(v)) + } + if opts.PrintAddresses { + ptr = formatPointer(v) + } + skipType = true // Let the underlying value print the type instead + return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), false, m), ""} + case reflect.Interface: + if v.IsNil() { + return textNil + } + // Interfaces accept different concrete types, + // so configure the underlying value to explicitly print the type. + skipType = true // Print the concrete type instead + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), false, m) + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +// formatMapKey formats v as if it were a map key. +// The result is guaranteed to be a single line. +func formatMapKey(v reflect.Value) string { + var opts formatOptions + opts.TypeMode = elideType + opts.ShallowPointers = true + s := opts.FormatValue(v, false, visitedPointers{}).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 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) +} + +// formatPointer prints the address of the pointer. +func formatPointer(v reflect.Value) string { + p := v.Pointer() + if flags.Deterministic { + p = 0xdeadf00f // Only used for stable testing purposes + } + return fmt.Sprintf("⟪0x%x⟫", p) +} + +type visitedPointers map[value.Pointer]struct{} + +// Visit inserts pointer v into the visited map and reports whether it had +// already been visited before. +func (m visitedPointers) Visit(v reflect.Value) bool { + p := value.PointerOf(v) + _, visited := m[p] + m[p] = struct{}{} + return visited +} diff --git a/cmp/report_slices.go b/cmp/report_slices.go new file mode 100644 index 0000000..344cbac --- /dev/null +++ b/cmp/report_slices.go @@ -0,0 +1,337 @@ +// 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. + +package cmp + +import ( + "bytes" + "fmt" + "reflect" + "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.NumIgnored+v.NumCompared+v.NumTransformed > 0: + // TODO: Handle the case where someone uses bytes.Equal on a large slice. + return false // Some custom option was used to determined equality + case !v.ValueX.IsValid() || !v.ValueY.IsValid(): + return false // Both values must be valid + case v.Type.Kind() == reflect.Slice && (v.ValueX.IsNil() || v.ValueY.IsNil()): + return false // Both of values have to be non-nil + } + + switch t := v.Type; 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 + } + + // 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 = 64 + return v.ValueX.Len() >= minLength && v.ValueY.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 + + // Auto-detect the type of the data. + var isLinedText, isText, isBinary bool + var sx, sy string + switch { + case t.Kind() == reflect.String: + sx, sy = vx.String(), vy.String() + isText = true // Initial estimate, verify later + case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)): + sx, sy = string(vx.Bytes()), string(vy.Bytes()) + isBinary = true // Initial estimate, verify later + 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 isText || isBinary { + var numLines, lastLineIdx, maxLineLen int + isBinary = false + for i, r := range sx + sy { + if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError { + isBinary = true + break + } + if r == '\n' { + if maxLineLen < i-lastLineIdx { + maxLineLen = i - lastLineIdx + } + lastLineIdx = i + 1 + numLines++ + } + } + isText = !isBinary + isLinedText = isText && numLines >= 4 && maxLineLen <= 256 + } + + // 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 isLinedText: + ssx := strings.Split(sx, "\n") + ssy := strings.Split(sy, "\n") + 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 the text appears to be single-lined text, + // then perform differencing in approximately fixed-sized chunks. + // The output is printed as quoted strings. + case isText: + 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)} + }, + ) + delim = "" + // 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{"{", list, "}"} + if !isText { + // 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{"strings.Join(", out, fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)} + if t != reflect.TypeOf([]byte(nil)) { + 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) { + es := diff.Difference(vx.Len(), vy.Len(), func(ix int, iy int) diff.Result { + return diff.BoolResult(vx.Index(ix).Interface() == vy.Index(iy).Interface()) + }) + + 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() + } + + groups := coalesceAdjacentEdits(name, es) + groups = coalesceInterveningIdentical(groups, chunkSize/4) + for i, ds := range groups { + // 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. + 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()) + } + assert(vx.Len() == 0 && vy.Len() == 0) + return list +} + +// coalesceAdjacentEdits coalesces the list of edits into groups of adjacent +// equal or unequal counts. +func coalesceAdjacentEdits(name string, es diff.EditScript) (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 _, e := range es { + switch e { + case diff.Identity: + lastStats(1).NumIdentical++ + case diff.UniqueX: + lastStats(2).NumRemoved++ + case diff.UniqueY: + lastStats(2).NumInserted++ + case diff.Modified: + lastStats(2).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. +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 +} diff --git a/cmp/report_text.go b/cmp/report_text.go new file mode 100644 index 0000000..8b8fcab --- /dev/null +++ b/cmp/report_text.go @@ -0,0 +1,387 @@ +// 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. + +package cmp + +import ( + "bytes" + "fmt" + "math/rand" + "strings" + "time" + + "github.com/google/go-cmp/cmp/internal/flags" +) + +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 + +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., "}" +} + +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 + 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 != diffStats{} + if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { + if hasStats { + *s = append(*s, textRecord{Value: textEllipsis, Comment: ds}) + } else { + *s = append(*s, textRecord{Value: textEllipsis}) + } + 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{"{", s, "}"}.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:]) > 80 { + 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 len(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 len(r.Value.(textLine)) }, + ) + + // 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.Value.Equal(textEllipsis) { + 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) 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..83031a7 --- /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.md 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/testdata/diffs b/cmp/testdata/diffs new file mode 100644 index 0000000..56a9a32 --- /dev/null +++ b/cmp/testdata/diffs @@ -0,0 +1,1231 @@ +<<< TestDiff/Comparer#09 + struct{ A int; B int; C int }{ + A: 1, + B: 2, +- C: 3, ++ C: 4, + } +>>> TestDiff/Comparer#09 +<<< TestDiff/Comparer#12 + &struct{ A *int }{ +- A: &4, ++ A: &5, + } +>>> TestDiff/Comparer#12 +<<< TestDiff/Comparer#16 + &struct{ R *bytes.Buffer }{ +- R: s"", ++ R: nil, + } +>>> TestDiff/Comparer#16 +<<< TestDiff/Comparer#23 + []*regexp.Regexp{ + nil, +- s"a*b*c*", ++ s"a*b*d*", + } +>>> TestDiff/Comparer#23 +<<< TestDiff/Comparer#25 + &&&int( +- 0, ++ 1, + ) +>>> TestDiff/Comparer#25 +<<< TestDiff/Comparer#28 + struct{ fmt.Stringer }( +- s"hello", ++ s"hello2", + ) +>>> TestDiff/Comparer#28 +<<< TestDiff/Comparer#29 + [16]uint8{ +- 0x0c, 0xc1, 0x75, 0xb9, 0xc0, 0xf1, 0xb6, 0xa8, 0x31, 0xc3, 0x99, 0xe2, 0x69, 0x77, 0x26, 0x61, ++ 0x92, 0xeb, 0x5f, 0xfe, 0xe6, 0xae, 0x2f, 0xec, 0x3a, 0xd7, 0x1c, 0x77, 0x75, 0x31, 0x57, 0x8f, + } +>>> TestDiff/Comparer#29 +<<< TestDiff/Comparer#30 + interface{}( +- &fmt.Stringer(nil), + ) +>>> TestDiff/Comparer#30 +<<< TestDiff/Comparer#31 + []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#31 +<<< TestDiff/Comparer#36 + []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#36 +<<< TestDiff/Comparer#37 + map[*testprotos.Stringer]*testprotos.Stringer( +- {s"hello": s"world"}, ++ nil, + ) +>>> TestDiff/Comparer#37 +<<< TestDiff/Comparer#38 + interface{}( +- []*testprotos.Stringer{s`multi\nline\nline\nline`}, + ) +>>> TestDiff/Comparer#38 +<<< TestDiff/Comparer#42 + []interface{}{ + map[string]interface{}{ + "avg": float64(0.278), +- "hr": int(65), ++ "hr": float64(65), + "name": string("Mark McGwire"), + }, + map[string]interface{}{ + "avg": float64(0.288), +- "hr": int(63), ++ "hr": float64(63), + "name": string("Sammy Sosa"), + }, + } +>>> TestDiff/Comparer#42 +<<< TestDiff/Comparer#43 + map[*int]string{ +- ⟪0xdeadf00f⟫: "hello", ++ ⟪0xdeadf00f⟫: "world", + } +>>> TestDiff/Comparer#43 +<<< TestDiff/Comparer#44 + (*int)( +- &0, ++ &0, + ) +>>> TestDiff/Comparer#44 +<<< TestDiff/Comparer#45 + [2][]int{ + {..., 1, 2, 3, ..., 4, 5, 6, 7, ..., 8, ..., 9, ...}, + { + ... // 6 ignored and 1 identical elements +- 20, ++ 2, + ... // 3 ignored elements + }, + } +>>> TestDiff/Comparer#45 +<<< TestDiff/Comparer#46 + [2]map[string]int{ + {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, + { + ... // 2 ignored entries + "keep1": 1, ++ "keep2": 2, + }, + } +>>> TestDiff/Comparer#46 +<<< TestDiff/Transformer + uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( +- 0, ++ 1, + ))))))) +>>> TestDiff/Transformer +<<< TestDiff/Transformer#02 + []int{ + Inverse(λ, int64(0)), +- Inverse(λ, int64(-5)), ++ Inverse(λ, int64(3)), + Inverse(λ, int64(0)), +- Inverse(λ, int64(-1)), ++ Inverse(λ, int64(-5)), + } +>>> TestDiff/Transformer#02 +<<< TestDiff/Transformer#03 + int(Inverse(λ, interface{}( +- string("zero"), ++ float64(1), + ))) +>>> TestDiff/Transformer#03 +<<< TestDiff/Transformer#04 + string(Inverse(ParseJSON, map[string]interface{}{ + "address": map[string]interface{}{ +- "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": []interface{}{}, + "firstName": string("John"), + "isAlive": bool(true), + "lastName": string("Smith"), + "phoneNumbers": []interface{}{ + map[string]interface{}{ +- "number": string("212 555-4321"), ++ "number": string("212 555-1234"), + "type": string("home"), + }, + map[string]interface{}{"number": string("646 555-4567"), "type": string("office")}, + map[string]interface{}{"number": string("123 456-7890"), "type": string("mobile")}, + }, ++ "spouse": nil, + })) +>>> TestDiff/Transformer#04 +<<< TestDiff/Transformer#05 + cmp_test.StringBytes{ + String: Inverse(SplitString, []string{ + "some", + "multi", +- "Line", ++ "line", + "string", + }), + Bytes: []uint8(Inverse(SplitBytes, [][]uint8{ + {0x73, 0x6f, 0x6d, 0x65}, + {0x6d, 0x75, 0x6c, 0x74, 0x69}, + {0x6c, 0x69, 0x6e, 0x65}, + { +- 0x62, ++ 0x42, + 0x79, + 0x74, + ... // 2 identical elements + }, + })), + } +>>> TestDiff/Transformer#05 +<<< TestDiff/Reporter + 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 +<<< TestDiff/Reporter#01 + 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#01 +<<< TestDiff/Reporter#02 + 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#02 +<<< TestDiff/Reporter#03 + 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#03 +<<< TestDiff/Reporter#04 + 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#04 +<<< TestDiff/Reporter#05 + 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#05 +<<< TestDiff/Reporter#06 + cmp_test.MyComposite{ + StringA: strings.Join({ +- "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.", +- "", + }, "\n"), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter#06 +<<< TestDiff/Reporter#07 + 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#07 +<<< TestDiff/Reporter#08 + 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#08 +<<< TestDiff/EmbeddedStruct/ParentStructA#04 + teststructs.ParentStructA{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructA#04 +<<< TestDiff/EmbeddedStruct/ParentStructB#04 + teststructs.ParentStructB{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructB#04 +<<< TestDiff/EmbeddedStruct/ParentStructC#04 + teststructs.ParentStructC{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructC#04 +<<< TestDiff/EmbeddedStruct/ParentStructD#04 + teststructs.ParentStructD{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +>>> TestDiff/EmbeddedStruct/ParentStructD#04 +<<< TestDiff/EmbeddedStruct/ParentStructE#05 + 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#05 +<<< TestDiff/EmbeddedStruct/ParentStructF#05 + 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#05 +<<< TestDiff/EmbeddedStruct/ParentStructG#04 + &teststructs.ParentStructG{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructG#04 +<<< TestDiff/EmbeddedStruct/ParentStructH#05 + &teststructs.ParentStructH{ + PublicStruct: &teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +>>> TestDiff/EmbeddedStruct/ParentStructH#05 +<<< TestDiff/EmbeddedStruct/ParentStructI#06 + &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#06 +<<< TestDiff/EmbeddedStruct/ParentStructJ#05 + &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#05 +<<< TestDiff/EqualMethod/StructB + teststructs.StructB{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB +<<< TestDiff/EqualMethod/StructD + teststructs.StructD{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD +<<< TestDiff/EqualMethod/StructE + teststructs.StructE{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE +<<< TestDiff/EqualMethod/StructF + teststructs.StructF{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF +<<< TestDiff/EqualMethod/StructA1#01 + teststructs.StructA1{ + StructA: teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1#01 +<<< TestDiff/EqualMethod/StructA1#03 + &teststructs.StructA1{ + StructA: teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA1#03 +<<< TestDiff/EqualMethod/StructB1#01 + teststructs.StructB1{ + StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1#01 +<<< TestDiff/EqualMethod/StructB1#03 + &teststructs.StructB1{ + StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB1#03 +<<< TestDiff/EqualMethod/StructD1 + teststructs.StructD1{ +- StructD: teststructs.StructD{X: "NotEqual"}, ++ StructD: teststructs.StructD{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructD1 +<<< TestDiff/EqualMethod/StructE1 + teststructs.StructE1{ +- StructE: teststructs.StructE{X: "NotEqual"}, ++ StructE: teststructs.StructE{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructE1 +<<< TestDiff/EqualMethod/StructF1 + teststructs.StructF1{ +- StructF: teststructs.StructF{X: "NotEqual"}, ++ StructF: teststructs.StructF{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructF1 +<<< TestDiff/EqualMethod/StructA2#01 + teststructs.StructA2{ + StructA: &teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2#01 +<<< TestDiff/EqualMethod/StructA2#03 + &teststructs.StructA2{ + StructA: &teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructA2#03 +<<< TestDiff/EqualMethod/StructB2#01 + teststructs.StructB2{ + StructB: &teststructs.StructB{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2#01 +<<< TestDiff/EqualMethod/StructB2#03 + &teststructs.StructB2{ + StructB: &teststructs.StructB{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructB2#03 +<<< TestDiff/EqualMethod/StructNo + teststructs.StructNo{ +- X: "NotEqual", ++ X: "not_equal", + } +>>> TestDiff/EqualMethod/StructNo +<<< TestDiff/Cycle#01 + &&cmp_test.P( +- &⟪0xdeadf00f⟫, ++ &&⟪0xdeadf00f⟫, + ) +>>> TestDiff/Cycle#01 +<<< TestDiff/Cycle#03 + cmp_test.S{ +- {{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}, ++ {{{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}}, + } +>>> TestDiff/Cycle#03 +<<< TestDiff/Cycle#05 + cmp_test.M{ +- 0: {0: ⟪0xdeadf00f⟫}, ++ 0: {0: {0: ⟪0xdeadf00f⟫}}, + } +>>> TestDiff/Cycle#05 +<<< TestDiff/Cycle#07 + map[string]*cmp_test.CycleAlpha{ + "Bar": &{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}}}, "Buzz": ⟪0xdeadf00f⟫}}, + "BuzzBarBravo": &{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}, + }, + }, + }, + }, + }, + "BuzzBarBravo": &{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}, + }, + "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": ⟪0xdeadf00f⟫}}, + }, + }, + }, + }, + }, + }, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}}}, "Buzz": ⟪0xdeadf00f⟫}}, + "BuzzBarBravo": &{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}, + }, + }, + }, + "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + }, + }, + "BuzzBarBravo": &{ +- ID: 103, ++ ID: 0, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ +- ID: 102, ++ ID: 0, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}, + }, + "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": ⟪0xdeadf00f⟫}}, + }, + }, + "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + }, + }, + }, + }, + "Foo": &{ + Name: "Foo", + Bravos: map[string]*cmp_test.CycleBravo{ + "FooBravo": &{ +- ID: 101, ++ ID: 0, + Name: "FooBravo", + Mods: 100, + Alphas: map[string]*cmp_test.CycleAlpha{"Foo": &{Name: "Foo", Bravos: map[string]*cmp_test.CycleBravo{"FooBravo": &{Name: "FooBravo", Mods: 100, Alphas: map[string]*cmp_test.CycleAlpha{"Foo": ⟪0xdeadf00f⟫}}}}}, + }, + }, + }, + } +>>> TestDiff/Cycle#07 +<<< TestDiff/Cycle#08 + map[string]*cmp_test.CycleAlpha{ + "Bar": &{ + Name: "Bar", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ + ID: 102, + Name: "BarBuzzBravo", + Mods: 2, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}}}, "Buzz": ⟪0xdeadf00f⟫}}, + "BuzzBarBravo": &{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": &{ ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{ ++ "BarBuzzBravo": &{ ++ ID: 102, ++ Name: "BarBuzzBravo", ++ Mods: 2, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": ⟪0xdeadf00f⟫, ++ "Buzz": &{ ++ Name: "Buzz", ++ Bravos: map[string]*cmp_test.CycleBravo{ ++ "BarBuzzBravo": ⟪0xdeadf00f⟫, ++ "BuzzBarBravo": &{ ++ ID: 103, ++ Name: "BuzzBarBravo", ++ Alphas: map[string]*cmp_test.CycleAlpha(⟪0xdeadf00f⟫), ++ }, ++ }, ++ }, ++ }, ++ }, ++ "BuzzBarBravo": ⟪0xdeadf00f⟫, ++ }, ++ }, ++ "Buzz": ⟪0xdeadf00f⟫, ++ }, + }, + }, + }, + }, + }, + "BuzzBarBravo": &{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, + Alphas: map[string]*cmp_test.CycleAlpha{ + "Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}, +- "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo"}, ++ "BuzzBarBravo": &{ ++ ID: 103, ++ Name: "BuzzBarBravo", ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": &{ ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{ ++ "BarBuzzBravo": &{ ++ ID: 102, ++ Name: "BarBuzzBravo", ++ Mods: 2, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": ⟪0xdeadf00f⟫, ++ "Buzz": &{ ++ Name: "Buzz", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}, ++ }, ++ }, ++ }, ++ "BuzzBarBravo": ⟪0xdeadf00f⟫, ++ }, ++ }, ++ "Buzz": ⟪0xdeadf00f⟫, ++ }, ++ }, + }, + }, + }, + }, + }, + }, + "Buzz": &{ + Name: "Buzz", + Bravos: map[string]*cmp_test.CycleBravo{ + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}}}, "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: map[string]*cmp_test.CycleAlpha{"Bar": ⟪0xdeadf00f⟫, "Buzz": ⟪0xdeadf00f⟫}}}}, "Buzz": ⟪0xdeadf00f⟫}}, "BuzzBarBravo": ⟪0xdeadf00f⟫}}}}, + "BuzzBarBravo": &{ + ID: 103, + Name: "BuzzBarBravo", + Mods: 0, +- Alphas: nil, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": &{ ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{ ++ "BarBuzzBravo": &{ ++ ID: 102, ++ Name: "BarBuzzBravo", ++ Mods: 2, ++ Alphas: map[string]*cmp_test.CycleAlpha{ ++ "Bar": ⟪0xdeadf00f⟫, ++ "Buzz": &{ ++ Name: "Buzz", ++ Bravos: map[string]*cmp_test.CycleBravo{ ++ "BarBuzzBravo": ⟪0xdeadf00f⟫, ++ "BuzzBarBravo": &{ ++ ID: 103, ++ Name: "BuzzBarBravo", ++ Alphas: map[string]*cmp_test.CycleAlpha(⟪0xdeadf00f⟫), ++ }, ++ }, ++ }, ++ }, ++ }, ++ "BuzzBarBravo": ⟪0xdeadf00f⟫, ++ }, ++ }, ++ "Buzz": ⟪0xdeadf00f⟫, ++ }, + }, + }, + }, + "Foo": &{Name: "Foo", Bravos: map[string]*cmp_test.CycleBravo{"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: map[string]*cmp_test.CycleAlpha{"Foo": &{Name: "Foo", Bravos: map[string]*cmp_test.CycleBravo{"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: map[string]*cmp_test.CycleAlpha{"Foo": ⟪0xdeadf00f⟫}}}}}}}}, + } +>>> TestDiff/Cycle#08 +<<< TestDiff/Project1#02 + 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#02 +<<< TestDiff/Project1#04 + teststructs.Eagle{ + ... // 2 identical fields + Desc: "some description", + DescLong: "", + Dreamers: []teststructs.Dreamer{ + {}, + { + ... // 4 identical fields + ContSlaps: nil, + ContSlapsInterval: 0, + Animal: []interface{}{ + 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: []testprotos.SummerType{1, 2, 3}, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + ... // 1 ignored field + }, + }, + }, + StateGoverner: "", + PrankRating: "", + ... // 2 identical fields + } +>>> TestDiff/Project1#04 +<<< TestDiff/Project2#02 + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ + 17: {s"germ1"}, + 18: { +- s"germ2", + s"germ3", + s"germ4", ++ s"germ2", + }, + }, + CleanGerms: nil, + GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"}, + ... // 7 identical fields + } +>>> TestDiff/Project2#02 +<<< TestDiff/Project2#04 + teststructs.GermBatch{ + DirtyGerms: map[int32][]*testprotos.Germ{ ++ 17: {s"germ1"}, + 18: Inverse(Sort, []*testprotos.Germ{ + s"germ2", + s"germ3", +- s"germ4", + }), + }, + CleanGerms: nil, + GermMap: map[int32]*testprotos.Germ{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#04 +<<< TestDiff/Project3#03 + 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#03 +<<< TestDiff/Project4#03 + 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#03 @@ -0,0 +1,5 @@ +module github.com/google/go-cmp + +go 1.8 + +require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 @@ -0,0 +1,2 @@ +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |