From a02fa9f0a2b3432ef9a311f083acef9aa9bb123b Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Feb 2019 17:37:20 -0800 Subject: [PATCH 01/99] Refactor reporter implementation (#112) Switch the internal interface for reporting to use a push-pop mechanism. This API is strictly more flexible than the previous Report-only API since it enables the reporter to re-construct the entire value tree. This change is done in preparation for a major overhaul of reporter logic. --- cmp/compare.go | 176 +++++++++++++++++++++------------ cmp/compare_test.go | 4 +- cmp/options.go | 85 +++++++++++----- cmp/{reporter.go => report.go} | 24 ++++- 4 files changed, 196 insertions(+), 93 deletions(-) rename cmp/{reporter.go => report.go} (72%) diff --git a/cmp/compare.go b/cmp/compare.go index a7bcaab..654f46a 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -77,8 +77,32 @@ var nothing = reflect.Value{} // Map keys are equal according to the == operator. // To use custom comparisons for map keys, consider using cmpopts.SortMaps. func Equal(x, y interface{}, opts ...Option) bool { + 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() + } + s := newState(opts) - s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) + s.pushStep(&pathStep{typ: t}, vx, vy) + s.compareAny(vx, vy) + s.popStep() return s.result.Equal() } @@ -91,7 +115,7 @@ func Equal(x, y interface{}, opts ...Option) bool { // Do not depend on this output being stable. func Diff(x, y interface{}, opts ...Option) string { r := new(defaultReporter) - opts = Options{Options(opts), r} + opts = Options{Options(opts), reporter(r)} eq := Equal(x, y, opts...) d := r.String() if (d == "") != eq { @@ -103,9 +127,9 @@ func Diff(x, y interface{}, opts ...Option) string { type state struct { // These fields represent the "comparison state". // Calling statelessCompare must not result in observable changes to these. - result diff.Result // The current result of comparison - curPath Path // The current path in the value tree - reporter reporter // Optional reporter used for difference formatting + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + reporters []reporterOption // Optional reporters // recChecker checks for infinite cycles applying the same set of // transformers upon the output of itself. @@ -150,11 +174,8 @@ func (s *state) processOption(opt Option) { for t := range opt { s.exporters[t] = true } - case reporter: - if s.reporter != nil { - panic("difference reporter already registered") - } - s.reporter = opt + case reporterOption: + s.reporters = append(s.reporters, opt) default: panic(fmt.Sprintf("unknown option %T", opt)) } @@ -169,12 +190,12 @@ func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { // It is an implementation bug if the contents of curPath differs from // when calling this function to when returning from it. - oldResult, oldReporter := s.result, s.reporter + oldResult, oldReporters := s.result, s.reporters s.result = diff.Result{} // Reset result - s.reporter = nil // Remove reporter to avoid spurious printouts + s.reporters = nil // Remove reporters to avoid spurious printouts s.compareAny(vx, vy) res := s.result - s.result, s.reporter = oldResult, oldReporter + s.result, s.reporters = oldResult, oldReporters return res } @@ -184,18 +205,14 @@ func (s *state) compareAny(vx, vy reflect.Value) { // Rule 0: Differing types are never equal. if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid(), vx, vy) + s.report(vx.IsValid() == vy.IsValid()) return } if vx.Type() != vy.Type() { - s.report(false, vx, vy) // Possible for path to be empty + s.report(false) return } t := vx.Type() - if len(s.curPath) == 0 { - s.curPath.push(&pathStep{typ: t}) - defer s.curPath.pop() - } vx, vy = s.tryExporting(vx, vy) // Rule 1: Check whether an option applies on this node in the value tree. @@ -211,35 +228,35 @@ func (s *state) compareAny(vx, vy reflect.Value) { // Rule 3: Recursively descend into each value's underlying kind. switch t.Kind() { case reflect.Bool: - s.report(vx.Bool() == vy.Bool(), vx, vy) + s.report(vx.Bool() == vy.Bool()) return case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s.report(vx.Int() == vy.Int(), vx, vy) + s.report(vx.Int() == vy.Int()) return case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s.report(vx.Uint() == vy.Uint(), vx, vy) + s.report(vx.Uint() == vy.Uint()) return case reflect.Float32, reflect.Float64: - s.report(vx.Float() == vy.Float(), vx, vy) + s.report(vx.Float() == vy.Float()) return case reflect.Complex64, reflect.Complex128: - s.report(vx.Complex() == vy.Complex(), vx, vy) + s.report(vx.Complex() == vy.Complex()) return case reflect.String: - s.report(vx.String() == vy.String(), vx, vy) + s.report(vx.String() == vy.String()) return case reflect.Chan, reflect.UnsafePointer: - s.report(vx.Pointer() == vy.Pointer(), vx, vy) + s.report(vx.Pointer() == vy.Pointer()) return case reflect.Func: - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil()) return case reflect.Struct: s.compareStruct(vx, vy, t) return case reflect.Slice: if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil()) return } fallthrough @@ -251,25 +268,27 @@ func (s *state) compareAny(vx, vy reflect.Value) { return case reflect.Ptr: if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil()) return } - s.curPath.push(&indirect{pathStep{t.Elem()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) + vx, vy = vx.Elem(), vy.Elem() + s.pushStep(&indirect{pathStep{t.Elem()}}, vx, vy) + s.compareAny(vx, vy) + s.popStep() return case reflect.Interface: if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil()) return } - if vx.Elem().Type() != vy.Elem().Type() { - s.report(false, vx.Elem(), vy.Elem()) + vx, vy = vx.Elem(), vy.Elem() + if vx.Type() != vy.Type() { + s.report(false) return } - s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}}) - defer s.curPath.pop() - s.compareAny(vx.Elem(), vy.Elem()) + s.pushStep(&typeAssertion{pathStep{vx.Type()}}, vx, vy) + s.compareAny(vx, vy) + s.popStep() return default: panic(fmt.Sprintf("%v kind not handled", t.Kind())) @@ -318,7 +337,7 @@ func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { } eq := s.callTTBFunc(m.Func, vx, vy) - s.report(eq, vx, vy) + s.report(eq) return true } @@ -391,11 +410,8 @@ func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { var vax, vay reflect.Value // Addressable versions of vx and vy step := &structField{} - s.curPath.push(step) - defer s.curPath.pop() for i := 0; i < t.NumField(); i++ { - vvx := vx.Field(i) - vvy := vy.Field(i) + vvx, vvy := vx.Field(i), vy.Field(i) step.typ = t.Field(i).Type step.name = t.Field(i).Name step.idx = i @@ -418,18 +434,22 @@ func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { step.pvy = vay step.field = t.Field(i) } + s.pushStep(step, vvx, vvy) s.compareAny(vvx, vvy) + s.popStep() } } func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { step := &sliceIndex{pathStep{t.Elem()}, 0, 0} - s.curPath.push(step) // Compute an edit-script for slices vx and vy. es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { step.xkey, step.ykey = ix, iy - return s.statelessCompare(vx.Index(ix), vy.Index(iy)) + s.curPath.push(step) + ret := s.statelessCompare(vx.Index(ix), vy.Index(iy)) + s.curPath.pop() + return ret }) // Report the entire slice as is if the arrays are of primitive kind, @@ -442,8 +462,7 @@ func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { isPrimitive = true } if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { - s.curPath.pop() // Pop first since we are reporting the whole slice - s.report(false, vx, vy) + s.report(false) return } @@ -453,49 +472,55 @@ func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { switch e { case diff.UniqueX: step.xkey, step.ykey = ix, -1 - s.report(false, vx.Index(ix), nothing) + vvx := vx.Index(ix) + s.pushStep(step, vvx, nothing) + s.report(false) + s.popStep() ix++ case diff.UniqueY: step.xkey, step.ykey = -1, iy - s.report(false, nothing, vy.Index(iy)) + vvy := vy.Index(iy) + s.pushStep(step, nothing, vvy) + s.report(false) + s.popStep() iy++ default: step.xkey, step.ykey = ix, iy + vvx, vvy := vx.Index(ix), vy.Index(iy) + s.pushStep(step, vvx, vvy) if e == diff.Identity { - s.report(true, vx.Index(ix), vy.Index(iy)) + s.report(true) } else { - s.compareAny(vx.Index(ix), vy.Index(iy)) + s.compareAny(vvx, vvy) } + s.popStep() ix++ iy++ } } - s.curPath.pop() return } func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), vx, vy) + s.report(vx.IsNil() && vy.IsNil()) return } // We combine and sort the two map keys so that we can perform the // comparisons in a deterministic order. step := &mapIndex{pathStep: pathStep{t.Elem()}} - s.curPath.push(step) - defer s.curPath.pop() for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { step.key = k - vvx := vx.MapIndex(k) - vvy := vy.MapIndex(k) + vvx, vvy := vx.MapIndex(k), vy.MapIndex(k) + s.pushStep(step, vvx, vvy) switch { case vvx.IsValid() && vvy.IsValid(): s.compareAny(vvx, vvy) case vvx.IsValid() && !vvy.IsValid(): - s.report(false, vvx, nothing) + s.report(false) case !vvx.IsValid() && vvy.IsValid(): - s.report(false, nothing, vvy) + s.report(false) default: // It is possible for both vvx and vvy to be invalid if the // key contained a NaN value in it. @@ -514,19 +539,42 @@ func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { const help = "consider providing a Comparer to compare the map" panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) } + s.popStep() } } -// report records the result of a single comparison. -// It also calls Report if any reporter is registered. -func (s *state) report(eq bool, vx, vy reflect.Value) { +func (s *state) pushStep(ps PathStep, x, y reflect.Value) { + s.curPath.push(ps) + for _, r := range s.reporters { + r.PushStep(ps, x, y) + } +} + +func (s *state) popStep() { + s.curPath.pop() + for _, r := range s.reporters { + r.PopStep() + } +} + +func (s *state) report(eq bool) { if eq { s.result.NSame++ } else { s.result.NDiff++ } - if s.reporter != nil { - s.reporter.Report(vx, vy, eq, s.curPath) + for _, r := range s.reporters { + if eq { + r.Report(reportEqual) + } else { + r.Report(reportUnequal) + } + } +} + +func (s *state) reportIgnore() { + for _, r := range s.reporters { + r.Report(reportIgnore) } } diff --git a/cmp/compare_test.go b/cmp/compare_test.go index c98b088..0a850f9 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -335,7 +335,7 @@ root: x: new(fmt.Stringer), y: nil, wantDiff: ` -: +root: -: & +: `, }, { @@ -426,7 +426,7 @@ root: // Ensure Stringer avoids double-quote escaping if possible. label: label, x: []*pb.Stringer{{`multi\nline\nline\nline`}}, - wantDiff: ":\n\t-: []*testprotos.Stringer{s`multi\\nline\\nline\\nline`}\n\t+: ", + wantDiff: "root:\n\t-: []*testprotos.Stringer{s`multi\\nline\\nline\\nline`}\n\t+: ", }, { label: label, x: struct{ I Iface2 }{}, diff --git a/cmp/options.go b/cmp/options.go index a9306b4..a8307dc 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -193,7 +193,7 @@ type ignore struct{ core } func (ignore) isFiltered() bool { return false } func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } -func (ignore) apply(_ *state, _, _ reflect.Value) { return } +func (ignore) apply(s *state, _, _ reflect.Value) { s.reportIgnore() } func (ignore) String() string { return "Ignore()" } // invalid is a sentinel Option type to indicate that some options could not @@ -277,12 +277,15 @@ func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) appl func (tr *transformer) apply(s *state, vx, vy reflect.Value) { // Update path before calling the Transformer so that dynamic checks // will use the updated path. - s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) - defer s.curPath.pop() - - vx = s.callTRFunc(tr.fnc, vx) - vy = s.callTRFunc(tr.fnc, vy) - s.compareAny(vx, vy) + step := &transform{pathStep{tr.fnc.Type().Out(0)}, tr} + s.curPath.push(step) + vvx := s.callTRFunc(tr.fnc, vx) + vvy := s.callTRFunc(tr.fnc, vy) + s.curPath.pop() + + s.pushStep(step, vvx, vvy) + s.compareAny(vvx, vvy) + s.popStep() } func (tr transformer) String() string { @@ -330,7 +333,7 @@ func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applica func (cm *comparer) apply(s *state, vx, vy reflect.Value) { eq := s.callTTBFunc(cm.fnc, vx, vy) - s.report(eq, vx, vy) + s.report(eq) } func (cm comparer) String() string { @@ -384,23 +387,61 @@ func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) appli panic("not implemented") } -// reporter is an Option that configures how differences are reported. -type reporter interface { - // TODO: Not exported yet. +type reportFlags uint64 + +const ( + _ reportFlags = (1 << iota) / 2 + + // reportEqual reports whether the node is equal. + // It may not be issued with reportIgnore or reportUnequal. + reportEqual + // reportUnequal reports whether the node is not equal. + // It may not be issued with reportIgnore or reportEqual. + reportUnequal + // reportIgnore reports whether the node was ignored. + // It may not be issued with reportEqual or reportUnequal. + reportIgnore +) + +// 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 { + // TODO: Export this option. + + // PushStep is called when a tree-traversal operation is performed + // and provides the sub-values of x and y after applying the operation. + // The PathStep is valid until the step is popped, while the reflect.Values + // are valid while the entire tree is still being traversed. // - // Perhaps add PushStep and PopStep and change Report to only accept - // a PathStep instead of the full-path? Adding a PushStep and PopStep makes - // it clear that we are traversing the value tree in a depth-first-search - // manner, which has an effect on how values are printed. + // Equal and Diff always call PushStep at the start to provide an + // operation-less PathStep used to report the root values. + PushStep(ps PathStep, x, y reflect.Value) + + // Report is called at 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(reportFlags) + + // PopStep ascends back up the value tree. + // There is always a matching pop call for every push call. + PopStep() +}) Option { + return reporterOption{r} +} - Option +type reporterOption struct{ reporterIface } +type reporterIface interface { + PushStep(PathStep, reflect.Value, reflect.Value) + Report(reportFlags) + PopStep() +} - // Report is called for every comparison made and will be provided with - // the two values being compared, the equality result, and the - // current path in the value tree. It is possible for x or y to be an - // invalid reflect.Value if one of the values is non-existent; - // which is possible with maps and slices. - Report(x, y reflect.Value, eq bool, p Path) +func (reporterOption) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { + panic("not implemented") } // normalizeOption normalizes the input options such that all Options groups diff --git a/cmp/reporter.go b/cmp/report.go similarity index 72% rename from cmp/reporter.go rename to cmp/report.go index 20e9f18..327f332 100644 --- a/cmp/reporter.go +++ b/cmp/report.go @@ -14,18 +14,32 @@ import ( type defaultReporter struct { Option + + curPath Path + curVals [][2]reflect.Value + diffs []string // List of differences, possibly truncated ndiffs int // Total number of differences nbytes int // Number of bytes in diffs nlines int // Number of lines in diffs } -var _ reporter = (*defaultReporter)(nil) - -func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { - if eq { - return // Ignore equal results +func (r *defaultReporter) PushStep(ps PathStep, x, y reflect.Value) { + r.curPath.push(ps) + r.curVals = append(r.curVals, [2]reflect.Value{x, y}) +} +func (r *defaultReporter) Report(f reportFlags) { + if f == reportUnequal { + vs := r.curVals[len(r.curVals)-1] + r.report(vs[0], vs[1], r.curPath) } +} +func (r *defaultReporter) PopStep() { + r.curPath.pop() + r.curVals = r.curVals[:len(r.curVals)-1] +} + +func (r *defaultReporter) report(x, y reflect.Value, p Path) { const maxBytes = 4096 const maxLines = 256 r.ndiffs++ From 7d316222e18768fec502f76523cd295a12f7f09a Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Feb 2019 17:42:07 -0800 Subject: [PATCH 02/99] Rename unsafe_x.go as export_x.go (#117) Rename the file based on what it does rather than how its implemented. --- cmp/compare.go | 9 ++++----- cmp/{unsafe_panic.go => export_panic.go} | 4 ++-- cmp/{unsafe_reflect.go => export_unsafe.go} | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) rename cmp/{unsafe_panic.go => export_panic.go} (65%) rename cmp/{unsafe_reflect.go => export_unsafe.go} (67%) diff --git a/cmp/compare.go b/cmp/compare.go index 654f46a..7681d4a 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -298,10 +298,9 @@ func (s *state) compareAny(vx, vy reflect.Value) { func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { if sf.force { - // Use unsafe pointer arithmetic to get read-write access to an - // unexported field in the struct. - vx = unsafeRetrieveField(sf.pvx, sf.field) - vy = unsafeRetrieveField(sf.pvy, sf.field) + // Forcibly obtain read-write access to an unexported struct field. + vx = retrieveUnexportedField(sf.pvx, sf.field) + vy = retrieveUnexportedField(sf.pvy, sf.field) } else { // We are not allowed to export the value, so invalidate them // so that tryOptions can panic later if not explicitly ignored. @@ -423,7 +422,7 @@ func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { // Defer checking of unexported fields until later to give an // Ignore a chance to ignore the field. if !vax.IsValid() || !vay.IsValid() { - // For unsafeRetrieveField to work, the parent struct must + // For retrieveUnexportedField to work, the parent struct must // be addressable. Create a new copy of the values if // necessary to make them addressable. vax = makeAddressable(vx) diff --git a/cmp/unsafe_panic.go b/cmp/export_panic.go similarity index 65% rename from cmp/unsafe_panic.go rename to cmp/export_panic.go index 8193f08..abc3a1c 100644 --- a/cmp/unsafe_panic.go +++ b/cmp/export_panic.go @@ -10,6 +10,6 @@ import "reflect" const supportAllowUnexported = false -func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value { - panic("unsafeRetrieveField is not implemented") +func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value { + panic("retrieveUnexportedField is not implemented") } diff --git a/cmp/unsafe_reflect.go b/cmp/export_unsafe.go similarity index 67% rename from cmp/unsafe_reflect.go rename to cmp/export_unsafe.go index 926fd47..59d4ee9 100644 --- a/cmp/unsafe_reflect.go +++ b/cmp/export_unsafe.go @@ -13,11 +13,11 @@ import ( const supportAllowUnexported = true -// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct -// such that the value has read-write permissions. +// 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. -func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value { +func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value { return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() } From 3e44f050a3ba1ebec4b48a79f0e8c63dbb4a9772 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Feb 2019 17:53:42 -0800 Subject: [PATCH 03/99] Rename {NDiff,NSame} as {NumDiff,NumSame} (#118) --- cmp/compare.go | 4 +-- cmp/internal/diff/diff.go | 16 ++++++------ cmp/internal/diff/diff_test.go | 48 +++++++++++++++++----------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 7681d4a..fda1670 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -558,9 +558,9 @@ func (s *state) popStep() { func (s *state) report(eq bool) { if eq { - s.result.NSame++ + s.result.NumSame++ } else { - s.result.NDiff++ + s.result.NumDiff++ } for _, r := range s.reporters { if eq { diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 6326465..8179011 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -85,22 +85,22 @@ func (es EditScript) LenY() int { return len(es) - es.stats().NX } type EqualFunc func(ix int, iy int) Result // Result is the result of comparison. -// NSame is the number of sub-elements that are equal. -// NDiff is the number of sub-elements that are not equal. -type Result struct{ NSame, NDiff int } +// NumSame is the number of sub-elements that are equal. +// NumDiff is the number of sub-elements that are not equal. +type Result struct{ NumSame, NumDiff int } // Equal indicates whether the symbols are equal. Two symbols are equal -// if and only if NDiff == 0. If Equal, then they are also Similar. -func (r Result) Equal() bool { return r.NDiff == 0 } +// if and only if NumDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NumDiff == 0 } // Similar indicates whether two symbols are similar and may be represented // by using the Modified type. As a special case, we consider binary comparisons // (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. // -// The exact ratio of NSame to NDiff to determine similarity may change. +// The exact ratio of NumSame to NumDiff to determine similarity may change. func (r Result) Similar() bool { - // Use NSame+1 to offset NSame so that binary comparisons are similar. - return r.NSame+1 >= r.NDiff + // Use NumSame+1 to offset NumSame so that binary comparisons are similar. + return r.NumSame+1 >= r.NumDiff } // Difference reports whether two lists of lengths nx and ny are equal diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go index 0752f4a..ef39077 100644 --- a/cmp/internal/diff/diff_test.go +++ b/cmp/internal/diff/diff_test.go @@ -387,9 +387,9 @@ func compareByte(x, y byte) (r Result) { } var ( - equalResult = Result{NDiff: 0} - similarResult = Result{NDiff: 1} - differentResult = Result{NDiff: 2} + equalResult = Result{NumDiff: 0} + similarResult = Result{NumDiff: 1} + differentResult = Result{NumDiff: 2} ) func TestResult(t *testing.T) { @@ -398,39 +398,39 @@ func TestResult(t *testing.T) { wantEqual bool wantSimilar bool }{ - // equalResult is equal since NDiff == 0, by definition of Equal method. + // equalResult is equal since NumDiff == 0, by definition of Equal method. {equalResult, true, true}, // similarResult is similar since it is a binary result where only one - // element was compared (i.e., Either NSame==1 or NDiff==1). + // element was compared (i.e., Either NumSame==1 or NumDiff==1). {similarResult, false, true}, // differentResult is different since there are enough differences that // it isn't even considered similar. {differentResult, false, false}, // Zero value is always equal. - {Result{NSame: 0, NDiff: 0}, true, true}, + {Result{NumSame: 0, NumDiff: 0}, true, true}, - // Binary comparisons (where NSame+NDiff == 1) are always similar. - {Result{NSame: 1, NDiff: 0}, true, true}, - {Result{NSame: 0, NDiff: 1}, false, true}, + // Binary comparisons (where NumSame+NumDiff == 1) are always similar. + {Result{NumSame: 1, NumDiff: 0}, true, true}, + {Result{NumSame: 0, NumDiff: 1}, false, true}, // More complex ratios. The exact ratio for similarity may change, // and may require updates to these test cases. - {Result{NSame: 1, NDiff: 1}, false, true}, - {Result{NSame: 1, NDiff: 2}, false, true}, - {Result{NSame: 1, NDiff: 3}, false, false}, - {Result{NSame: 2, NDiff: 1}, false, true}, - {Result{NSame: 2, NDiff: 2}, false, true}, - {Result{NSame: 2, NDiff: 3}, false, true}, - {Result{NSame: 3, NDiff: 1}, false, true}, - {Result{NSame: 3, NDiff: 2}, false, true}, - {Result{NSame: 3, NDiff: 3}, false, true}, - {Result{NSame: 1000, NDiff: 0}, true, true}, - {Result{NSame: 1000, NDiff: 1}, false, true}, - {Result{NSame: 1000, NDiff: 2}, false, true}, - {Result{NSame: 0, NDiff: 1000}, false, false}, - {Result{NSame: 1, NDiff: 1000}, false, false}, - {Result{NSame: 2, NDiff: 1000}, false, false}, + {Result{NumSame: 1, NumDiff: 1}, false, true}, + {Result{NumSame: 1, NumDiff: 2}, false, true}, + {Result{NumSame: 1, NumDiff: 3}, false, false}, + {Result{NumSame: 2, NumDiff: 1}, false, true}, + {Result{NumSame: 2, NumDiff: 2}, false, true}, + {Result{NumSame: 2, NumDiff: 3}, false, true}, + {Result{NumSame: 3, NumDiff: 1}, false, true}, + {Result{NumSame: 3, NumDiff: 2}, false, true}, + {Result{NumSame: 3, NumDiff: 3}, false, true}, + {Result{NumSame: 1000, NumDiff: 0}, true, true}, + {Result{NumSame: 1000, NumDiff: 1}, false, true}, + {Result{NumSame: 1000, NumDiff: 2}, false, true}, + {Result{NumSame: 0, NumDiff: 1000}, false, false}, + {Result{NumSame: 1, NumDiff: 1000}, false, false}, + {Result{NumSame: 2, NumDiff: 1000}, false, false}, } for _, tt := range tests { From ce5a20dabf115a429c982a468ae9d703c0a11e71 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Feb 2019 21:53:11 -0800 Subject: [PATCH 04/99] Update to go1.12 (#120) --- .travis.yml | 3 +++ go.mod | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 93ed6a8..ae1878d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ matrix: script: - go test -v -race ./... - go: 1.11.x + script: + - go test -v -race ./... + - go: 1.12.x script: - diff -u <(echo -n) <(gofmt -d .) - go test -v -race ./... diff --git a/go.mod b/go.mod index aa2daba..6c0e40e 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/google/go-cmp + +go 1.8 From 7586b665d3c0159456ec158ed00f5e98715113ea Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 26 Feb 2019 10:12:38 -0800 Subject: [PATCH 05/99] Add Values method to PathStep (#119) Some of the value information is already in the path (e.g., MapIndex.Key and SliceIndex.Key), which seems to suggest that all of the value information should be in the Path. Doing so has several benefits: * It simplifies the comparison logic * It paves the way for FilterPath to be able to ignore missing slice elements or map entries * It allows for a simpler API for custom reporters One regression introduced by this change is the removal of batching for slices when sufficient number of elements differ. This logic was added as a hack in the past to keep the reporter output small in such cases. However, this batching functionality should be the responsibility of the reporter logic, not the comparer logic. A future change will refactor the reporter logic to reintroduce that feature. --- cmp/compare.go | 231 +++++++++++++++++--------------------------- cmp/compare_test.go | 106 ++++++++++++++++++-- cmp/options.go | 101 ++++++++++--------- cmp/path.go | 68 +++++++++++-- cmp/report.go | 9 +- go.mod | 2 - 6 files changed, 301 insertions(+), 216 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index fda1670..8502c6b 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -36,14 +36,9 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) -var nothing = reflect.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: // -// • If two values are not of the same type, then they are never equal -// and the overall result is false. -// // • Let S be the set of all Ignore, Transformer, and Comparer options that // remain after applying all path filters, value filters, and type filters. // If at least one Ignore exists in S, then the comparison is ignored. @@ -56,26 +51,34 @@ var nothing = reflect.Value{} // // • 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. +// 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. -// Pointers are equal if the underlying values they point to are also equal. -// Interfaces are equal if their underlying concrete values are also equal. // -// Structs are equal if all of their fields are equal. If a struct contains -// unexported fields, Equal panics unless the AllowUnexported option is used or -// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. +// 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 AllowUnexported +// 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 slice or array elements report equal. +// Empty non-nil slices and nil slices are not equal; to equate empty slices, +// consider using cmpopts.EquateEmpty. // -// Arrays, slices, and maps are equal if they are both nil or both non-nil -// with the same length and the elements at each index or key are equal. -// Note that a non-nil empty slice and a nil slice are not equal. -// To equate empty slices and maps, consider using cmpopts.EquateEmpty. +// Maps are equal if they are both nil or both non-nil, where recursively +// calling Equal on all 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. func Equal(x, y interface{}, opts ...Option) bool { vx := reflect.ValueOf(x) vy := reflect.ValueOf(y) @@ -100,9 +103,7 @@ func Equal(x, y interface{}, opts ...Option) bool { } s := newState(opts) - s.pushStep(&pathStep{typ: t}, vx, vy) - s.compareAny(vx, vy) - s.popStep() + s.compareAny(&pathStep{t, vx, vy}) return s.result.Equal() } @@ -184,7 +185,7 @@ func (s *state) processOption(opt Option) { // statelessCompare compares two values and returns the result. // This function is stateless in that it does not alter the current result, // or output to any registered reporters. -func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { +func (s *state) statelessCompare(step PathStep) diff.Result { // We do not save and restore the curPath because all of the compareX // methods should properly push and pop from the path. // It is an implementation bug if the contents of curPath differs from @@ -193,35 +194,42 @@ func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { oldResult, oldReporters := s.result, s.reporters s.result = diff.Result{} // Reset result s.reporters = nil // Remove reporters to avoid spurious printouts - s.compareAny(vx, vy) + s.compareAny(step) res := s.result s.result, s.reporters = oldResult, oldReporters return res } -func (s *state) compareAny(vx, vy reflect.Value) { +func (s *state) compareAny(step PathStep) { // TODO: Support cyclic data structures. + + // Update the path stack. + s.curPath.push(step) + defer s.curPath.pop() + for _, r := range s.reporters { + r.PushStep(step) + defer r.PopStep() + } s.recChecker.Check(s.curPath) - // Rule 0: Differing types are never equal. + // Obtain the current type and values. + t := step.Type() + vx, vy := step.Values() + + // TODO: Removing this check allows us FilterPath to operate on missing + // slice elements and map entries. if !vx.IsValid() || !vy.IsValid() { s.report(vx.IsValid() == vy.IsValid()) return } - if vx.Type() != vy.Type() { - s.report(false) - return - } - t := vx.Type() - vx, vy = s.tryExporting(vx, vy) // Rule 1: Check whether an option applies on this node in the value tree. - if s.tryOptions(vx, vy, t) { + if s.tryOptions(t, vx, vy) { return } // Rule 2: Check whether the type has a valid Equal method. - if s.tryMethod(vx, vy, t) { + if s.tryMethod(t, vx, vy) { return } @@ -252,7 +260,7 @@ func (s *state) compareAny(vx, vy reflect.Value) { s.report(vx.IsNil() && vy.IsNil()) return case reflect.Struct: - s.compareStruct(vx, vy, t) + s.compareStruct(t, vx, vy) return case reflect.Slice: if vx.IsNil() || vy.IsNil() { @@ -261,10 +269,10 @@ func (s *state) compareAny(vx, vy reflect.Value) { } fallthrough case reflect.Array: - s.compareSlice(vx, vy, t) + s.compareSlice(t, vx, vy) return case reflect.Map: - s.compareMap(vx, vy, t) + s.compareMap(t, vx, vy) return case reflect.Ptr: if vx.IsNil() || vy.IsNil() { @@ -272,9 +280,7 @@ func (s *state) compareAny(vx, vy reflect.Value) { return } vx, vy = vx.Elem(), vy.Elem() - s.pushStep(&indirect{pathStep{t.Elem()}}, vx, vy) - s.compareAny(vx, vy) - s.popStep() + s.compareAny(&indirect{pathStep{t.Elem(), vx, vy}}) return case reflect.Interface: if vx.IsNil() || vy.IsNil() { @@ -286,49 +292,31 @@ func (s *state) compareAny(vx, vy reflect.Value) { s.report(false) return } - s.pushStep(&typeAssertion{pathStep{vx.Type()}}, vx, vy) - s.compareAny(vx, vy) - s.popStep() + s.compareAny(&typeAssertion{pathStep{vx.Type(), vx, vy}}) return default: panic(fmt.Sprintf("%v kind not handled", t.Kind())) } } -func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { - if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { - if sf.force { - // Forcibly obtain read-write access to an unexported struct field. - vx = retrieveUnexportedField(sf.pvx, sf.field) - vy = retrieveUnexportedField(sf.pvy, sf.field) - } else { - // We are not allowed to export the value, so invalidate them - // so that tryOptions can panic later if not explicitly ignored. - vx = nothing - vy = nothing - } - } - return vx, vy -} - -func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { +func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { // If there were no FilterValues, we will not detect invalid inputs, - // so manually check for them and append invalid if necessary. + // so manually check for them and append a validator if necessary. // We still evaluate the options since an ignore can override invalid. opts := s.opts - if !vx.IsValid() || !vy.IsValid() { - opts = Options{opts, invalid{}} + if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { + opts = Options{opts, validator{}} } // Evaluate all filters and apply the remaining options. - if opt := opts.filter(s, vx, vy, t); opt != nil { + if opt := opts.filter(s, t, vx, vy); opt != nil { opt.apply(s, vx, vy) return true } return false } -func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { +func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { // Check if this type even has an Equal method. m, ok := t.MethodByName("Equal") if !ok || !function.IsType(m.Type, function.EqualAssignable) { @@ -340,7 +328,7 @@ func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { return true } -func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { +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] @@ -351,11 +339,12 @@ func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { // unsafe mutations to the input. c := make(chan reflect.Value) go detectRaces(c, f, v) + got := <-c want := f.Call([]reflect.Value{v})[0] - if got := <-c; !s.statelessCompare(got, want).Equal() { + if step.vx, step.vy = got, want; !s.statelessCompare(step).Equal() { // To avoid false-positives with non-reflexive equality operations, // we sanity check whether a value is equal to itself. - if !s.statelessCompare(want, want).Equal() { + if step.vx, step.vy = want, want; !s.statelessCompare(step).Equal() { return want } panic(fmt.Sprintf("non-deterministic function detected: %s", function.NameOf(f))) @@ -376,8 +365,9 @@ func (s *state) callTTBFunc(f, x, y reflect.Value) bool { // unsafe mutations to the input. c := make(chan reflect.Value) go detectRaces(c, f, y, x) + got := <-c want := f.Call([]reflect.Value{x, y})[0].Bool() - if got := <-c; !got.IsValid() || got.Bool() != want { + if !got.IsValid() || got.Bool() != want { panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", function.NameOf(f))) } return want @@ -405,13 +395,14 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { return v } -func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { +func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { var vax, vay reflect.Value // Addressable versions of vx and vy step := &structField{} for i := 0; i < t.NumField(); i++ { - vvx, vvy := vx.Field(i), vy.Field(i) step.typ = t.Field(i).Type + step.vx = vx.Field(i) + step.vy = vy.Field(i) step.name = t.Field(i).Name step.idx = i step.unexported = !isExported(step.name) @@ -428,71 +419,48 @@ func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { vax = makeAddressable(vx) vay = makeAddressable(vy) } - step.force = s.exporters[t] + step.mayForce = s.exporters[t] step.pvx = vax step.pvy = vay step.field = t.Field(i) } - s.pushStep(step, vvx, vvy) - s.compareAny(vvx, vvy) - s.popStep() + s.compareAny(step) } } -func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { - step := &sliceIndex{pathStep{t.Elem()}, 0, 0} +func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { + step := &sliceIndex{pathStep: pathStep{typ: t.Elem()}} + 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 + } // Compute an edit-script for slices vx and vy. - es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { - step.xkey, step.ykey = ix, iy - s.curPath.push(step) - ret := s.statelessCompare(vx.Index(ix), vy.Index(iy)) - s.curPath.pop() - return ret + edits := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + return s.statelessCompare(withIndexes(ix, iy)) }) - // Report the entire slice as is if the arrays are of primitive kind, - // and the arrays are different enough. - isPrimitive := false - switch t.Elem().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: - isPrimitive = true - } - if isPrimitive && es.Dist() > (vx.Len()+vy.Len())/4 { - s.report(false) - return - } - // Replay the edit-script. var ix, iy int - for _, e := range es { + for _, e := range edits { switch e { case diff.UniqueX: - step.xkey, step.ykey = ix, -1 - vvx := vx.Index(ix) - s.pushStep(step, vvx, nothing) - s.report(false) - s.popStep() + s.compareAny(withIndexes(ix, -1)) ix++ case diff.UniqueY: - step.xkey, step.ykey = -1, iy - vvy := vy.Index(iy) - s.pushStep(step, nothing, vvy) - s.report(false) - s.popStep() + s.compareAny(withIndexes(-1, iy)) iy++ default: - step.xkey, step.ykey = ix, iy - vvx, vvy := vx.Index(ix), vy.Index(iy) - s.pushStep(step, vvx, vvy) - if e == diff.Identity { - s.report(true) - } else { - s.compareAny(vvx, vvy) - } - s.popStep() + s.compareAny(withIndexes(ix, iy)) ix++ iy++ } @@ -500,7 +468,7 @@ func (s *state) compareSlice(vx, vy reflect.Value, t reflect.Type) { return } -func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { +func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { if vx.IsNil() || vy.IsNil() { s.report(vx.IsNil() && vy.IsNil()) return @@ -508,20 +476,13 @@ func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { // We combine and sort the two map keys so that we can perform the // comparisons in a deterministic order. - step := &mapIndex{pathStep: pathStep{t.Elem()}} + step := &mapIndex{pathStep: pathStep{typ: t.Elem()}} for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.vx = vx.MapIndex(k) + step.vy = vy.MapIndex(k) step.key = k - vvx, vvy := vx.MapIndex(k), vy.MapIndex(k) - s.pushStep(step, vvx, vvy) - switch { - case vvx.IsValid() && vvy.IsValid(): - s.compareAny(vvx, vvy) - case vvx.IsValid() && !vvy.IsValid(): - s.report(false) - case !vvx.IsValid() && vvy.IsValid(): - s.report(false) - default: - // It is possible for both vvx and vvy to be invalid if the + if !step.vx.IsValid() && !step.vy.IsValid() { + // It is possible for both vx and vy to be invalid if the // key contained a NaN value in it. // // Even with the ability to retrieve NaN keys in Go 1.12, @@ -538,21 +499,7 @@ func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { const help = "consider providing a Comparer to compare the map" panic(fmt.Sprintf("%#v has map key with NaNs\n%s", s.curPath, help)) } - s.popStep() - } -} - -func (s *state) pushStep(ps PathStep, x, y reflect.Value) { - s.curPath.push(ps) - for _, r := range s.reporters { - r.PushStep(ps, x, y) - } -} - -func (s *state) popStep() { - s.curPath.pop() - for _, r := range s.reporters { - r.PopStep() + s.compareAny(step) } } diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 0a850f9..d7a8f9b 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -14,6 +14,7 @@ import ( "math/rand" "reflect" "regexp" + "runtime" "sort" "strings" "sync" @@ -124,6 +125,10 @@ func comparerTests() []test { } return []test{{ + label: label, + x: nil, + y: nil, + }, { label: label, x: 1, y: 1, @@ -327,9 +332,57 @@ root: x: md5.Sum([]byte{'a'}), y: md5.Sum([]byte{'b'}), wantDiff: ` -{[16]uint8}: - -: [16]uint8{0x0c, 0xc1, 0x75, 0xb9, 0xc0, 0xf1, 0xb6, 0xa8, 0x31, 0xc3, 0x99, 0xe2, 0x69, 0x77, 0x26, 0x61} - +: [16]uint8{0x92, 0xeb, 0x5f, 0xfe, 0xe6, 0xae, 0x2f, 0xec, 0x3a, 0xd7, 0x1c, 0x77, 0x75, 0x31, 0x57, 0x8f}`, +{[16]uint8}[0]: + -: 0x0c + +: 0x92 +{[16]uint8}[1]: + -: 0xc1 + +: 0xeb +{[16]uint8}[2]: + -: 0x75 + +: 0x5f +{[16]uint8}[3]: + -: 0xb9 + +: 0xfe +{[16]uint8}[4]: + -: 0xc0 + +: 0xe6 +{[16]uint8}[5]: + -: 0xf1 + +: 0xae +{[16]uint8}[6]: + -: 0xb6 + +: 0x2f +{[16]uint8}[7]: + -: 0xa8 + +: 0xec +{[16]uint8}[8]: + -: 0x31 + +: 0x3a +{[16]uint8}[9]: + -: 0xc3 + +: 0xd7 +{[16]uint8}[10]: + -: 0x99 + +: 0x1c +{[16]uint8}[11->?]: + -: 0xe2 + +: +{[16]uint8}[12->?]: + -: 0x69 + +: +{[16]uint8}[?->12]: + -: + +: 0x75 +{[16]uint8}[?->13]: + -: + +: 0x31 +{[16]uint8}[14]: + -: 0x26 + +: 0x57 +{[16]uint8}[15]: + -: 0x61 + +: 0x8f`, }, { label: label, x: new(fmt.Stringer), @@ -410,9 +463,36 @@ root: }), }, wantDiff: ` -{[]int}: - -: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - +: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}`, +λ({[]int}[0]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[1]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[2]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[3]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[4]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[5]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[6]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[7]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[8]): + -: float64(NaN) + +: float64(NaN) +λ({[]int}[9]): + -: float64(NaN) + +: float64(NaN)`, }, { // Ensure reasonable Stringer formatting of map keys. label: label, @@ -729,6 +809,16 @@ func embeddedTests() []test { return s } + // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122). + // The upstream fix landed in Go1.10, so we can remove this when dropping + // support for Go1.9 and below. + wantPanicNotGo110 := func(s string) string { + if v := runtime.Version(); strings.HasPrefix(v, "go1.8") || strings.HasPrefix(v, "go1.9") { + return "" + } + return s + } + return []test{{ label: label + "ParentStructA", x: ts.ParentStructA{}, @@ -1041,7 +1131,7 @@ func embeddedTests() []test { label: label + "ParentStructG", x: ts.ParentStructG{}, y: ts.ParentStructG{}, - wantPanic: "cannot handle unexported field", + wantPanic: wantPanicNotGo110("cannot handle unexported field"), }, { label: label + "ParentStructG", x: ts.ParentStructG{}, @@ -1127,7 +1217,7 @@ func embeddedTests() []test { label: label + "ParentStructI", x: ts.ParentStructI{}, y: ts.ParentStructI{}, - wantPanic: "cannot handle unexported field", + wantPanic: wantPanicNotGo110("cannot handle unexported field"), }, { label: label + "ParentStructI", x: ts.ParentStructI{}, diff --git a/cmp/options.go b/cmp/options.go index a8307dc..964cf81 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -29,11 +29,11 @@ type Option interface { // An Options is returned only if multiple comparers or transformers // can apply simultaneously and will only contain values of those types // or sub-Options containing values of those types. - filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption + filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption } // applicableOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer +// Fundamental: ignore | validator | *comparer | *transformer // Grouping: Options type applicableOption interface { Option @@ -43,7 +43,7 @@ type applicableOption interface { } // coreOption represents the following types: -// Fundamental: ignore | invalid | *comparer | *transformer +// Fundamental: ignore | validator | *comparer | *transformer // Filters: *pathFilter | *valuesFilter type coreOption interface { Option @@ -63,19 +63,19 @@ func (core) isCore() {} // on all individual options held within. type Options []Option -func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { +func (opts Options) filter(s *state, t reflect.Type, vx, vy reflect.Value) (out applicableOption) { for _, opt := range opts { - switch opt := opt.filter(s, vx, vy, t); opt.(type) { + switch opt := opt.filter(s, t, vx, vy); opt.(type) { case ignore: return ignore{} // Only ignore can short-circuit evaluation - case invalid: - out = invalid{} // Takes precedence over comparer or transformer + case validator: + out = validator{} // Takes precedence over comparer or transformer case *comparer, *transformer, Options: switch out.(type) { case nil: out = opt - case invalid: - // Keep invalid + case validator: + // Keep validator case *comparer, *transformer, Options: out = Options{out, opt} // Conflicting comparers or transformers } @@ -124,9 +124,9 @@ type pathFilter struct { opt Option } -func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { +func (f pathFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { if f.fnc(s.curPath) { - return f.opt.filter(s, vx, vy, t) + return f.opt.filter(s, t, vx, vy) } return nil } @@ -137,8 +137,9 @@ func (f pathFilter) String() string { // FilterValues returns a new Option where opt is only evaluated if filter f, // which is a function of the form "func(T, T) bool", returns true for the -// current pair of values being compared. If the type of the values is not -// assignable to T, then this filter implicitly returns false. +// current pair of values being compared. If either value is invalid or +// the type of the values is not assignable to T, then this filter implicitly +// returns false. // // The filter function must be // symmetric (i.e., agnostic to the order of the inputs) and @@ -170,12 +171,12 @@ type valuesFilter struct { opt Option } -func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { - if !vx.IsValid() || !vy.IsValid() { - return invalid{} +func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { + if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { + return validator{} } if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { - return f.opt.filter(s, vx, vy, t) + return f.opt.filter(s, t, vx, vy) } return nil } @@ -192,18 +193,27 @@ func Ignore() Option { return ignore{} } type ignore struct{ core } func (ignore) isFiltered() bool { return false } -func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } +func (ignore) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { return ignore{} } func (ignore) apply(s *state, _, _ reflect.Value) { s.reportIgnore() } func (ignore) String() string { return "Ignore()" } -// invalid is a sentinel Option type to indicate that some options could not -// be evaluated due to unexported fields. -type invalid struct{ core } +// validator is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields, missing slice elements, or +// missing map entries. Both values are validator only for unexported fields. +type validator struct{ core } -func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } -func (invalid) apply(s *state, _, _ reflect.Value) { - const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" - panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) +func (validator) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + return validator{} +} +func (validator) apply(s *state, vx, vy reflect.Value) { + // Unable to Interface implies unexported field without visibility access. + if (vx.IsValid() && !vx.CanInterface()) || (vy.IsValid() && !vy.CanInterface()) { + const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" + panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) + } + + // Implies missing slice element or map entry. + s.report(vx.IsValid() == vy.IsValid()) } // identRx represents a valid identifier according to the Go specification. @@ -260,7 +270,7 @@ type transformer struct { func (tr *transformer) isFiltered() bool { return tr.typ != nil } -func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption { +func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) applicableOption { for i := len(s.curPath) - 1; i >= 0; i-- { if t, ok := s.curPath[i].(*transform); !ok { break // Hit most recent non-Transform step @@ -275,17 +285,11 @@ func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) appl } func (tr *transformer) apply(s *state, vx, vy reflect.Value) { - // Update path before calling the Transformer so that dynamic checks - // will use the updated path. - step := &transform{pathStep{tr.fnc.Type().Out(0)}, tr} - s.curPath.push(step) - vvx := s.callTRFunc(tr.fnc, vx) - vvy := s.callTRFunc(tr.fnc, vy) - s.curPath.pop() - - s.pushStep(step, vvx, vvy) - s.compareAny(vvx, vvy) - s.popStep() + step := &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 { @@ -324,7 +328,7 @@ type comparer struct { func (cm *comparer) isFiltered() bool { return cm.typ != nil } -func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { +func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applicableOption { if cm.typ == nil || t.AssignableTo(cm.typ) { return cm } @@ -383,7 +387,7 @@ func AllowUnexported(types ...interface{}) Option { type visibleStructs map[reflect.Type]bool -func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { +func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { panic("not implemented") } @@ -411,14 +415,15 @@ const ( func reporter(r interface { // TODO: Export this option. - // PushStep is called when a tree-traversal operation is performed - // and provides the sub-values of x and y after applying the operation. - // The PathStep is valid until the step is popped, while the reflect.Values - // are valid while the entire tree is still being traversed. + // 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. + // + // Equal always call PushStep at the start to provide an operation-less + // PathStep used to report the root values. // - // Equal and Diff always call PushStep at the start to provide an - // operation-less PathStep used to report the root values. - PushStep(ps PathStep, x, y reflect.Value) + // The entries of a map are iterated through in an unspecified order. + PushStep(PathStep) // Report is called at exactly once on leaf nodes to report whether the // comparison identified the node as equal, unequal, or ignored. @@ -435,12 +440,12 @@ func reporter(r interface { type reporterOption struct{ reporterIface } type reporterIface interface { - PushStep(PathStep, reflect.Value, reflect.Value) + PushStep(PathStep) Report(reportFlags) PopStep() } -func (reporterOption) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { +func (reporterOption) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { panic("not implemented") } diff --git a/cmp/path.go b/cmp/path.go index 49b622b..ad6c0f5 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -29,23 +29,47 @@ type ( // these types as values of this type will be returned by this package. PathStep interface { String() string - Type() reflect.Type // Resulting type after performing the path step - isPathStep() + + // 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 + // AllowUnexported 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) } // StructField represents a struct field access on a field called Name. StructField interface { PathStep + + // Name is the field name. Name() string + + // Index is the index of the field in the parent struct type. + // See reflect.Type.Field. Index() int + isStructField() } // SliceIndex is an index operation on a slice or array at some index Key. SliceIndex interface { PathStep - Key() int // May return -1 if in a split state - // SplitKeys returns the indexes for indexing into slices in the + // Key is the index key; it may return -1 if in a split state + Key() int + + // 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 @@ -54,30 +78,39 @@ type ( // Key is guaranteed to return -1 if and only if the indexes returned // by SplitKeys are not the same. SplitKeys will never return -1 for // both indexes. - SplitKeys() (x int, y int) + SplitKeys() (ix, iy int) isSliceIndex() } // MapIndex is an index operation on a map at some index Key. MapIndex interface { PathStep + + // Key is the value of the map key. Key() reflect.Value + isMapIndex() } // Indirect represents pointer indirection on the parent type. Indirect interface { PathStep + isIndirect() } // TypeAssertion represents a type assertion on an interface. TypeAssertion interface { PathStep + isTypeAssertion() } // Transform is a transformation from the parent type to the current type. Transform interface { PathStep + + // Name is the name of the Transformer. Name() string + + // Func is the function pointer to the transformer function. Func() reflect.Value // Option returns the originally constructed Transformer option. @@ -185,7 +218,8 @@ func (pa Path) GoString() string { type ( pathStep struct { - typ reflect.Type + typ reflect.Type + vx, vy reflect.Value } structField struct { @@ -196,7 +230,7 @@ type ( // These fields are used for forcibly accessing an unexported field. // pvx, pvy, and field are only valid if unexported is true. unexported bool - force bool // Forcibly allow visibility + mayForce bool // Forcibly allow visibility pvx, pvy reflect.Value // Parent values field reflect.StructField // Field information } @@ -220,7 +254,8 @@ type ( } ) -func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } func (ps pathStep) String() string { if ps.typ == nil { return "" @@ -233,6 +268,19 @@ func (ps pathStep) String() string { } func (ps pathStep) isPathStep() {} +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) + vy = retrieveUnexportedField(sf.pvy, sf.field) + 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) } func (sf structField) Name() string { return sf.name } func (sf structField) Index() int { return sf.idx } @@ -259,8 +307,8 @@ func (si sliceIndex) Key() int { } return si.xkey } -func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey } -func (si sliceIndex) isSliceIndex() {} +func (si sliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } +func (si sliceIndex) isSliceIndex() {} func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } func (mi mapIndex) Key() reflect.Value { return mi.key } diff --git a/cmp/report.go b/cmp/report.go index 327f332..d3e4b28 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -16,7 +16,6 @@ type defaultReporter struct { Option curPath Path - curVals [][2]reflect.Value diffs []string // List of differences, possibly truncated ndiffs int // Total number of differences @@ -24,19 +23,17 @@ type defaultReporter struct { nlines int // Number of lines in diffs } -func (r *defaultReporter) PushStep(ps PathStep, x, y reflect.Value) { +func (r *defaultReporter) PushStep(ps PathStep) { r.curPath.push(ps) - r.curVals = append(r.curVals, [2]reflect.Value{x, y}) } func (r *defaultReporter) Report(f reportFlags) { if f == reportUnequal { - vs := r.curVals[len(r.curVals)-1] - r.report(vs[0], vs[1], r.curPath) + vx, vy := r.curPath.Last().Values() + r.report(vx, vy, r.curPath) } } func (r *defaultReporter) PopStep() { r.curPath.pop() - r.curVals = r.curVals[:len(r.curVals)-1] } func (r *defaultReporter) report(x, y reflect.Value, p Path) { diff --git a/go.mod b/go.mod index 6c0e40e..aa2daba 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1 @@ module github.com/google/go-cmp - -go 1.8 From cc11d21e54df4cff1c8c2f793a1874b0aeeedf68 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 26 Feb 2019 15:43:54 -0800 Subject: [PATCH 06/99] Augment Report to indicate how comparison was determined (#122) This is necessary for the upcoming reporter re-implementation so that it can format the differences with more intelligence. No tests added in this PR since an upcoming change will heavily test these code paths. --- cmp/compare.go | 50 ++++++++++++++++++++------------------------- cmp/options.go | 25 +++++++++++++++-------- cmp/options_test.go | 8 ++++---- cmp/report.go | 4 +--- 4 files changed, 43 insertions(+), 44 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 8502c6b..d5bad0c 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -219,7 +219,7 @@ func (s *state) compareAny(step PathStep) { // TODO: Removing this check allows us FilterPath to operate on missing // slice elements and map entries. if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid()) + s.report(vx.IsValid() == vy.IsValid(), 0) return } @@ -236,35 +236,35 @@ func (s *state) compareAny(step PathStep) { // Rule 3: Recursively descend into each value's underlying kind. switch t.Kind() { case reflect.Bool: - s.report(vx.Bool() == vy.Bool()) + s.report(vx.Bool() == vy.Bool(), 0) return case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - s.report(vx.Int() == vy.Int()) + s.report(vx.Int() == vy.Int(), 0) return case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - s.report(vx.Uint() == vy.Uint()) + s.report(vx.Uint() == vy.Uint(), 0) return case reflect.Float32, reflect.Float64: - s.report(vx.Float() == vy.Float()) + s.report(vx.Float() == vy.Float(), 0) return case reflect.Complex64, reflect.Complex128: - s.report(vx.Complex() == vy.Complex()) + s.report(vx.Complex() == vy.Complex(), 0) return case reflect.String: - s.report(vx.String() == vy.String()) + s.report(vx.String() == vy.String(), 0) return case reflect.Chan, reflect.UnsafePointer: - s.report(vx.Pointer() == vy.Pointer()) + s.report(vx.Pointer() == vy.Pointer(), 0) return case reflect.Func: - s.report(vx.IsNil() && vy.IsNil()) + s.report(vx.IsNil() && vy.IsNil(), 0) return case reflect.Struct: s.compareStruct(t, vx, vy) return case reflect.Slice: if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil()) + s.report(vx.IsNil() && vy.IsNil(), 0) return } fallthrough @@ -276,7 +276,7 @@ func (s *state) compareAny(step PathStep) { return case reflect.Ptr: if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil()) + s.report(vx.IsNil() && vy.IsNil(), 0) return } vx, vy = vx.Elem(), vy.Elem() @@ -284,12 +284,12 @@ func (s *state) compareAny(step PathStep) { return case reflect.Interface: if vx.IsNil() || vy.IsNil() { - s.report(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) + s.report(false, 0) return } s.compareAny(&typeAssertion{pathStep{vx.Type(), vx, vy}}) @@ -324,7 +324,7 @@ func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { } eq := s.callTTBFunc(m.Func, vx, vy) - s.report(eq) + s.report(eq, reportByMethod) return true } @@ -470,7 +470,7 @@ func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil()) + s.report(vx.IsNil() && vy.IsNil(), 0) return } @@ -503,24 +503,18 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { } } -func (s *state) report(eq bool) { - if eq { - s.result.NumSame++ - } else { - s.result.NumDiff++ - } - for _, r := range s.reporters { +func (s *state) report(eq bool, rf reportFlags) { + if rf&reportIgnored == 0 { if eq { - r.Report(reportEqual) + s.result.NumSame++ + rf |= reportEqual } else { - r.Report(reportUnequal) + s.result.NumDiff++ + rf |= reportUnequal } } -} - -func (s *state) reportIgnore() { for _, r := range s.reporters { - r.Report(reportIgnore) + r.Report(rf) } } diff --git a/cmp/options.go b/cmp/options.go index 964cf81..add5dd6 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -194,7 +194,7 @@ 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.reportIgnore() } +func (ignore) apply(s *state, _, _ reflect.Value) { s.report(true, reportIgnored) } func (ignore) String() string { return "Ignore()" } // validator is a sentinel Option type to indicate that some options could not @@ -213,7 +213,7 @@ func (validator) apply(s *state, vx, vy reflect.Value) { } // Implies missing slice element or map entry. - s.report(vx.IsValid() == vy.IsValid()) + s.report(vx.IsValid() == vy.IsValid(), 0) } // identRx represents a valid identifier according to the Go specification. @@ -337,7 +337,7 @@ func (cm *comparer) filter(_ *state, t reflect.Type, _, _ reflect.Value) applica func (cm *comparer) apply(s *state, vx, vy reflect.Value) { eq := s.callTTBFunc(cm.fnc, vx, vy) - s.report(eq) + s.report(eq, reportByFunc) } func (cm comparer) String() string { @@ -391,20 +391,27 @@ func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) appli panic("not implemented") } -type reportFlags uint64 +// reportFlags is a bit-set representing how a comparison was determined. +type reportFlags uint const ( _ reportFlags = (1 << iota) / 2 // reportEqual reports whether the node is equal. - // It may not be issued with reportIgnore or reportUnequal. + // This may be ORed with reportByMethod or reportByFunc. reportEqual // reportUnequal reports whether the node is not equal. - // It may not be issued with reportIgnore or reportEqual. + // This may be ORed with reportByMethod or reportByFunc. reportUnequal - // reportIgnore reports whether the node was ignored. - // It may not be issued with reportEqual or reportUnequal. - reportIgnore + // reportIgnored reports whether the node was ignored. + reportIgnored + + // reportByMethod reports whether equality was determined by calling the + // Equal method. This may be ORed with reportEqual or reportUnequal. + reportByMethod + // reportByFunc reports whether equality was determined by calling a custom + // Comparer function. This may be ORed with reportEqual or reportUnequal. + reportByFunc ) // reporter is an Option that can be passed to Equal. When Equal traverses diff --git a/cmp/options_test.go b/cmp/options_test.go index f5d2b4a..f876eab 100644 --- a/cmp/options_test.go +++ b/cmp/options_test.go @@ -128,7 +128,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, &defaultReporter{}}, + args: []interface{}{func(Path) bool { return true }, reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterPath", @@ -137,7 +137,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -170,7 +170,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, &defaultReporter{}}, + args: []interface{}{func(int, int) bool { return true }, reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -179,7 +179,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }} diff --git a/cmp/report.go b/cmp/report.go index d3e4b28..60f3f37 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -13,8 +13,6 @@ import ( ) type defaultReporter struct { - Option - curPath Path diffs []string // List of differences, possibly truncated @@ -27,7 +25,7 @@ func (r *defaultReporter) PushStep(ps PathStep) { r.curPath.push(ps) } func (r *defaultReporter) Report(f reportFlags) { - if f == reportUnequal { + if f&reportUnequal > 0 { vx, vy := r.curPath.Last().Values() r.report(vx, vy, r.curPath) } From fd81a2bda5e4727f3671d01a5a3c1d54be06f19d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 26 Feb 2019 16:33:33 -0800 Subject: [PATCH 07/99] Evaluate options even if values are invalid (#121) Rather than checking up-front whether the values are invalid, give tryOptions a chance to operate on them and potentially ignore the values. This is useful since a value can only be invalid if it is a missing slice element or map entry, and provides FilterPath combined with Ignore the ability to ignore such cases. Some complexity is added to compareSlice to look for ignored elements first before applying diffing so that we can decouple the stability of the diffing algorithm from the primary result. --- cmp/compare.go | 54 ++++++++++++++++++++++++++++---------- cmp/compare_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++--- cmp/options.go | 5 ++++ go.mod | 2 ++ 4 files changed, 107 insertions(+), 18 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index d5bad0c..748ba67 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -65,12 +65,12 @@ import ( // 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 slice or array elements report equal. +// 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 map entries report equal. +// 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, @@ -216,13 +216,6 @@ func (s *state) compareAny(step PathStep) { t := step.Type() vx, vy := step.Values() - // TODO: Removing this check allows us FilterPath to operate on missing - // slice elements and map entries. - if !vx.IsValid() || !vy.IsValid() { - s.report(vx.IsValid() == vy.IsValid(), 0) - return - } - // Rule 1: Check whether an option applies on this node in the value tree. if s.tryOptions(t, vx, vy) { return @@ -444,14 +437,47 @@ func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { return step } - // Compute an edit-script for slices vx and vy. - edits := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { - return s.statelessCompare(withIndexes(ix, iy)) + // 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 edit-script. + // Replay the ignore-scripts and the edit-script. var ix, iy int - for _, e := range edits { + 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)) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index d7a8f9b..a467fbe 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -32,11 +32,12 @@ var now = time.Now() func intPtr(n int) *int { return &n } type test struct { - label string // Test description + label string // Test name x, y interface{} // Input values to compare opts []cmp.Option // Input options wantDiff string // The exact difference string wantPanic string // Sub-string of an expected panic message + reason string // The reason for the expected outcome } func TestDiff(t *testing.T) { @@ -67,16 +68,17 @@ func TestDiff(t *testing.T) { }() 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", gotPanic) + t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) } if got, want := strings.TrimSpace(gotDiff), strings.TrimSpace(tt.wantDiff); got != want { - t.Fatalf("difference message:\ngot:\n%s\n\nwant:\n%s", got, want) + t.Fatalf("difference message:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, tt.wantDiff, tt.reason) } } else { if !strings.Contains(gotPanic, tt.wantPanic) { - t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + t.Fatalf("panic message:\ngot: %s\nwant: %s\nreason: %v", gotPanic, tt.wantPanic, tt.reason) } } }) @@ -549,6 +551,60 @@ root[1]["hr"]: label: label, x: struct{ _ string }{}, y: struct{ _ string }{}, + }, { + 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()), + }, + wantDiff: ` +{[2][]int}[1][5->3]: + -: 20 + +: 2`, + 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()), + }, + wantDiff: ` +{[2]map[string]int}[1]["keep2"]: + -: + +: 2`, + reason: "all zero map entries are ignored (even if missing)", }} } diff --git a/cmp/options.go b/cmp/options.go index add5dd6..c980c93 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -106,6 +106,11 @@ func (opts Options) String() string { // FilterPath returns a new Option where opt is only evaluated if filter f // returns true for the current Path in the value tree. // +// This filter is called even if a slice element or map entry is missing and +// provides an opportunity to ignore such cases. The filter function must be +// symmetric such that the filter result is identical regardless of whether the +// missing value is from x or y. +// // The option passed in may be an Ignore, Transformer, Comparer, Options, or // a previously filtered Option. func FilterPath(f func(Path) bool, opt Option) Option { diff --git a/go.mod b/go.mod index aa2daba..6c0e40e 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/google/go-cmp + +go 1.8 From 64cb04e86054b58b5c3658c2d7dfbdbb81753f05 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 27 Feb 2019 10:28:44 -0800 Subject: [PATCH 08/99] Add BenchmarkBytes (#125) Add a benchmark that uses cmp.{Equal,Diff} to measure performance of comparing large byte slices. Comparing large slices of primitives is generally considered the most pathological use-case for cmp since the rules of cmp dictate that it must run the filters on every element of the slice even if the filters never apply. In theory, a user of cmp could short-circuit comparison of slices by providing a comparer for slices (e.g., cmp.Comparer(bytes.Equal)), but it would be ideal for cmp to be sufficiently optimized that comparing large slices is not too costly. BenchmarkBytes/4KiB/EqualFilter0-4 300 3792755 ns/op 2.16 MB/s 628702 B/op 8276 allocs/op BenchmarkBytes/4KiB/EqualFilter1-4 200 6058637 ns/op 1.35 MB/s 890920 B/op 16471 allocs/op BenchmarkBytes/4KiB/EqualFilter2-4 200 7498098 ns/op 1.09 MB/s 890952 B/op 16472 allocs/op BenchmarkBytes/4KiB/EqualFilter3-4 100 10094237 ns/op 0.81 MB/s 891016 B/op 16473 allocs/op BenchmarkBytes/4KiB/EqualFilter4-4 100 11128655 ns/op 0.74 MB/s 891016 B/op 16473 allocs/op BenchmarkBytes/4KiB/EqualFilter5-4 100 12952062 ns/op 0.63 MB/s 891144 B/op 16474 allocs/op BenchmarkBytes/4KiB/DiffFilter0-4 300 4015119 ns/op 2.04 MB/s 629425 B/op 8305 allocs/op BenchmarkBytes/4KiB/DiffFilter1-4 200 6457963 ns/op 1.27 MB/s 891738 B/op 16501 allocs/op BenchmarkBytes/4KiB/DiffFilter2-4 200 8371022 ns/op 0.98 MB/s 891770 B/op 16502 allocs/op BenchmarkBytes/4KiB/DiffFilter3-4 200 9420398 ns/op 0.87 MB/s 891834 B/op 16503 allocs/op BenchmarkBytes/4KiB/DiffFilter4-4 100 12493540 ns/op 0.66 MB/s 891835 B/op 16503 allocs/op BenchmarkBytes/4KiB/DiffFilter5-4 100 14208928 ns/op 0.58 MB/s 891965 B/op 16504 allocs/op BenchmarkBytes/64KiB/EqualFilter0-4 20 66146276 ns/op 1.98 MB/s 10826734 B/op 131206 allocs/op BenchmarkBytes/64KiB/EqualFilter1-4 10 102687117 ns/op 1.28 MB/s 15021123 B/op 262281 allocs/op BenchmarkBytes/64KiB/EqualFilter2-4 10 125444269 ns/op 1.04 MB/s 15021174 B/op 262282 allocs/op BenchmarkBytes/64KiB/EqualFilter3-4 10 154392619 ns/op 0.85 MB/s 15021219 B/op 262283 allocs/op BenchmarkBytes/64KiB/EqualFilter4-4 10 183307772 ns/op 0.72 MB/s 15021219 B/op 262283 allocs/op BenchmarkBytes/64KiB/EqualFilter5-4 5 212334761 ns/op 0.62 MB/s 15021340 B/op 262284 allocs/op BenchmarkBytes/64KiB/DiffFilter0-4 20 67147375 ns/op 1.95 MB/s 10828312 B/op 131244 allocs/op BenchmarkBytes/64KiB/DiffFilter1-4 10 105385709 ns/op 1.24 MB/s 15022724 B/op 262319 allocs/op BenchmarkBytes/64KiB/DiffFilter2-4 10 135994379 ns/op 0.96 MB/s 15022763 B/op 262320 allocs/op BenchmarkBytes/64KiB/DiffFilter3-4 10 159346836 ns/op 0.82 MB/s 15022820 B/op 262321 allocs/op BenchmarkBytes/64KiB/DiffFilter4-4 10 190129916 ns/op 0.69 MB/s 15022843 B/op 262321 allocs/op BenchmarkBytes/64KiB/DiffFilter5-4 5 208805873 ns/op 0.63 MB/s 15022996 B/op 262322 allocs/op BenchmarkBytes/1MiB/EqualFilter0-4 2 978302544 ns/op 2.14 MB/s 173184064 B/op 2097346 allocs/op BenchmarkBytes/1MiB/EqualFilter1-4 1 1543190869 ns/op 1.36 MB/s 240293104 B/op 4194502 allocs/op BenchmarkBytes/1MiB/EqualFilter2-4 1 1998443802 ns/op 1.05 MB/s 240293232 B/op 4194504 allocs/op BenchmarkBytes/1MiB/EqualFilter3-4 1 2507293058 ns/op 0.84 MB/s 240293328 B/op 4194506 allocs/op BenchmarkBytes/1MiB/EqualFilter4-4 1 2981132381 ns/op 0.70 MB/s 240293008 B/op 4194502 allocs/op BenchmarkBytes/1MiB/EqualFilter5-4 1 3351177035 ns/op 0.63 MB/s 240293424 B/op 4194506 allocs/op BenchmarkBytes/1MiB/DiffFilter0-4 1 1132136753 ns/op 1.85 MB/s 173185752 B/op 2097384 allocs/op BenchmarkBytes/1MiB/DiffFilter1-4 1 1666196345 ns/op 1.26 MB/s 240294504 B/op 4194537 allocs/op BenchmarkBytes/1MiB/DiffFilter2-4 1 2204467232 ns/op 0.95 MB/s 240294600 B/op 4194539 allocs/op BenchmarkBytes/1MiB/DiffFilter3-4 1 2499107753 ns/op 0.84 MB/s 240294600 B/op 4194539 allocs/op BenchmarkBytes/1MiB/DiffFilter4-4 1 2966222324 ns/op 0.71 MB/s 240295016 B/op 4194544 allocs/op BenchmarkBytes/1MiB/DiffFilter5-4 1 3382045549 ns/op 0.62 MB/s 240294728 B/op 4194540 allocs/op BenchmarkBytes/16MiB/EqualFilter0-4 1 16585516720 ns/op 2.02 MB/s 2967917664 B/op 33554689 allocs/op BenchmarkBytes/16MiB/EqualFilter1-4 1 23980880452 ns/op 1.40 MB/s 4041659536 B/op 67109123 allocs/op BenchmarkBytes/16MiB/EqualFilter2-4 1 30729382462 ns/op 1.09 MB/s 4041659568 B/op 67109124 allocs/op BenchmarkBytes/16MiB/EqualFilter3-4 1 37830223988 ns/op 0.89 MB/s 4041660016 B/op 67109129 allocs/op BenchmarkBytes/16MiB/EqualFilter4-4 1 44731081109 ns/op 0.75 MB/s 4041659536 B/op 67109124 allocs/op BenchmarkBytes/16MiB/EqualFilter5-4 1 52110015114 ns/op 0.64 MB/s 4041659760 B/op 67109126 allocs/op BenchmarkBytes/16MiB/DiffFilter0-4 1 20349410654 ns/op 1.65 MB/s 2967919128 B/op 33554724 allocs/op BenchmarkBytes/16MiB/DiffFilter1-4 1 27073250483 ns/op 1.24 MB/s 4041661320 B/op 67109163 allocs/op BenchmarkBytes/16MiB/DiffFilter2-4 1 32223912220 ns/op 1.04 MB/s 4041661064 B/op 67109160 allocs/op BenchmarkBytes/16MiB/DiffFilter3-4 1 39189759283 ns/op 0.86 MB/s 4041661128 B/op 67109161 allocs/op BenchmarkBytes/16MiB/DiffFilter4-4 1 48344470628 ns/op 0.69 MB/s 4041661256 B/op 67109163 allocs/op BenchmarkBytes/16MiB/DiffFilter5-4 1 51184873999 ns/op 0.66 MB/s 4041661256 B/op 67109162 allocs/op The last benchmark shows that the current implementation allocates an astonishing 3.75GiB just to compare a 16MiB byte slice. --- cmp/compare_test.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index a467fbe..8c2cf34 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -2132,3 +2132,53 @@ func project4Tests() []test { +: `, }} } + +// 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]...) + } + }) + } + }) + } +} From 2e500c523dc96605f4021e8644a28ca61804db7b Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 27 Feb 2019 17:51:18 -0800 Subject: [PATCH 09/99] Add validator option once at state creation (#128) Avoid appending validator in every call to tryOptions and instead ensure that the state starts with one in the options list. Doing so dramatically reduces the number of allocations. benchmark old MB/s new MB/s speedup BenchmarkBytes/4KiB/EqualFilter0-4 2.16 2.56 1.19x BenchmarkBytes/4KiB/EqualFilter1-4 1.35 1.53 1.13x BenchmarkBytes/4KiB/EqualFilter2-4 1.09 1.20 1.10x BenchmarkBytes/4KiB/EqualFilter3-4 0.81 0.99 1.22x BenchmarkBytes/4KiB/EqualFilter4-4 0.74 0.77 1.04x BenchmarkBytes/4KiB/EqualFilter5-4 0.63 0.69 1.10x BenchmarkBytes/4KiB/DiffFilter0-4 2.04 2.38 1.17x BenchmarkBytes/4KiB/DiffFilter1-4 1.27 1.52 1.20x BenchmarkBytes/4KiB/DiffFilter2-4 0.98 1.15 1.17x BenchmarkBytes/4KiB/DiffFilter3-4 0.87 0.96 1.10x BenchmarkBytes/4KiB/DiffFilter4-4 0.66 0.81 1.23x BenchmarkBytes/4KiB/DiffFilter5-4 0.58 0.69 1.19x BenchmarkBytes/64KiB/EqualFilter0-4 1.98 2.31 1.17x BenchmarkBytes/64KiB/EqualFilter1-4 1.28 1.71 1.34x BenchmarkBytes/64KiB/EqualFilter2-4 1.04 1.23 1.18x BenchmarkBytes/64KiB/EqualFilter3-4 0.85 0.93 1.09x BenchmarkBytes/64KiB/EqualFilter4-4 0.72 0.83 1.15x BenchmarkBytes/64KiB/EqualFilter5-4 0.62 0.70 1.13x BenchmarkBytes/64KiB/DiffFilter0-4 1.95 2.29 1.17x BenchmarkBytes/64KiB/DiffFilter1-4 1.24 1.56 1.26x BenchmarkBytes/64KiB/DiffFilter2-4 0.96 1.14 1.19x BenchmarkBytes/64KiB/DiffFilter3-4 0.82 0.97 1.18x BenchmarkBytes/64KiB/DiffFilter4-4 0.69 0.79 1.14x BenchmarkBytes/64KiB/DiffFilter5-4 0.63 0.70 1.11x BenchmarkBytes/1MiB/EqualFilter0-4 2.14 2.40 1.12x BenchmarkBytes/1MiB/EqualFilter1-4 1.36 1.63 1.20x BenchmarkBytes/1MiB/EqualFilter2-4 1.05 1.12 1.07x BenchmarkBytes/1MiB/EqualFilter3-4 0.84 0.81 0.96x BenchmarkBytes/1MiB/EqualFilter4-4 0.70 0.72 1.03x BenchmarkBytes/1MiB/EqualFilter5-4 0.63 0.64 1.02x BenchmarkBytes/1MiB/DiffFilter0-4 1.85 2.07 1.12x BenchmarkBytes/1MiB/DiffFilter1-4 1.26 1.49 1.18x BenchmarkBytes/1MiB/DiffFilter2-4 0.95 1.00 1.05x BenchmarkBytes/1MiB/DiffFilter3-4 0.84 0.88 1.05x BenchmarkBytes/1MiB/DiffFilter4-4 0.71 0.72 1.01x BenchmarkBytes/1MiB/DiffFilter5-4 0.62 0.67 1.08x BenchmarkBytes/16MiB/EqualFilter0-4 2.02 2.16 1.07x BenchmarkBytes/16MiB/EqualFilter1-4 1.40 1.56 1.11x BenchmarkBytes/16MiB/EqualFilter2-4 1.09 1.06 0.97x BenchmarkBytes/16MiB/EqualFilter3-4 0.89 0.88 0.99x BenchmarkBytes/16MiB/EqualFilter4-4 0.75 0.76 1.01x BenchmarkBytes/16MiB/EqualFilter5-4 0.64 0.64 1.00x BenchmarkBytes/16MiB/DiffFilter0-4 1.65 2.15 1.30x BenchmarkBytes/16MiB/DiffFilter1-4 1.24 1.35 1.09x BenchmarkBytes/16MiB/DiffFilter2-4 1.04 0.97 0.93x BenchmarkBytes/16MiB/DiffFilter3-4 0.86 0.87 1.01x BenchmarkBytes/16MiB/DiffFilter4-4 0.69 0.75 1.09x BenchmarkBytes/16MiB/DiffFilter5-4 0.66 0.65 0.98x benchmark old allocs new allocs delta BenchmarkBytes/4KiB/EqualFilter0-4 8276 83 -99.00% BenchmarkBytes/4KiB/EqualFilter1-4 16471 84 -99.49% BenchmarkBytes/4KiB/EqualFilter2-4 16472 85 -99.48% BenchmarkBytes/4KiB/EqualFilter3-4 16473 85 -99.48% BenchmarkBytes/4KiB/EqualFilter4-4 16473 86 -99.48% BenchmarkBytes/4KiB/EqualFilter5-4 16474 86 -99.48% BenchmarkBytes/4KiB/DiffFilter0-4 8305 111 -98.66% BenchmarkBytes/4KiB/DiffFilter1-4 16501 112 -99.32% BenchmarkBytes/4KiB/DiffFilter2-4 16502 113 -99.32% BenchmarkBytes/4KiB/DiffFilter3-4 16503 113 -99.32% BenchmarkBytes/4KiB/DiffFilter4-4 16503 114 -99.31% BenchmarkBytes/4KiB/DiffFilter5-4 16504 114 -99.31% BenchmarkBytes/64KiB/EqualFilter0-4 131206 133 -99.90% BenchmarkBytes/64KiB/EqualFilter1-4 262281 134 -99.95% BenchmarkBytes/64KiB/EqualFilter2-4 262282 135 -99.95% BenchmarkBytes/64KiB/EqualFilter3-4 262283 135 -99.95% BenchmarkBytes/64KiB/EqualFilter4-4 262283 136 -99.95% BenchmarkBytes/64KiB/EqualFilter5-4 262284 137 -99.95% BenchmarkBytes/64KiB/DiffFilter0-4 131244 171 -99.87% BenchmarkBytes/64KiB/DiffFilter1-4 262319 172 -99.93% BenchmarkBytes/64KiB/DiffFilter2-4 262320 173 -99.93% BenchmarkBytes/64KiB/DiffFilter3-4 262321 173 -99.93% BenchmarkBytes/64KiB/DiffFilter4-4 262321 174 -99.93% BenchmarkBytes/64KiB/DiffFilter5-4 262322 174 -99.93% BenchmarkBytes/1MiB/EqualFilter0-4 2097346 192 -99.99% BenchmarkBytes/1MiB/EqualFilter1-4 4194502 193 -100.00% BenchmarkBytes/1MiB/EqualFilter2-4 4194504 196 -100.00% BenchmarkBytes/1MiB/EqualFilter3-4 4194506 194 -100.00% BenchmarkBytes/1MiB/EqualFilter4-4 4194502 198 -100.00% BenchmarkBytes/1MiB/EqualFilter5-4 4194506 196 -100.00% BenchmarkBytes/1MiB/DiffFilter0-4 2097384 229 -99.99% BenchmarkBytes/1MiB/DiffFilter1-4 4194537 231 -99.99% BenchmarkBytes/1MiB/DiffFilter2-4 4194539 233 -99.99% BenchmarkBytes/1MiB/DiffFilter3-4 4194539 234 -99.99% BenchmarkBytes/1MiB/DiffFilter4-4 4194544 233 -99.99% BenchmarkBytes/1MiB/DiffFilter5-4 4194540 233 -99.99% BenchmarkBytes/16MiB/EqualFilter0-4 33554689 256 -100.00% BenchmarkBytes/16MiB/EqualFilter1-4 67109123 255 -100.00% BenchmarkBytes/16MiB/EqualFilter2-4 67109124 257 -100.00% BenchmarkBytes/16MiB/EqualFilter3-4 67109129 257 -100.00% BenchmarkBytes/16MiB/EqualFilter4-4 67109124 258 -100.00% BenchmarkBytes/16MiB/EqualFilter5-4 67109126 257 -100.00% BenchmarkBytes/16MiB/DiffFilter0-4 33554724 292 -100.00% BenchmarkBytes/16MiB/DiffFilter1-4 67109163 293 -100.00% BenchmarkBytes/16MiB/DiffFilter2-4 67109160 294 -100.00% BenchmarkBytes/16MiB/DiffFilter3-4 67109161 294 -100.00% BenchmarkBytes/16MiB/DiffFilter4-4 67109163 295 -100.00% BenchmarkBytes/16MiB/DiffFilter5-4 67109162 299 -100.00% benchmark old bytes new bytes delta BenchmarkBytes/4KiB/EqualFilter0-4 628702 366504 -41.70% BenchmarkBytes/4KiB/EqualFilter1-4 890920 366536 -58.86% BenchmarkBytes/4KiB/EqualFilter2-4 890952 366601 -58.85% BenchmarkBytes/4KiB/EqualFilter3-4 891016 366602 -58.86% BenchmarkBytes/4KiB/EqualFilter4-4 891016 366730 -58.84% BenchmarkBytes/4KiB/EqualFilter5-4 891144 366728 -58.85% BenchmarkBytes/4KiB/DiffFilter0-4 629425 367149 -41.67% BenchmarkBytes/4KiB/DiffFilter1-4 891738 367180 -58.82% BenchmarkBytes/4KiB/DiffFilter2-4 891770 367244 -58.82% BenchmarkBytes/4KiB/DiffFilter3-4 891834 367245 -58.82% BenchmarkBytes/4KiB/DiffFilter4-4 891835 367372 -58.81% BenchmarkBytes/4KiB/DiffFilter5-4 891965 367372 -58.81% BenchmarkBytes/64KiB/EqualFilter0-4 10826734 6632425 -38.74% BenchmarkBytes/64KiB/EqualFilter1-4 15021123 6632443 -55.85% BenchmarkBytes/64KiB/EqualFilter2-4 15021174 6632502 -55.85% BenchmarkBytes/64KiB/EqualFilter3-4 15021219 6632540 -55.85% BenchmarkBytes/64KiB/EqualFilter4-4 15021219 6632640 -55.84% BenchmarkBytes/64KiB/EqualFilter5-4 15021340 6632707 -55.84% BenchmarkBytes/64KiB/DiffFilter0-4 10828312 6633995 -38.73% BenchmarkBytes/64KiB/DiffFilter1-4 15022724 6634049 -55.84% BenchmarkBytes/64KiB/DiffFilter2-4 15022763 6634091 -55.84% BenchmarkBytes/64KiB/DiffFilter3-4 15022820 6634081 -55.84% BenchmarkBytes/64KiB/DiffFilter4-4 15022843 6634241 -55.84% BenchmarkBytes/64KiB/DiffFilter5-4 15022996 6634222 -55.84% BenchmarkBytes/1MiB/EqualFilter0-4 173184064 106075104 -38.75% BenchmarkBytes/1MiB/EqualFilter1-4 240293104 106075088 -55.86% BenchmarkBytes/1MiB/EqualFilter2-4 240293232 106075344 -55.86% BenchmarkBytes/1MiB/EqualFilter3-4 240293328 106075152 -55.86% BenchmarkBytes/1MiB/EqualFilter4-4 240293008 106075504 -55.86% BenchmarkBytes/1MiB/EqualFilter5-4 240293424 106075376 -55.86% BenchmarkBytes/1MiB/DiffFilter0-4 173185752 106076648 -38.75% BenchmarkBytes/1MiB/DiffFilter1-4 240294504 106076776 -55.86% BenchmarkBytes/1MiB/DiffFilter2-4 240294600 106076936 -55.86% BenchmarkBytes/1MiB/DiffFilter3-4 240294600 106076936 -55.86% BenchmarkBytes/1MiB/DiffFilter4-4 240295016 106076968 -55.86% BenchmarkBytes/1MiB/DiffFilter5-4 240294728 106076968 -55.86% BenchmarkBytes/16MiB/EqualFilter0-4 2967917664 1894175792 -36.18% BenchmarkBytes/16MiB/EqualFilter1-4 4041659536 1894175696 -53.13% BenchmarkBytes/16MiB/EqualFilter2-4 4041659568 1894175856 -53.13% BenchmarkBytes/16MiB/EqualFilter3-4 4041660016 1894175856 -53.13% BenchmarkBytes/16MiB/EqualFilter4-4 4041659536 1894175984 -53.13% BenchmarkBytes/16MiB/EqualFilter5-4 4041659760 1894175888 -53.13% BenchmarkBytes/16MiB/DiffFilter0-4 2967919128 1894177352 -36.18% BenchmarkBytes/16MiB/DiffFilter1-4 4041661320 1894177352 -53.13% BenchmarkBytes/16MiB/DiffFilter2-4 4041661064 1894177448 -53.13% BenchmarkBytes/16MiB/DiffFilter3-4 4041661128 1894177448 -53.13% BenchmarkBytes/16MiB/DiffFilter4-4 4041661256 1894177576 -53.13% BenchmarkBytes/16MiB/DiffFilter5-4 4041661256 1894177832 -53.13% --- cmp/compare.go | 13 +++---------- cmp/options.go | 23 +++++++++++++++++------ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 748ba67..939a3b5 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -146,7 +146,8 @@ type state struct { } func newState(opts []Option) *state { - s := new(state) + // Always ensure a validator option exists to validate the inputs. + s := &state{opts: Options{validator{}}} for _, opt := range opts { s.processOption(opt) } @@ -293,16 +294,8 @@ func (s *state) compareAny(step PathStep) { } func (s *state) tryOptions(t reflect.Type, vx, vy reflect.Value) bool { - // If there were no FilterValues, we will not detect invalid inputs, - // so manually check for them and append a validator if necessary. - // We still evaluate the options since an ignore can override invalid. - opts := s.opts - if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { - opts = Options{opts, validator{}} - } - // Evaluate all filters and apply the remaining options. - if opt := opts.filter(s, t, vx, vy); opt != nil { + if opt := s.opts.filter(s, t, vx, vy); opt != nil { opt.apply(s, vx, vy) return true } diff --git a/cmp/options.go b/cmp/options.go index c980c93..6d4335b 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -178,7 +178,7 @@ type valuesFilter struct { func (f valuesFilter) filter(s *state, t reflect.Type, vx, vy reflect.Value) applicableOption { if !vx.IsValid() || !vx.CanInterface() || !vy.IsValid() || !vy.CanInterface() { - return validator{} + return nil } if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { return f.opt.filter(s, t, vx, vy) @@ -207,18 +207,29 @@ func (ignore) String() string // missing map entries. Both values are validator only for unexported fields. type validator struct{ core } -func (validator) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { - return validator{} +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.IsValid() && !vx.CanInterface()) || (vy.IsValid() && !vy.CanInterface()) { + if !vx.CanInterface() || !vy.CanInterface() { const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) } - // Implies missing slice element or map entry. - s.report(vx.IsValid() == vy.IsValid(), 0) + panic("not reachable") } // identRx represents a valid identifier according to the Go specification. From c81281657ad99ba22e14fda7c4dfaaf2974c454e Mon Sep 17 00:00:00 2001 From: LMMilewski Date: Wed, 27 Feb 2019 18:41:37 -0800 Subject: [PATCH 10/99] Nudge people to use custom comparers rather than Ignore/Allow Unexported options (#115) A common scenario is: 1. someone uses cmp.Diff on big.Int (transitively) 2. they get a message that says uses Ignore/Allow Unexported options 3. they see that AllowUnexported is hard to use correctly 4. they use IgnoreUnexported They end up with: cmpopts.IgnoreUnexported(big.Int{}) Which definitely doesn't do what's intended. If we point out that a custom comparer is what they most likely need, then they are more likely to use cmp correctly --- cmp/cmpopts/ignore.go | 4 ++++ cmp/options.go | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index e86554b..6d9b514 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -112,6 +112,10 @@ func (tf ifaceFilter) filter(p cmp.Path) bool { // In particular, unexported fields within the struct's exported fields // of struct types, including anonymous fields, will not be ignored unless the // type of the field itself is also passed to IgnoreUnexported. +// +// Avoid ignoring unexported fields of a type which you do not control (i.e. a +// type from another repository), as changes to the implementation of such types +// may change how the comparison behaves. Prefer a custom Comparer instead. func IgnoreUnexported(typs ...interface{}) cmp.Option { ux := newUnexportedFilter(typs...) return cmp.FilterPath(ux.filter, cmp.Ignore()) diff --git a/cmp/options.go b/cmp/options.go index 6d4335b..a265597 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -225,7 +225,7 @@ func (validator) apply(s *state, vx, vy reflect.Value) { // Unable to Interface implies unexported field without visibility access. if !vx.CanInterface() || !vy.CanInterface() { - const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" + const help = "consider using a custom Comparer; if you control the implementation of type, you can also consider AllowUnexported or cmpopts.IgnoreUnexported" panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) } @@ -371,7 +371,7 @@ func (cm comparer) String() string { // defined in an internal package where the semantic meaning of an unexported // field is in the control of the user. // -// For some cases, a custom Comparer should be used instead that defines +// In many cases, a custom Comparer should be used instead that defines // equality as a function of the public API of a type rather than the underlying // unexported implementation. // From 2940eda701e08ed0bd3cda4a6c69efb50af6db51 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 17:36:18 -0700 Subject: [PATCH 11/99] Implement a unified difference reporter (#124) The previous implementation of the reporter simply listed all differences, each qualified by the full path to the difference. This method of reporting is exact, but difficult for humans to parse. It is one of the more common sources of complaints by users and a significant reason why cmp is not preferred over competing libraries. This change reimplements the reporter to format the output as a structured literal in pseudo-Go syntax. The output resembles literals that the user would likely have in their test code. Differences between the x and y values are denoted by a '-' or '+' prefix at the start of the line. An overview of the new implementation is as follows: * report.go: The defaultReporter type implements the Reporter interface. * report_value: Through the PushStep/PopStep API, the defaultReporter is able to contruct an in-memory valueNode tree representing the comparison of x and y as cmp.Equal walks the sub-values. * report_compare.go: After report_value.go constructs an AST-representation of the compared values, report_compare.go formats the valueNode tree as a textNode tree, which is the textual output in a tree form. Some relevant design decisions include: * The format logic goes through effort to avoid printing ignored nodes. * Some number of surrounding equal (but not ignored) struct fields, slice elements, or map entries are printed for context. * cmp.Equal may declare two sub-reflect.Values to be equal, but are different values when printed. In order to present a unified view on this "equal" node, the logic formats both values and arbitrarily choses the one with the shorter string. * Transformed nodes are formatted with the pseudo-Go syntax of: Inverse(TransformerName, OutputType{...}) where Inverse is some magical pseudo-function that inverts the transformation referred to by TransformerName. The OutputType literal is the output of the transformation. * report_reflect.go: This contains logic to pretty-print reflect.Values and is relied upon by report_compare.go to format the leaves of the tree. Note that the leaves of the tree can be any arbitrary Go type and value (including cyclic data structures). * report_text.go: This contains logic for purely lexicographical formatting and is depended upon by the other report_*.go files. Advantages: * The output is more familiar as it uses pseudo-Go syntax for literals * It provides context about surrounding struct fields, slice elements, or map entries that were equal * Inserted and removed elements in a slice are easier to visualize * Related diffs lie on the same indentation * For diffs in a deeply nested value, the output is easier to visualize than having a list of all the full paths to the diff. Disadvantages: * The implementation is drastically more complex. * In most cases, the output is longer (though more sparse) --- cmp/compare.go | 21 +- cmp/compare_test.go | 1201 +++++++++++++++--------- cmp/example_test.go | 201 ++-- cmp/internal/flags/flags.go | 9 + cmp/internal/flags/toolchain_legacy.go | 10 + cmp/internal/flags/toolchain_recent.go | 10 + cmp/internal/value/format.go | 280 ------ cmp/internal/value/format_test.go | 93 -- cmp/internal/value/zero.go | 45 + cmp/internal/value/zero_test.go | 45 + cmp/path.go | 9 - cmp/report.go | 71 +- cmp/report_compare.go | 290 ++++++ cmp/report_reflect.go | 279 ++++++ cmp/report_text.go | 382 ++++++++ cmp/report_value.go | 120 +++ 16 files changed, 2115 insertions(+), 951 deletions(-) create mode 100644 cmp/internal/flags/flags.go create mode 100644 cmp/internal/flags/toolchain_legacy.go create mode 100644 cmp/internal/flags/toolchain_recent.go delete mode 100644 cmp/internal/value/format.go delete mode 100644 cmp/internal/value/format_test.go create mode 100644 cmp/internal/value/zero.go create mode 100644 cmp/internal/value/zero_test.go create mode 100644 cmp/report_compare.go create mode 100644 cmp/report_reflect.go create mode 100644 cmp/report_text.go create mode 100644 cmp/report_value.go diff --git a/cmp/compare.go b/cmp/compare.go index 939a3b5..2762733 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -32,6 +32,7 @@ import ( "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" ) @@ -109,9 +110,15 @@ func Equal(x, y interface{}, opts ...Option) bool { // Diff returns a human-readable report of the differences between two values. // It returns an empty string if and only if Equal returns true for the same -// input values and options. The output string will use the "-" symbol to -// indicate elements removed from x, and the "+" symbol to indicate elements -// added to y. +// 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. func Diff(x, y interface{}, opts ...Option) string { @@ -373,10 +380,10 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { // Otherwise, it returns the input value as is. func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143). - // The upstream fix landed in Go1.10, so we can remove this when drop support - // for Go1.9 and below. - if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { - return reflect.New(t).Elem() + if !flags.AtLeastGo110 { + if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t { + return reflect.New(t).Elem() + } } return v } diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 8c2cf34..33a4791 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -14,7 +14,6 @@ import ( "math/rand" "reflect" "regexp" - "runtime" "sort" "strings" "sync" @@ -23,11 +22,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-cmp/cmp/internal/flags" + pb "github.com/google/go-cmp/cmp/internal/testprotos" ts "github.com/google/go-cmp/cmp/internal/teststructs" ) -var now = time.Now() +func init() { + flags.Deterministic = true +} + +var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC) func intPtr(n int) *int { return &n } @@ -73,7 +78,8 @@ func TestDiff(t *testing.T) { if gotPanic != "" { t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) } - if got, want := strings.TrimSpace(gotDiff), strings.TrimSpace(tt.wantDiff); got != want { + tt.wantDiff = strings.TrimPrefix(tt.wantDiff, "\n") + if gotDiff != tt.wantDiff { t.Fatalf("difference message:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, tt.wantDiff, tt.reason) } } else { @@ -181,10 +187,17 @@ func comparerTests() []test { x: struct{ A, B, C int }{1, 2, 3}, y: struct{ A, B, C int }{1, 2, 3}, }, { - label: label, - x: struct{ A, B, C int }{1, 2, 3}, - y: struct{ A, B, C int }{1, 2, 4}, - wantDiff: "root.C:\n\t-: 3\n\t+: 4\n", + label: label, + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 4}, + wantDiff: ` + struct{ A int; B int; C int }{ + A: 1, + B: 2, +- C: 3, ++ C: 4, + } +`, }, { label: label, x: struct{ a, b, c int }{1, 2, 3}, @@ -195,10 +208,15 @@ func comparerTests() []test { x: &struct{ A *int }{intPtr(4)}, y: &struct{ A *int }{intPtr(4)}, }, { - label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, - wantDiff: "*root.A:\n\t-: 4\n\t+: 5\n", + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + wantDiff: ` + &struct{ A *int }{ +- A: &4, ++ A: &5, + } +`, }, { label: label, x: &struct{ A *int }{intPtr(4)}, @@ -218,10 +236,15 @@ func comparerTests() []test { x: &struct{ R *bytes.Buffer }{}, y: &struct{ R *bytes.Buffer }{}, }, { - label: label, - x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, - y: &struct{ R *bytes.Buffer }{}, - wantDiff: "root.R:\n\t-: s\"\"\n\t+: \n", + label: label, + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + wantDiff: ` + &struct{ R *bytes.Buffer }{ +- R: s"", ++ R: nil, + } +`, }, { label: label, x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, @@ -276,9 +299,12 @@ func comparerTests() []test { return x.String() == y.String() })}, wantDiff: ` -{[]*regexp.Regexp}[1]: - -: s"a*b*c*" - +: s"a*b*d*"`, + []*regexp.Regexp{ + nil, +- s"a*b*c*", ++ s"a*b*d*", + } +`, }, { label: label, x: func() ***int { @@ -308,9 +334,11 @@ func comparerTests() []test { return &c }(), wantDiff: ` -***{***int}: - -: 0 - +: 1`, + &&&int( +- 0, ++ 1, + ) +`, }, { label: label, x: []int{1, 2, 3, 4, 5}[:3], @@ -326,93 +354,117 @@ func comparerTests() []test { y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, wantDiff: ` -root: - -: s"hello" - +: s"hello2"`, + struct{ fmt.Stringer }( +- s"hello", ++ s"hello2", + ) +`, }, { label: label, x: md5.Sum([]byte{'a'}), y: md5.Sum([]byte{'b'}), wantDiff: ` -{[16]uint8}[0]: - -: 0x0c - +: 0x92 -{[16]uint8}[1]: - -: 0xc1 - +: 0xeb -{[16]uint8}[2]: - -: 0x75 - +: 0x5f -{[16]uint8}[3]: - -: 0xb9 - +: 0xfe -{[16]uint8}[4]: - -: 0xc0 - +: 0xe6 -{[16]uint8}[5]: - -: 0xf1 - +: 0xae -{[16]uint8}[6]: - -: 0xb6 - +: 0x2f -{[16]uint8}[7]: - -: 0xa8 - +: 0xec -{[16]uint8}[8]: - -: 0x31 - +: 0x3a -{[16]uint8}[9]: - -: 0xc3 - +: 0xd7 -{[16]uint8}[10]: - -: 0x99 - +: 0x1c -{[16]uint8}[11->?]: - -: 0xe2 - +: -{[16]uint8}[12->?]: - -: 0x69 - +: -{[16]uint8}[?->12]: - -: - +: 0x75 -{[16]uint8}[?->13]: - -: - +: 0x31 -{[16]uint8}[14]: - -: 0x26 - +: 0x57 -{[16]uint8}[15]: - -: 0x61 - +: 0x8f`, + [16]uint8{ +- 0x0c, ++ 0x92, +- 0xc1, ++ 0xeb, +- 0x75, ++ 0x5f, +- 0xb9, ++ 0xfe, +- 0xc0, ++ 0xe6, +- 0xf1, ++ 0xae, +- 0xb6, ++ 0x2f, +- 0xa8, ++ 0xec, +- 0x31, ++ 0x3a, +- 0xc3, ++ 0xd7, +- 0x99, ++ 0x1c, +- 0xe2, +- 0x69, + 0x77, ++ 0x75, ++ 0x31, +- 0x26, ++ 0x57, +- 0x61, ++ 0x8f, + } +`, }, { label: label, x: new(fmt.Stringer), y: nil, wantDiff: ` -root: - -: & - +: `, + interface{}( +- &fmt.Stringer(nil), + ) +`, }, { label: label, x: makeTarHeaders('0'), y: makeTarHeaders('\x00'), wantDiff: ` -{[]cmp_test.tarHeader}[0].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[1].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[2].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[3].Typeflag: - -: 0x30 - +: 0x00 -{[]cmp_test.tarHeader}[4].Typeflag: - -: 0x30 - +: 0x00`, + []cmp_test.tarHeader{ + { + ... // 4 identical fields + Size: 1, + ModTime: s"2009-11-10 23:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 2, + ModTime: s"2009-11-11 00:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 4, + ModTime: s"2009-11-11 01:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 8, + ModTime: s"2009-11-11 02:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 16, + ModTime: s"2009-11-11 03:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + } +`, }, { label: label, x: make([]int, 1000), @@ -465,50 +517,49 @@ root: }), }, wantDiff: ` -λ({[]int}[0]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[1]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[2]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[3]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[4]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[5]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[6]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[7]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[8]): - -: float64(NaN) - +: float64(NaN) -λ({[]int}[9]): - -: float64(NaN) - +: float64(NaN)`, + []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)), + } +`, }, { // Ensure reasonable Stringer formatting of map keys. label: label, x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, y: map[*pb.Stringer]*pb.Stringer(nil), wantDiff: ` -{map[*testprotos.Stringer]*testprotos.Stringer}: - -: map[*testprotos.Stringer]*testprotos.Stringer{s"hello": s"world"} - +: map[*testprotos.Stringer]*testprotos.Stringer(nil)`, + map[*testprotos.Stringer]*testprotos.Stringer( +- {⟪0xdeadf00f⟫: s"world"}, ++ nil, + ) +`, }, { // Ensure Stringer avoids double-quote escaping if possible. - label: label, - x: []*pb.Stringer{{`multi\nline\nline\nline`}}, - wantDiff: "root:\n\t-: []*testprotos.Stringer{s`multi\\nline\\nline\\nline`}\n\t+: ", + label: label, + x: []*pb.Stringer{{`multi\nline\nline\nline`}}, + wantDiff: strings.Replace(` + interface{}( +- []*testprotos.Stringer{s'multi\nline\nline\nline'}, + ) +`, "'", "`", -1), }, { label: label, x: struct{ I Iface2 }{}, @@ -541,16 +592,49 @@ root: x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}}, y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}}, wantDiff: ` -root[0]["hr"]: - -: int(65) - +: float64(65) -root[1]["hr"]: - -: int(63) - +: float64(63)`, + []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"), + }, + } +`, }, { label: label, - x: struct{ _ string }{}, - y: struct{ _ string }{}, + x: map[*int]string{ + new(int): "hello", + }, + y: map[*int]string{ + new(int): "world", + }, + wantDiff: ` + map[*int]string{ +- ⟪0xdeadf00f⟫: "hello", ++ ⟪0xdeadf00f⟫: "world", + } +`, + }, { + label: label, + x: intPtr(0), + y: intPtr(0), + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + // TODO: This output is unhelpful and should show the address. + wantDiff: ` + (*int)( +- &0, ++ &0, + ) +`, }, { label: label, x: [2][]int{ @@ -574,9 +658,16 @@ root[1]["hr"]: }, cmp.Ignore()), }, wantDiff: ` -{[2][]int}[1][5->3]: - -: 20 - +: 2`, + [2][]int{ + {..., 1, 2, 3, ..., 4, 5, 6, 7, ..., 8, ..., 9, ...}, + { + ... // 6 ignored and 1 identical elements +- 20, ++ 2, + ... // 3 ignored elements + }, + } +`, reason: "all zero slice elements are ignored (even if missing)", }, { label: label, @@ -601,9 +692,15 @@ root[1]["hr"]: }, cmp.Ignore()), }, wantDiff: ` -{[2]map[string]int}[1]["keep2"]: - -: - +: 2`, + [2]map[string]int{ + {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, + { + ... // 2 ignored entries + "keep1": 1, ++ "keep2": 2, + }, + } +`, reason: "all zero map entries are ignored (even if missing)", }} } @@ -638,9 +735,11 @@ func transformerTests() []test { cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), }, wantDiff: ` -λ(λ(λ({uint8}))): - -: 0x00 - +: 0x01`, + uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( +- 0x00, ++ 0x01, + ))))))) +`, }, { label: label, x: 0, @@ -665,12 +764,15 @@ func transformerTests() []test { ), }, wantDiff: ` -λ({[]int}[1]): - -: -5 - +: 3 -λ({[]int}[3]): - -: -1 - +: -5`, + []int{ + Inverse(λ, int64(0)), +- Inverse(λ, int64(-5)), ++ Inverse(λ, int64(3)), + Inverse(λ, int64(0)), +- Inverse(λ, int64(-1)), ++ Inverse(λ, int64(-5)), + } +`, }, { label: label, x: 0, @@ -678,15 +780,17 @@ func transformerTests() []test { opts: []cmp.Option{ cmp.Transformer("λ", func(in int) interface{} { if in == 0 { - return "string" + return "zero" } return float64(in) }), }, wantDiff: ` -λ({int}): - -: "string" - +: 1`, + int(Inverse(λ, interface{}( +- string("zero"), ++ float64(1), + ))) +`, }, { label: label, x: `{ @@ -726,18 +830,32 @@ func transformerTests() []test { }), }, wantDiff: ` -ParseJSON({string})["address"]["city"]: - -: "Los Angeles" - +: "New York" -ParseJSON({string})["address"]["state"]: - -: "CA" - +: "NY" -ParseJSON({string})["phoneNumbers"][0]["number"]: - -: "212 555-4321" - +: "212 555-1234" -ParseJSON({string})["spouse"]: - -: - +: interface {}(nil)`, + 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, + })) +`, }, { label: label, x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")}, @@ -747,12 +865,28 @@ ParseJSON({string})["spouse"]: transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }), }, wantDiff: ` -SplitString({cmp_test.StringBytes}.String)[2]: - -: "Line" - +: "line" -SplitBytes({cmp_test.StringBytes}.Bytes)[3][0]: - -: 0x62 - +: 0x42`, + 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 + }, + })), + } +`, }, { x: "a\nb\nc\n", y: "a\nb\nc\n", @@ -866,10 +1000,8 @@ func embeddedTests() []test { } // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122). - // The upstream fix landed in Go1.10, so we can remove this when dropping - // support for Go1.9 and below. wantPanicNotGo110 := func(s string) string { - if v := runtime.Version(); strings.HasPrefix(v, "go1.8") || strings.HasPrefix(v, "go1.9") { + if !flags.AtLeastGo110 { return "" } return s @@ -910,12 +1042,15 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, wantDiff: ` -{teststructs.ParentStructA}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructA}.privateStruct.private: - -: 2 - +: 3`, + teststructs.ParentStructA{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +`, }, { label: label + "ParentStructB", x: ts.ParentStructB{}, @@ -955,12 +1090,15 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, wantDiff: ` -{teststructs.ParentStructB}.PublicStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructB}.PublicStruct.private: - -: 2 - +: 3`, + teststructs.ParentStructB{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +`, }, { label: label + "ParentStructC", x: ts.ParentStructC{}, @@ -996,18 +1134,19 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, wantDiff: ` -{teststructs.ParentStructC}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructC}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructC}.Public: - -: 3 - +: 4 -{teststructs.ParentStructC}.private: - -: 4 - +: 5`, + teststructs.ParentStructC{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +`, }, { label: label + "ParentStructD", x: ts.ParentStructD{}, @@ -1047,18 +1186,19 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, wantDiff: ` -{teststructs.ParentStructD}.PublicStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructD}.PublicStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructD}.Public: - -: 3 - +: 4 -{teststructs.ParentStructD}.private: - -: 4 - +: 5`, + teststructs.ParentStructD{ + PublicStruct: teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + } +`, }, { label: label + "ParentStructE", x: ts.ParentStructE{}, @@ -1106,18 +1246,21 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, wantDiff: ` -{teststructs.ParentStructE}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructE}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructE}.PublicStruct.Public: - -: 3 - +: 4 -{teststructs.ParentStructE}.PublicStruct.private: - -: 4 - +: 5`, + teststructs.ParentStructE{ + privateStruct: teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +`, }, { label: label + "ParentStructF", x: ts.ParentStructF{}, @@ -1165,24 +1308,25 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, wantDiff: ` -{teststructs.ParentStructF}.privateStruct.Public: - -: 1 - +: 2 -{teststructs.ParentStructF}.privateStruct.private: - -: 2 - +: 3 -{teststructs.ParentStructF}.PublicStruct.Public: - -: 3 - +: 4 -{teststructs.ParentStructF}.PublicStruct.private: - -: 4 - +: 5 -{teststructs.ParentStructF}.Public: - -: 5 - +: 6 -{teststructs.ParentStructF}.private: - -: 6 - +: 7`, + 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, + } +`, }, { label: label + "ParentStructG", x: ts.ParentStructG{}, @@ -1218,12 +1362,15 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, wantDiff: ` -{*teststructs.ParentStructG}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructG}.privateStruct.private: - -: 2 - +: 3`, + &teststructs.ParentStructG{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +`, }, { label: label + "ParentStructH", x: ts.ParentStructH{}, @@ -1263,12 +1410,15 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, wantDiff: ` -{*teststructs.ParentStructH}.PublicStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructH}.PublicStruct.private: - -: 2 - +: 3`, + &teststructs.ParentStructH{ + PublicStruct: &teststructs.PublicStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + } +`, }, { label: label + "ParentStructI", x: ts.ParentStructI{}, @@ -1319,18 +1469,21 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, wantDiff: ` -{*teststructs.ParentStructI}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructI}.privateStruct.private: - -: 2 - +: 3 -{*teststructs.ParentStructI}.PublicStruct.Public: - -: 3 - +: 4 -{*teststructs.ParentStructI}.PublicStruct.private: - -: 4 - +: 5`, + &teststructs.ParentStructI{ + privateStruct: &teststructs.privateStruct{ +- Public: 1, ++ Public: 2, +- private: 2, ++ private: 3, + }, + PublicStruct: &teststructs.PublicStruct{ +- Public: 3, ++ Public: 4, +- private: 4, ++ private: 5, + }, + } +`, }, { label: label + "ParentStructJ", x: ts.ParentStructJ{}, @@ -1374,30 +1527,33 @@ func embeddedTests() []test { cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, wantDiff: ` -{*teststructs.ParentStructJ}.privateStruct.Public: - -: 1 - +: 2 -{*teststructs.ParentStructJ}.privateStruct.private: - -: 2 - +: 3 -{*teststructs.ParentStructJ}.PublicStruct.Public: - -: 3 - +: 4 -{*teststructs.ParentStructJ}.PublicStruct.private: - -: 4 - +: 5 -{*teststructs.ParentStructJ}.Public.Public: - -: 7 - +: 8 -{*teststructs.ParentStructJ}.Public.private: - -: 8 - +: 9 -{*teststructs.ParentStructJ}.private.Public: - -: 5 - +: 6 -{*teststructs.ParentStructJ}.private.private: - -: 6 - +: 7`, + &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, + }, + } +`, }} } @@ -1444,9 +1600,11 @@ func methodTests() []test { x: ts.StructB{X: "NotEqual"}, y: ts.StructB{X: "not_equal"}, wantDiff: ` -{teststructs.StructB}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructB{ +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructB", x: ts.StructB{X: "NotEqual"}, @@ -1469,9 +1627,11 @@ func methodTests() []test { x: ts.StructD{X: "NotEqual"}, y: ts.StructD{X: "not_equal"}, wantDiff: ` -{teststructs.StructD}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructD{ +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructD", x: ts.StructD{X: "NotEqual"}, @@ -1486,9 +1646,11 @@ func methodTests() []test { x: ts.StructE{X: "NotEqual"}, y: ts.StructE{X: "not_equal"}, wantDiff: ` -{teststructs.StructE}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructE{ +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructE", x: ts.StructE{X: "NotEqual"}, @@ -1503,9 +1665,11 @@ func methodTests() []test { x: ts.StructF{X: "NotEqual"}, y: ts.StructF{X: "not_equal"}, wantDiff: ` -{teststructs.StructF}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructF{ +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructF", x: &ts.StructF{X: "NotEqual"}, @@ -1515,41 +1679,65 @@ func methodTests() []test { x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructA1", - x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructA1", + x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + teststructs.StructA1{ + StructA: teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructA1", x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructA1", - x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructA1", + x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + &teststructs.StructA1{ + StructA: teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructB1", x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, opts: []cmp.Option{derefTransform}, }, { - label: label + "StructB1", - x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: "{teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructB1", + x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantDiff: ` + teststructs.StructB1{ + StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructB1", x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, opts: []cmp.Option{derefTransform}, }, { - label: label + "StructB1", - x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: "{*teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructB1", + x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantDiff: ` + &teststructs.StructB1{ + StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructC1", x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, @@ -1563,12 +1751,13 @@ func methodTests() []test { x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, wantDiff: ` -{teststructs.StructD1}.StructD.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructD1}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructD1{ +- StructD: teststructs.StructD{X: "NotEqual"}, ++ StructD: teststructs.StructD{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructD1", x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, @@ -1583,12 +1772,13 @@ func methodTests() []test { x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, wantDiff: ` -{teststructs.StructE1}.StructE.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructE1}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructE1{ +- StructE: teststructs.StructE{X: "NotEqual"}, ++ StructE: teststructs.StructE{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructE1", x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, @@ -1603,12 +1793,13 @@ func methodTests() []test { x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, wantDiff: ` -{teststructs.StructF1}.StructF.X: - -: "NotEqual" - +: "not_equal" -{teststructs.StructF1}.X: - -: "NotEqual" - +: "not_equal"`, + teststructs.StructF1{ +- StructF: teststructs.StructF{X: "NotEqual"}, ++ StructF: teststructs.StructF{X: "not_equal"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructF1", x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, @@ -1618,37 +1809,61 @@ func methodTests() []test { x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructA2", - x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructA2", + x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + teststructs.StructA2{ + StructA: &teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructA2", x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructA2", - x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructA2", + x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + &teststructs.StructA2{ + StructA: &teststructs.StructA{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructB2", x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructB2", - x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructB2", + x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + teststructs.StructB2{ + StructB: &teststructs.StructB{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructB2", x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, }, { - label: label + "StructB2", - x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: "{*teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructB2", + x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, + y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, + wantDiff: ` + &teststructs.StructB2{ + StructB: &teststructs.StructB{X: "NotEqual"}, +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "StructC2", x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, @@ -1682,10 +1897,15 @@ func methodTests() []test { x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, }, { - label: label + "StructNo", - x: ts.StructNo{X: "NotEqual"}, - y: ts.StructNo{X: "not_equal"}, - wantDiff: "{teststructs.StructNo}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + label: label + "StructNo", + x: ts.StructNo{X: "NotEqual"}, + y: ts.StructNo{X: "not_equal"}, + wantDiff: ` + teststructs.StructNo{ +- X: "NotEqual", ++ X: "not_equal", + } +`, }, { label: label + "AssignA", x: ts.AssignA(func() int { return 0 }), @@ -1790,8 +2010,32 @@ func project1Tests() []test { y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata2"}}, }}}, - opts: []cmp.Option{cmp.Comparer(pb.Equal)}, - wantDiff: "{teststructs.Eagle}.Slaps[4].Args:\n\t-: s\"metadata\"\n\t+: s\"metadata2\"\n", + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantDiff: ` + 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 + } +`, }, { label: label, x: createEagle(), @@ -1814,21 +2058,78 @@ func project1Tests() []test { }(), opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, wantDiff: ` -{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.ID: - -: "southbay2" - +: "southbay" -*{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.State: - -: testprotos.Goat_States(6) - +: testprotos.Goat_States(5) -{teststructs.Eagle}.Slaps[0].Immutable.MildSlap: - -: false - +: true -{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[1->?]: - -: "bar" - +: -{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[2->?]: - -: "baz" - +: `, + 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: 0x00, + 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 + } +`, }} } @@ -1908,12 +2209,21 @@ func project2Tests() []test { }(), opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, wantDiff: ` -{teststructs.GermBatch}.DirtyGerms[18][0->?]: - -: s"germ2" - +: -{teststructs.GermBatch}.DirtyGerms[18][?->2]: - -: - +: s"germ2"`, + 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 + } +`, }, { label: label, x: createBatch(), @@ -1940,18 +2250,32 @@ func project2Tests() []test { }(), opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, wantDiff: ` -{teststructs.GermBatch}.DirtyGerms[17]: - -: - +: []*testprotos.Germ{s"germ1"} -Sort({teststructs.GermBatch}.DirtyGerms[18])[2->?]: - -: s"germ4" - +: -{teststructs.GermBatch}.DishMap[1]: - -: (*teststructs.Dish)(nil) - +: &teststructs.Dish{err: &errors.errorString{s: "unexpected EOF"}} -{teststructs.GermBatch}.GermStrain: - -: 421 - +: 22`, + 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: &errors.errorString{s: "EOF"}}, +- 1: nil, ++ 1: &{err: &errors.errorString{s: "unexpected EOF"}}, + 2: &{pb: &testprotos.Dish{Stringer: testprotos.Stringer{X: "dish"}}}, + }, + HasPreviousResult: true, + DirtyID: 10, + CleanID: 0, +- GermStrain: 421, ++ GermStrain: 22, + TotalDirtyGerms: 0, + InfectedAt: s"2009-11-10 23:00:00 +0000 UTC", + } +`, }} } @@ -2022,21 +2346,24 @@ func project3Tests() []test { }(), opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, wantDiff: ` -{teststructs.Dirt}.table: - -: &teststructs.MockTable{state: []string{"a", "c"}} - +: &teststructs.MockTable{state: []string{"a", "b", "c"}} -{teststructs.Dirt}.Discord: - -: teststructs.DiscordState(554) - +: teststructs.DiscordState(500) -λ({teststructs.Dirt}.Proto): - -: s"blah" - +: s"proto" -{teststructs.Dirt}.wizard["albus"]: - -: s"dumbledore" - +: -{teststructs.Dirt}.wizard["harry"]: - -: s"potter" - +: s"otter"`, + 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 + } +`, }} } @@ -2115,21 +2442,47 @@ func project4Tests() []test { }(), opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, wantDiff: ` -{teststructs.Cartel}.Headquarter.subDivisions[0->?]: - -: "alpha" - +: -{teststructs.Cartel}.Headquarter.publicMessage[2]: - -: 0x03 - +: 0x04 -{teststructs.Cartel}.Headquarter.publicMessage[3]: - -: 0x04 - +: 0x03 -{teststructs.Cartel}.poisons[0].poisonType: - -: testprotos.PoisonType(1) - +: testprotos.PoisonType(5) -{teststructs.Cartel}.poisons[1->?]: - -: &teststructs.Poison{poisonType: testprotos.PoisonType(2), manufacturer: "acme2"} - +: `, + teststructs.Cartel{ + Headquarter: teststructs.Headquarter{ + id: 0x05, + 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"}, + }, + } +`, }} } diff --git a/cmp/example_test.go b/cmp/example_test.go index 5507e0b..5954780 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -7,9 +7,11 @@ package cmp_test import ( "fmt" "math" + "net" "reflect" "sort" "strings" + "time" "github.com/google/go-cmp/cmp" ) @@ -18,108 +20,41 @@ import ( // fundamental options and filters and not in terms of what cool things you can // do with them since that overlaps with cmp/cmpopts. -// Use Diff for printing out human-readable errors for test cases comparing -// nested or structured data. +// Use Diff to print out a human-readable report of differences for tests +// comparing nested or structured data. func ExampleDiff_testing() { - // Code under test: - type ShipManifest struct { - Name string - Crew map[string]string - Androids int - Stolen bool - } - - // AddCrew tries to add the given crewmember to the manifest. - AddCrew := func(m *ShipManifest, name, title string) { - if m.Crew == nil { - m.Crew = make(map[string]string) - } - m.Crew[title] = name - } + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() - // Test function: - tests := []struct { - desc string - before *ShipManifest - name, title string - after *ShipManifest - }{ - { - desc: "add to empty", - before: &ShipManifest{}, - name: "Zaphod Beeblebrox", - title: "Galactic President", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - }, - { - desc: "add another", - before: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - name: "Trillian", - title: "Human", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - "Trillian": "Human", - }, - }, - }, - { - desc: "overwrite", - before: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Galactic President", - }, - }, - name: "Zaphod Beeblebrox", - title: "Just this guy, you know?", - after: &ShipManifest{ - Crew: map[string]string{ - "Zaphod Beeblebrox": "Just this guy, you know?", - }, - }, - }, - } - - var t fakeT - for _, test := range tests { - AddCrew(test.before, test.name, test.title) - if diff := cmp.Diff(test.before, test.after); diff != "" { - t.Errorf("%s: after AddCrew, manifest differs: (-want +got)\n%s", test.desc, diff) - } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) } // Output: - // add to empty: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Galactic President"]: - // -: "Zaphod Beeblebrox" - // +: - // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: - // -: - // +: "Galactic President" - // - // add another: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Human"]: - // -: "Trillian" - // +: - // {*cmp_test.ShipManifest}.Crew["Trillian"]: - // -: - // +: "Human" - // - // overwrite: after AddCrew, manifest differs: (-want +got) - // {*cmp_test.ShipManifest}.Crew["Just this guy, you know?"]: - // -: "Zaphod Beeblebrox" - // +: - // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: - // -: "Galactic President" - // +: "Just this guy, you know?" + // MakeGatewayInfo() mismatch (-want +got): + // cmp_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: 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 @@ -364,6 +299,78 @@ func ExampleOption_transformComplex() { // false } +type ( + Gateway struct { + SSID string + IPAddress net.IP + NetMask net.IPMask + Clients []Client + } + Client struct { + Hostname string + IPAddress net.IP + LastSeen time.Time + } +) + +func MakeGatewayInfo() (x, y Gateway) { + x = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 1), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 219), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }, { + Hostname: "americano", + IPAddress: net.IPv4(192, 168, 0, 188), + LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC), + }}, + } + y = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 2), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 221), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }}, + } + return x, y +} + +var t fakeT + type fakeT struct{} func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } diff --git a/cmp/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/value/format.go b/cmp/internal/value/format.go deleted file mode 100644 index bafb2d1..0000000 --- a/cmp/internal/value/format.go +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -// Package value provides functionality for reflect.Value types. -package value - -import ( - "fmt" - "reflect" - "strconv" - "strings" - "unicode" -) - -var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() - -// Format formats the value v as a string. -// -// This is similar to fmt.Sprintf("%+v", v) except this: -// * Prints the type unless it can be elided -// * Avoids printing struct fields that are zero -// * Prints a nil-slice as being nil, not empty -// * Prints map entries in deterministic order -func Format(v reflect.Value, conf FormatConfig) string { - conf.printType = true - conf.followPointers = true - conf.realPointers = true - return formatAny(v, conf, visited{}) -} - -type FormatConfig struct { - UseStringer bool // Should the String method be used if available? - printType bool // Should we print the type before the value? - PrintPrimitiveType bool // Should we print the type of primitives? - followPointers bool // Should we recursively follow pointers? - realPointers bool // Should we print the real address of pointers? -} - -func formatAny(v reflect.Value, conf FormatConfig, m visited) string { - // TODO: Should this be a multi-line printout in certain situations? - - if !v.IsValid() { - return "" - } - if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() { - if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { - return "" - } - - const stringerPrefix = "s" // Indicates that the String method was used - s := v.Interface().(fmt.Stringer).String() - return stringerPrefix + formatString(s) - } - - switch v.Kind() { - case reflect.Bool: - return formatPrimitive(v.Type(), v.Bool(), conf) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return formatPrimitive(v.Type(), v.Int(), conf) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { - // Unnamed uints are usually bytes or words, so use hexadecimal. - return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) - } - return formatPrimitive(v.Type(), v.Uint(), conf) - case reflect.Float32, reflect.Float64: - return formatPrimitive(v.Type(), v.Float(), conf) - case reflect.Complex64, reflect.Complex128: - return formatPrimitive(v.Type(), v.Complex(), conf) - case reflect.String: - return formatPrimitive(v.Type(), formatString(v.String()), conf) - case reflect.UnsafePointer, reflect.Chan, reflect.Func: - return formatPointer(v, conf) - case reflect.Ptr: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("(%v)(nil)", v.Type()) - } - return "" - } - if m.Visit(v) || !conf.followPointers { - return formatPointer(v, conf) - } - return "&" + formatAny(v.Elem(), conf, m) - case reflect.Interface: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - return formatAny(v.Elem(), conf, m) - case reflect.Slice: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - fallthrough - case reflect.Array: - var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface - for i := 0; i < v.Len(); i++ { - vi := v.Index(i) - if vi.CanAddr() { // Check for recursive elements - p := vi.Addr() - if m.Visit(p) { - subConf := conf - subConf.printType = true - ss = append(ss, "*"+formatPointer(p, subConf)) - continue - } - } - ss = append(ss, formatAny(vi, subConf, m)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Map: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "" - } - if m.Visit(v) { - return formatPointer(v, conf) - } - - var ss []string - keyConf, valConf := conf, conf - keyConf.printType = v.Type().Key().Kind() == reflect.Interface - keyConf.followPointers = false - valConf.printType = v.Type().Elem().Kind() == reflect.Interface - for _, k := range SortKeys(v.MapKeys()) { - sk := formatAny(k, keyConf, m) - sv := formatAny(v.MapIndex(k), valConf, m) - ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - case reflect.Struct: - var ss []string - subConf := conf - subConf.printType = true - for i := 0; i < v.NumField(); i++ { - vv := v.Field(i) - if isZero(vv) { - continue // Elide zero value fields - } - name := v.Type().Field(i).Name - subConf.UseStringer = conf.UseStringer - s := formatAny(vv, subConf, m) - ss = append(ss, fmt.Sprintf("%s: %s", name, s)) - } - s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) - if conf.printType { - return v.Type().String() + s - } - return s - default: - panic(fmt.Sprintf("%v kind not handled", v.Kind())) - } -} - -func formatString(s string) string { - // Use quoted string if it the same length as a raw string literal. - // Otherwise, attempt to use the raw string form. - qs := strconv.Quote(s) - if len(qs) == 1+len(s)+1 { - return qs - } - - // Disallow newlines to ensure output is a single line. - // Only allow printable runes for readability purposes. - rawInvalid := func(r rune) bool { - return r == '`' || r == '\n' || !unicode.IsPrint(r) - } - if strings.IndexFunc(s, rawInvalid) < 0 { - return "`" + s + "`" - } - return qs -} - -func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string { - if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") { - return fmt.Sprintf("%v(%v)", t, v) - } - return fmt.Sprintf("%v", v) -} - -func formatPointer(v reflect.Value, conf FormatConfig) string { - p := v.Pointer() - if !conf.realPointers { - p = 0 // For deterministic printing purposes - } - s := formatHex(uint64(p)) - if conf.printType { - return fmt.Sprintf("(%v)(%s)", v.Type(), s) - } - return s -} - -func formatHex(u uint64) string { - var f string - switch { - case u <= 0xff: - f = "0x%02x" - case u <= 0xffff: - f = "0x%04x" - case u <= 0xffffff: - f = "0x%06x" - case u <= 0xffffffff: - f = "0x%08x" - case u <= 0xffffffffff: - f = "0x%010x" - case u <= 0xffffffffffff: - f = "0x%012x" - case u <= 0xffffffffffffff: - f = "0x%014x" - case u <= 0xffffffffffffffff: - f = "0x%016x" - } - return fmt.Sprintf(f, u) -} - -// isZero reports whether v is the zero value. -// This does not rely on Interface and so can be used on unexported fields. -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Bool: - return v.Bool() == false - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Complex64, reflect.Complex128: - return v.Complex() == 0 - case reflect.String: - return v.String() == "" - case reflect.UnsafePointer: - return v.Pointer() == 0 - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: - return v.IsNil() - case reflect.Array: - for i := 0; i < v.Len(); i++ { - if !isZero(v.Index(i)) { - return false - } - } - return true - case reflect.Struct: - for i := 0; i < v.NumField(); i++ { - if !isZero(v.Field(i)) { - return false - } - } - return true - } - return false -} - -type visited map[Pointer]bool - -func (m visited) Visit(v reflect.Value) bool { - p := PointerOf(v) - visited := m[p] - m[p] = true - return visited -} diff --git a/cmp/internal/value/format_test.go b/cmp/internal/value/format_test.go deleted file mode 100644 index d676da2..0000000 --- a/cmp/internal/value/format_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package value - -import ( - "bytes" - "io" - "reflect" - "testing" -) - -func TestFormat(t *testing.T) { - type key struct { - a int - b string - c chan bool - } - - tests := []struct { - in interface{} - want string - }{{ - in: []int{}, - want: "[]int{}", - }, { - in: []int(nil), - want: "[]int(nil)", - }, { - in: []int{1, 2, 3, 4, 5}, - want: "[]int{1, 2, 3, 4, 5}", - }, { - in: []interface{}{1, true, "hello", struct{ A, B int }{1, 2}}, - want: "[]interface {}{1, true, \"hello\", struct { A int; B int }{A: 1, B: 2}}", - }, { - in: []struct{ A, B int }{{1, 2}, {0, 4}, {}}, - want: "[]struct { A int; B int }{{A: 1, B: 2}, {B: 4}, {}}", - }, { - in: map[*int]string{new(int): "hello"}, - want: "map[*int]string{0x00: \"hello\"}", - }, { - in: map[key]string{{}: "hello"}, - want: "map[value.key]string{{}: \"hello\"}", - }, { - in: map[key]string{{a: 5, b: "key", c: make(chan bool)}: "hello"}, - want: "map[value.key]string{{a: 5, b: \"key\", c: (chan bool)(0x00)}: \"hello\"}", - }, { - in: map[io.Reader]string{new(bytes.Reader): "hello"}, - want: "map[io.Reader]string{(*bytes.Reader)(0x00): \"hello\"}", - }, { - in: func() interface{} { - var a = []interface{}{nil} - a[0] = a - return a - }(), - want: "[]interface {}{[]interface {}{*(*interface {})(0x00)}}", - }, { - in: func() interface{} { - type A *A - var a A - a = &a - return a - }(), - want: "&(value.A)(0x00)", - }, { - in: func() interface{} { - type A map[*A]A - a := make(A) - a[&a] = a - return a - }(), - want: "value.A{0x00: 0x00}", - }, { - in: func() interface{} { - var a [2]interface{} - a[0] = &a - return a - }(), - want: "[2]interface {}{&[2]interface {}{(*[2]interface {})(0x00), interface {}(nil)}, interface {}(nil)}", - }} - - for i, tt := range tests { - // Intentionally retrieve the value through an unexported field to - // ensure the format logic does not depend on read-write access - // to the reflect.Value. - v := reflect.ValueOf(struct{ x interface{} }{tt.in}).Field(0) - got := formatAny(v, FormatConfig{UseStringer: true, printType: true, followPointers: true}, visited{}) - if got != tt.want { - t.Errorf("test %d, Format():\ngot %q\nwant %q", i, got, tt.want) - } - } -} diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go new file mode 100644 index 0000000..d13a12c --- /dev/null +++ b/cmp/internal/value/zero.go @@ -0,0 +1,45 @@ +// 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 "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 v.Float() == 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() == 0 + case reflect.String: + return v.String() == "" + case reflect.UnsafePointer: + return v.Pointer() == 0 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if !IsZero(v.Index(i)) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !IsZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/cmp/internal/value/zero_test.go b/cmp/internal/value/zero_test.go new file mode 100644 index 0000000..536d50b --- /dev/null +++ b/cmp/internal/value/zero_test.go @@ -0,0 +1,45 @@ +// 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" + "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}, + } + + 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/path.go b/cmp/path.go index ad6c0f5..ea9575c 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -198,15 +198,6 @@ func (pa Path) GoString() string { ssPre = append(ssPre, s.trans.name+"(") ssPost = append(ssPost, ")") continue - case *typeAssertion: - // As a special-case, elide type assertions on anonymous types - // since they are typically generated dynamically and can be very - // verbose. For example, some transforms return interface{} because - // of Go's lack of generics, but typically take in and return the - // exact same concrete type. - if s.Type().PkgPath() == "" { - continue - } } ssPost = append(ssPost, s.String()) } diff --git a/cmp/report.go b/cmp/report.go index 60f3f37..6810a50 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -4,59 +4,48 @@ package cmp -import ( - "fmt" - "reflect" - "strings" - - "github.com/google/go-cmp/cmp/internal/value" -) - +// 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 { - curPath Path - - diffs []string // List of differences, possibly truncated - ndiffs int // Total number of differences - nbytes int // Number of bytes in diffs - nlines int // Number of lines in diffs + root *valueNode + curr *valueNode } func (r *defaultReporter) PushStep(ps PathStep) { - r.curPath.push(ps) + r.curr = r.curr.PushStep(ps) + if r.root == nil { + r.root = r.curr + } } func (r *defaultReporter) Report(f reportFlags) { - if f&reportUnequal > 0 { - vx, vy := r.curPath.Last().Values() - r.report(vx, vy, r.curPath) - } + r.curr.Report(f) } func (r *defaultReporter) PopStep() { - r.curPath.pop() + r.curr = r.curr.PopStep() } -func (r *defaultReporter) report(x, y reflect.Value, p Path) { - const maxBytes = 4096 - const maxLines = 256 - r.ndiffs++ - if r.nbytes < maxBytes && r.nlines < maxLines { - sx := value.Format(x, value.FormatConfig{UseStringer: true}) - sy := value.Format(y, value.FormatConfig{UseStringer: true}) - if sx == sy { - // Unhelpful output, so use more exact formatting. - sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true}) - sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true}) - } - s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) - r.diffs = append(r.diffs, s) - r.nbytes += len(s) - r.nlines += strings.Count(s, "\n") +// 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 (r *defaultReporter) String() string { - s := strings.Join(r.diffs, "") - if r.ndiffs == len(r.diffs) { - return s +func assert(ok bool) { + if !ok { + panic("assertion failure") } - return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs)) } diff --git a/cmp/report_compare.go b/cmp/report_compare.go new file mode 100644 index 0000000..2d782fe --- /dev/null +++ b/cmp/report_compare.go @@ -0,0 +1,290 @@ +// 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 { + // TODO: Add specialized formatting for slices of primitives. + + // 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, visitedPointers{}) + outy := opts.FormatValue(v.ValueY, 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, visitedPointers{}) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, 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, visitedPointers{}) + case diffInserted: + return opts.FormatValue(v.ValueY, 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.ValueX) + 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 { + // TODO: Add specialized formatting for slices of primitives. + 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..5521c60 --- /dev/null +++ b/cmp/report_reflect.go @@ -0,0 +1,279 @@ +// 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, 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.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + // Unnamed uints are usually bytes or words, so use hexadecimal. + if t.PkgPath() == "" || t.Kind() == reflect.Uintptr { + return textLine(formatHex(v.Uint())) + } + return textLine(fmt.Sprint(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 + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if value.IsZero(vv) { + continue // Elide fields with zero values + } + s := opts.WithTypeMode(autoType).FormatValue(vv, m) + list = append(list, textRecord{Key: t.Field(i).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, 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), 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(), 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(), 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.AvoidStringer = true + opts.ShallowPointers = true + s := opts.FormatValue(v, 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_text.go b/cmp/report_text.go new file mode 100644 index 0000000..80605d0 --- /dev/null +++ b/cmp/report_text.go @@ -0,0 +1,382 @@ +// 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 { + 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 = 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..fcff486 --- /dev/null +++ b/cmp/report_value.go @@ -0,0 +1,120 @@ +// 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(f reportFlags) { + assert(r.MaxDepth == 0) // May only be called on leaf nodes + + if f&reportEqual > 0 { + r.NumSame++ + } + if f&reportUnequal > 0 { + r.NumDiff++ + } + if f&reportIgnored > 0 { + r.NumIgnored++ + } + assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) + + if f&reportByMethod > 0 { + r.NumCompared++ + } + if f&reportByFunc > 0 { + r.NumCompared++ + } + assert(r.NumCompared <= 1) +} + +func (child *valueNode) PopStep() (parent *valueNode) { + parent = child.parent + if parent != nil { + 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 +} From 3177a94b4e99d833e6c0d9f864faeb482f8a7f6d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 18:07:37 -0700 Subject: [PATCH 12/99] Export the Reporter API (#123) The Reporter option allows users to hook in their own custom reporters to programatically interpret the diff structure. The Reporter API uses a push/pop mechanism, which is strictly more powerful than an API that only calls report on leaf nodes. The Reporter.Report method takes in a Result type to provide flexibility in what properties can be reported in the future since new properties can be added, but new methods cannot be easily added to the reporter interface. --- cmp/compare.go | 19 +++++----- cmp/example_reporter_test.go | 59 +++++++++++++++++++++++++++++ cmp/options.go | 73 +++++++++++++++++++++++------------- cmp/options_test.go | 8 ++-- cmp/report.go | 4 +- cmp/report_value.go | 41 ++++++++++---------- 6 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 cmp/example_reporter_test.go diff --git a/cmp/compare.go b/cmp/compare.go index 2762733..9fe9a4c 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -120,10 +120,11 @@ func Equal(x, y interface{}, opts ...Option) bool { // 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. +// 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 { r := new(defaultReporter) - opts = Options{Options(opts), reporter(r)} + opts = Options{Options(opts), Reporter(r)} eq := Equal(x, y, opts...) d := r.String() if (d == "") != eq { @@ -135,9 +136,9 @@ func Diff(x, y interface{}, opts ...Option) string { 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 - reporters []reporterOption // Optional reporters + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + reporters []reporter // Optional reporters // recChecker checks for infinite cycles applying the same set of // transformers upon the output of itself. @@ -183,7 +184,7 @@ func (s *state) processOption(opt Option) { for t := range opt { s.exporters[t] = true } - case reporterOption: + case reporter: s.reporters = append(s.reporters, opt) default: panic(fmt.Sprintf("unknown option %T", opt)) @@ -529,8 +530,8 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { } } -func (s *state) report(eq bool, rf reportFlags) { - if rf&reportIgnored == 0 { +func (s *state) report(eq bool, rf resultFlags) { + if rf&reportByIgnore == 0 { if eq { s.result.NumSame++ rf |= reportEqual @@ -540,7 +541,7 @@ func (s *state) report(eq bool, rf reportFlags) { } } for _, r := range s.reporters { - r.Report(rf) + r.Report(Result{flags: rf}) } } 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} + // +: +} diff --git a/cmp/options.go b/cmp/options.go index a265597..6e52fee 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -199,7 +199,7 @@ 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, reportIgnored) } +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 @@ -407,68 +407,87 @@ func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) appli panic("not implemented") } -// reportFlags is a bit-set representing how a comparison was determined. -type reportFlags uint +// 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 +} + +type resultFlags uint const ( - _ reportFlags = (1 << iota) / 2 + _ resultFlags = (1 << iota) / 2 - // reportEqual reports whether the node is equal. - // This may be ORed with reportByMethod or reportByFunc. reportEqual - // reportUnequal reports whether the node is not equal. - // This may be ORed with reportByMethod or reportByFunc. reportUnequal - // reportIgnored reports whether the node was ignored. - reportIgnored - - // reportByMethod reports whether equality was determined by calling the - // Equal method. This may be ORed with reportEqual or reportUnequal. + reportByIgnore reportByMethod - // reportByFunc reports whether equality was determined by calling a custom - // Comparer function. This may be ORed with reportEqual or reportUnequal. reportByFunc ) -// reporter is an Option that can be passed to Equal. When Equal traverses +// 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 { - // TODO: Export this option. - +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. + // The PathStep.Values are valid for the duration of the entire traversal + // and must not be mutated. // - // Equal always call PushStep at the start to provide an operation-less + // 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 at exactly once on leaf nodes to report whether the + // 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(reportFlags) + Report(Result) // PopStep ascends back up the value tree. // There is always a matching pop call for every push call. PopStep() }) Option { - return reporterOption{r} + return reporter{r} } -type reporterOption struct{ reporterIface } +type reporter struct{ reporterIface } type reporterIface interface { PushStep(PathStep) - Report(reportFlags) + Report(Result) PopStep() } -func (reporterOption) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { +func (reporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { panic("not implemented") } diff --git a/cmp/options_test.go b/cmp/options_test.go index f876eab..f8066c7 100644 --- a/cmp/options_test.go +++ b/cmp/options_test.go @@ -128,7 +128,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, reporter(&defaultReporter{})}, + args: []interface{}{func(Path) bool { return true }, Reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterPath", @@ -137,7 +137,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterPath", fnc: FilterPath, - args: []interface{}{func(Path) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}}, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -170,7 +170,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, reporter(&defaultReporter{})}, + args: []interface{}{func(int, int) bool { return true }, Reporter(&defaultReporter{})}, wantPanic: "invalid option type", }, { label: "FilterValues", @@ -179,7 +179,7 @@ func TestOptionPanic(t *testing.T) { }, { label: "FilterValues", fnc: FilterValues, - args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), reporter(&defaultReporter{})}}, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Reporter(&defaultReporter{})}}, wantPanic: "invalid option type", }} diff --git a/cmp/report.go b/cmp/report.go index 6810a50..6ddf299 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -26,8 +26,8 @@ func (r *defaultReporter) PushStep(ps PathStep) { r.root = r.curr } } -func (r *defaultReporter) Report(f reportFlags) { - r.curr.Report(f) +func (r *defaultReporter) Report(rs Result) { + r.curr.Report(rs) } func (r *defaultReporter) PopStep() { r.curr = r.curr.PopStep() diff --git a/cmp/report_value.go b/cmp/report_value.go index fcff486..83031a7 100644 --- a/cmp/report_value.go +++ b/cmp/report_value.go @@ -80,41 +80,42 @@ func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) { return child } -func (r *valueNode) Report(f reportFlags) { +func (r *valueNode) Report(rs Result) { assert(r.MaxDepth == 0) // May only be called on leaf nodes - if f&reportEqual > 0 { - r.NumSame++ - } - if f&reportUnequal > 0 { - r.NumDiff++ - } - if f&reportIgnored > 0 { + if rs.ByIgnore() { r.NumIgnored++ + } else { + if rs.Equal() { + r.NumSame++ + } else { + r.NumDiff++ + } } assert(r.NumSame+r.NumDiff+r.NumIgnored == 1) - if f&reportByMethod > 0 { + if rs.ByMethod() { r.NumCompared++ } - if f&reportByFunc > 0 { + if rs.ByFunc() { r.NumCompared++ } assert(r.NumCompared <= 1) } func (child *valueNode) PopStep() (parent *valueNode) { + if child.parent == nil { + return nil + } parent = child.parent - if parent != nil { - 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 - } + 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 } From 0376dcf9bae3ad0cad70a17edc2a21e0afd667e7 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 18:15:13 -0700 Subject: [PATCH 13/99] Add IgnoreSliceElements and IgnoreMapEntries helpers (#126) These helper options ignore slice elements or map entries based on a user-provided predicate function. These are especially useful for ignoring missing elements or entries. --- cmp/cmpopts/ignore.go | 58 ++++++++++++++++++++++++++ cmp/cmpopts/sort.go | 12 +++--- cmp/cmpopts/util_test.go | 76 +++++++++++++++++++++++++++++++++++ cmp/cmpopts/xform.go | 4 +- cmp/internal/function/func.go | 22 +++++++--- 5 files changed, 159 insertions(+), 13 deletions(-) diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index 6d9b514..ff8e785 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -11,6 +11,7 @@ import ( "unicode/utf8" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" ) // IgnoreFields returns an Option that ignores exported fields of the @@ -147,3 +148,60 @@ func isExported(id string) bool { r, _ := utf8.DecodeRuneInString(id) return unicode.IsUpper(r) } + +// IgnoreSliceElements returns an Option that ignores elements of []V. +// The discard function must be of the form "func(T) bool" which is used to +// ignore slice elements of type V, where V is assignable to T. +// Elements are ignored if the function reports true. +func IgnoreSliceElements(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + si, ok := p.Index(-1).(cmp.SliceIndex) + if !ok { + return false + } + if !si.Type().AssignableTo(vf.Type().In(0)) { + return false + } + vx, vy := si.Values() + if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} + +// IgnoreMapEntries returns an Option that ignores entries of map[K]V. +// The discard function must be of the form "func(T, R) bool" which is used to +// ignore map entries of type K and V, where K and V are assignable to T and R. +// Entries are ignored if the function reports true. +func IgnoreMapEntries(discardFunc interface{}) cmp.Option { + vf := reflect.ValueOf(discardFunc) + if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() { + panic(fmt.Sprintf("invalid discard function: %T", discardFunc)) + } + return cmp.FilterPath(func(p cmp.Path) bool { + mi, ok := p.Index(-1).(cmp.MapIndex) + if !ok { + return false + } + if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) { + return false + } + k := mi.Key() + vx, vy := mi.Values() + if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() { + return true + } + if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() { + return true + } + return false + }, cmp.Ignore()) +} diff --git a/cmp/cmpopts/sort.go b/cmp/cmpopts/sort.go index 8b2ef83..3a48046 100644 --- a/cmp/cmpopts/sort.go +++ b/cmp/cmpopts/sort.go @@ -26,10 +26,10 @@ import ( // !less(y, x) for two elements x and y, their relative order is maintained. // // SortSlices can be used in conjunction with EquateEmpty. -func SortSlices(less interface{}) cmp.Option { - vf := reflect.ValueOf(less) +func SortSlices(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { - panic(fmt.Sprintf("invalid less function: %T", less)) + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) } ss := sliceSorter{vf.Type().In(0), vf} return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort)) @@ -97,10 +97,10 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool { // • Total: if x != y, then either less(x, y) or less(y, x) // // SortMaps can be used in conjunction with EquateEmpty. -func SortMaps(less interface{}) cmp.Option { - vf := reflect.ValueOf(less) +func SortMaps(lessFunc interface{}) cmp.Option { + vf := reflect.ValueOf(lessFunc) if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { - panic(fmt.Sprintf("invalid less function: %T", less)) + panic(fmt.Sprintf("invalid less function: %T", lessFunc)) } ms := mapSorter{vf.Type().In(0), vf} return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort)) diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 53b9248..c85a282 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -20,7 +20,9 @@ import ( type ( MyInt int + MyInts []int MyFloat float32 + MyString string MyTime struct{ time.Time } MyStruct struct { A, B []int @@ -716,6 +718,80 @@ func TestOptions(t *testing.T) { }, wantEqual: true, reason: "equal because all Ignore options can be composed together", + }, { + label: "IgnoreSliceElements", + x: []int{1, 0, 2, 3, 0, 4, 0, 0}, + y: []int{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because zero elements are ignored", + }, { + label: "IgnoreSliceElements", + x: []MyInt{1, 0, 2, 3, 0, 4, 0, 0}, + y: []MyInt{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreSliceElements", + x: MyInts{1, 0, 2, 3, 0, 4, 0, 0}, + y: MyInts{0, 0, 0, 0, 1, 2, 3, 4}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + }, + wantEqual: true, + reason: "equal because the element type of MyInts is assignable to int", + }, { + label: "IgnoreSliceElements+EquateEmpty", + x: []MyInt{}, + y: []MyInt{0, 0, 0, 0}, + opts: []cmp.Option{ + IgnoreSliceElements(func(v int) bool { return v == 0 }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored elements does not imply empty slice", + }, { + label: "IgnoreMapEntries", + x: map[string]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: true, + reason: "equal because uppercase keys are ignored", + }, { + label: "IgnoreMapEntries", + x: map[MyString]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[MyString]int{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyString is not assignable to string", + }, { + label: "IgnoreMapEntries", + x: map[string]MyInt{"one": 1, "TWO": 2, "three": 3, "FIVE": 5}, + y: map[string]MyInt{"one": 1, "three": 3, "TEN": 10}, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + }, + wantEqual: false, + reason: "not equal because MyInt is not assignable to int", + }, { + label: "IgnoreMapEntries+EquateEmpty", + x: map[string]MyInt{"ONE": 1, "TWO": 2, "THREE": 3}, + y: nil, + opts: []cmp.Option{ + IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }), + EquateEmpty(), + }, + wantEqual: false, + reason: "not equal because ignored entries does not imply empty map", }, { label: "AcyclicTransformer", x: "a\nb\nc\nd", diff --git a/cmp/cmpopts/xform.go b/cmp/cmpopts/xform.go index be278f1..9d65155 100644 --- a/cmp/cmpopts/xform.go +++ b/cmp/cmpopts/xform.go @@ -29,7 +29,7 @@ func (xf xformFilter) filter(p cmp.Path) bool { // // Had this been an unfiltered Transformer instead, this would result in an // infinite cycle converting a string to []string to [][]string and so on. -func AcyclicTransformer(name string, f interface{}) cmp.Option { - xf := xformFilter{cmp.Transformer(name, f)} +func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option { + xf := xformFilter{cmp.Transformer(name, xformFunc)} return cmp.FilterPath(xf.filter, xf.xform) } diff --git a/cmp/internal/function/func.go b/cmp/internal/function/func.go index 1b4c4c5..ace1dbe 100644 --- a/cmp/internal/function/func.go +++ b/cmp/internal/function/func.go @@ -17,15 +17,19 @@ type funcType int const ( _ funcType = iota + tbFunc // func(T) bool ttbFunc // func(T, T) bool + trbFunc // func(T, R) bool tibFunc // func(T, I) bool trFunc // func(T) R - Equal = ttbFunc // func(T, T) bool - EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool - Transformer = trFunc // func(T) R - ValueFilter = ttbFunc // func(T, T) bool - Less = ttbFunc // func(T, T) bool + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool + ValuePredicate = tbFunc // func(T) bool + KeyValuePredicate = trbFunc // func(T, R) bool ) var boolType = reflect.TypeOf(true) @@ -37,10 +41,18 @@ func IsType(t reflect.Type, ft funcType) bool { } ni, no := t.NumIn(), t.NumOut() switch ft { + case tbFunc: // func(T) bool + if ni == 1 && no == 1 && t.Out(0) == boolType { + return true + } case ttbFunc: // func(T, T) bool if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { return true } + case trbFunc: // func(T, R) bool + if ni == 2 && no == 1 && t.Out(0) == boolType { + return true + } case tibFunc: // func(T, I) bool if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { return true From 49488b41f63c15271003a50efdc3ddd17171911c Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 18:19:40 -0700 Subject: [PATCH 14/99] Use concrete types for path steps (#129) Rather than representing each path step as an interface, use concrete types instead. This provides some performance benefits as it reduces the amount of virtual function calls and also provides the ability for the compiler to inline method calls. This is technically a breaking change, but since each of the path step interfaces were explicitly implemented in such a way that they couldn't be implemented directly (due to the presence of an unexported method), the only way someone could have been depending on these as interfaces is if they embedded the interface into another interface. Static analysis of all code at Google and publicly available on GitHub shows that this is not a problem. The performance benefits of this change is significant: benchmark old ns/op new ns/op delta BenchmarkBytes/64KiB/EqualFilter0-4 80551394 46592605 -42.16% BenchmarkBytes/64KiB/EqualFilter1-4 102922132 69974509 -32.01% BenchmarkBytes/64KiB/EqualFilter2-4 159009935 94474812 -40.59% BenchmarkBytes/64KiB/EqualFilter3-4 181231264 124601102 -31.25% BenchmarkBytes/64KiB/EqualFilter4-4 189775228 148864070 -21.56% BenchmarkBytes/64KiB/EqualFilter5-4 285065469 175198907 -38.54% benchmark old MB/s new MB/s speedup BenchmarkBytes/64KiB/EqualFilter0-4 1.63 2.81 1.72x BenchmarkBytes/64KiB/EqualFilter1-4 1.27 1.87 1.47x BenchmarkBytes/64KiB/EqualFilter2-4 0.82 1.39 1.70x BenchmarkBytes/64KiB/EqualFilter3-4 0.72 1.05 1.46x BenchmarkBytes/64KiB/EqualFilter4-4 0.69 0.88 1.28x BenchmarkBytes/64KiB/EqualFilter5-4 0.46 0.75 1.63x benchmark old allocs new allocs delta BenchmarkBytes/64KiB/EqualFilter0-4 133 134 +0.75% BenchmarkBytes/64KiB/EqualFilter1-4 134 134 +0.00% BenchmarkBytes/64KiB/EqualFilter2-4 135 135 +0.00% BenchmarkBytes/64KiB/EqualFilter3-4 135 135 +0.00% BenchmarkBytes/64KiB/EqualFilter4-4 136 136 +0.00% BenchmarkBytes/64KiB/EqualFilter5-4 136 136 +0.00% benchmark old bytes new bytes delta BenchmarkBytes/64KiB/EqualFilter0-4 6632417 6632523 +0.00% BenchmarkBytes/64KiB/EqualFilter1-4 6632416 6632464 +0.00% BenchmarkBytes/64KiB/EqualFilter2-4 6632464 6632507 +0.00% BenchmarkBytes/64KiB/EqualFilter3-4 6632502 6632483 -0.00% BenchmarkBytes/64KiB/EqualFilter4-4 6632652 6632668 +0.00% BenchmarkBytes/64KiB/EqualFilter5-4 6632604 6632659 +0.00% --- cmp/compare.go | 14 +-- cmp/options.go | 4 +- cmp/path.go | 327 +++++++++++++++++++++++-------------------------- 3 files changed, 160 insertions(+), 185 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 9fe9a4c..c81b046 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -282,7 +282,7 @@ func (s *state) compareAny(step PathStep) { return } vx, vy = vx.Elem(), vy.Elem() - s.compareAny(&indirect{pathStep{t.Elem(), vx, vy}}) + s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) return case reflect.Interface: if vx.IsNil() || vy.IsNil() { @@ -294,7 +294,7 @@ func (s *state) compareAny(step PathStep) { s.report(false, 0) return } - s.compareAny(&typeAssertion{pathStep{vx.Type(), vx, vy}}) + s.compareAny(TypeAssertion{&typeAssertion{pathStep{vx.Type(), vx, vy}}}) return default: panic(fmt.Sprintf("%v kind not handled", t.Kind())) @@ -322,7 +322,7 @@ func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { return true } -func (s *state) callTRFunc(f, v reflect.Value, step *transform) reflect.Value { +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] @@ -392,7 +392,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { var vax, vay reflect.Value // Addressable versions of vx and vy - step := &structField{} + step := StructField{&structField{}} for i := 0; i < t.NumField(); i++ { step.typ = t.Field(i).Type step.vx = vx.Field(i) @@ -423,8 +423,8 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { } func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { - step := &sliceIndex{pathStep: pathStep{typ: t.Elem()}} - withIndexes := func(ix, iy int) *sliceIndex { + step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}}} + withIndexes := func(ix, iy int) SliceIndex { if ix >= 0 { step.vx, step.xkey = vx.Index(ix), ix } else { @@ -503,7 +503,7 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { // We combine and sort the two map keys so that we can perform the // comparisons in a deterministic order. - step := &mapIndex{pathStep: pathStep{typ: t.Elem()}} + 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) diff --git a/cmp/options.go b/cmp/options.go index 6e52fee..7934481 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -288,7 +288,7 @@ 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 { + 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 @@ -301,7 +301,7 @@ func (tr *transformer) filter(s *state, t reflect.Type, _, _ reflect.Value) appl } func (tr *transformer) apply(s *state, vx, vy reflect.Value) { - step := &transform{pathStep{typ: tr.fnc.Type().Out(0)}, tr} + 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 diff --git a/cmp/path.go b/cmp/path.go index ea9575c..96fffd2 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -12,113 +12,52 @@ import ( "unicode/utf8" ) -type ( - // Path is a list of PathSteps describing the sequence of operations to get - // from some root type to the current position in the value tree. - // The first Path element is always an operation-less PathStep that exists - // simply to identify the initial type. - // - // When traversing structs with embedded structs, the embedded struct will - // always be accessed as a field before traversing the fields of the - // embedded struct themselves. That is, an exported field from the - // embedded struct will never be accessed directly from the parent struct. - Path []PathStep - - // PathStep is a union-type for specific operations to traverse - // a value's tree structure. Users of this package never need to implement - // these types as values of this type will be returned by this package. - PathStep interface { - String() string - - // Type 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 - // AllowUnexported 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) - } - - // StructField represents a struct field access on a field called Name. - StructField interface { - PathStep - - // Name is the field name. - Name() string - - // Index is the index of the field in the parent struct type. - // See reflect.Type.Field. - Index() int - - isStructField() - } - // SliceIndex is an index operation on a slice or array at some index Key. - SliceIndex interface { - PathStep - - // Key is the index key; it may return -1 if in a split state - Key() int - - // 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. - SplitKeys() (ix, iy int) - - isSliceIndex() - } - // MapIndex is an index operation on a map at some index Key. - MapIndex interface { - PathStep - - // Key is the value of the map key. - Key() reflect.Value - - isMapIndex() - } - // Indirect represents pointer indirection on the parent type. - Indirect interface { - PathStep - - isIndirect() - } - // TypeAssertion represents a type assertion on an interface. - TypeAssertion interface { - PathStep - - isTypeAssertion() - } - // Transform is a transformation from the parent type to the current type. - Transform interface { - PathStep - - // Name is the name of the Transformer. - Name() string +// 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 - // Func is the function pointer to the transformer function. - Func() reflect.Value + // Type is the resulting type after performing the path step. + Type() reflect.Type - // Option returns the originally constructed Transformer option. - // The == operator can be used to detect the exact option used. - Option() Option + // 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 + // AllowUnexported 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) +} - isTransform() - } +var ( + _ PathStep = StructField{} + _ PathStep = SliceIndex{} + _ PathStep = MapIndex{} + _ PathStep = Indirect{} + _ PathStep = TypeAssertion{} + _ PathStep = Transform{} ) func (pa *Path) push(s PathStep) { @@ -157,7 +96,7 @@ func (pa Path) Index(i int) PathStep { func (pa Path) String() string { var ss []string for _, s := range pa { - if _, ok := s.(*structField); ok { + if _, ok := s.(StructField); ok { ss = append(ss, s.String()) } } @@ -177,13 +116,13 @@ func (pa Path) GoString() string { nextStep = pa[i+1] } switch s := s.(type) { - case *indirect: + case Indirect: numIndirect++ pPre, pPost := "(", ")" switch nextStep.(type) { - case *indirect: + case Indirect: continue // Next step is indirection, so let them batch up - case *structField: + case StructField: numIndirect-- // Automatic indirection on struct fields case nil: pPre, pPost = "", "" // Last step; no need for parenthesis @@ -194,7 +133,7 @@ func (pa Path) GoString() string { } numIndirect = 0 continue - case *transform: + case Transform: ssPre = append(ssPre, s.trans.name+"(") ssPost = append(ssPost, ")") continue @@ -207,43 +146,10 @@ func (pa Path) GoString() string { return strings.Join(ssPre, "") + strings.Join(ssPost, "") } -type ( - pathStep struct { - typ reflect.Type - vx, vy reflect.Value - } - - 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 - pvx, pvy reflect.Value // Parent values - field reflect.StructField // Field information - } - sliceIndex struct { - pathStep - xkey, ykey int - } - mapIndex struct { - pathStep - key reflect.Value - } - indirect struct { - pathStep - } - typeAssertion struct { - pathStep - } - transform struct { - pathStep - trans *transformer - } -) +type pathStep struct { + typ reflect.Type + vx, vy reflect.Value +} func (ps pathStep) Type() reflect.Type { return ps.typ } func (ps pathStep) Values() (vx, vy reflect.Value) { return ps.vx, ps.vy } @@ -257,9 +163,24 @@ func (ps pathStep) String() string { } return fmt.Sprintf("{%s}", s) } -func (ps pathStep) isPathStep() {} -func (sf structField) Values() (vx, vy reflect.Value) { +// 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 + pvx, pvy reflect.Value // Parent values + 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 } @@ -272,12 +193,25 @@ func (sf structField) Values() (vx, vy reflect.Value) { } return sf.vx, sf.vy // CanInterface reports false } -func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) } -func (sf structField) Name() string { return sf.name } -func (sf structField) Index() int { return sf.idx } -func (sf structField) isStructField() {} +func (sf StructField) String() string { return fmt.Sprintf(".%s", sf.name) } + +// Name is the field name. +func (sf StructField) Name() string { return sf.name } -func (si sliceIndex) String() string { +// 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 +} + +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) @@ -292,39 +226,80 @@ func (si sliceIndex) String() string { return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) } } -func (si sliceIndex) Key() int { + +// Key is the index key; it may return -1 if in a split state +func (si SliceIndex) Key() int { if si.xkey != si.ykey { return -1 } return si.xkey } -func (si sliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } -func (si sliceIndex) isSliceIndex() {} -func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } -func (mi mapIndex) Key() reflect.Value { return mi.key } -func (mi mapIndex) isMapIndex() {} +// SplitKeys are the indexes for indexing into slices in the +// x and y values, respectively. These indexes may differ due to the +// insertion or removal of an element in one of the slices, causing +// all of the indexes to be shifted. If an index is -1, then that +// indicates that the element does not exist in the associated slice. +// +// Key is guaranteed to return -1 if and only if the indexes returned +// by SplitKeys are not the same. SplitKeys will never return -1 for +// both indexes. +func (si SliceIndex) SplitKeys() (ix, iy int) { return si.xkey, si.ykey } + +// MapIndex is an index operation on a map at some index Key. +type MapIndex struct{ *mapIndex } +type mapIndex struct { + pathStep + key reflect.Value +} -func (in indirect) String() string { return "*" } -func (in indirect) isIndirect() {} +func (mi MapIndex) Type() reflect.Type { return mi.typ } +func (mi MapIndex) Values() (vx, vy reflect.Value) { return mi.vx, mi.vy } +func (mi MapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } -func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } -func (ta typeAssertion) isTypeAssertion() {} +// Key is the value of the map key. +func (mi MapIndex) Key() reflect.Value { return mi.key } -func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } -func (tf transform) Name() string { return tf.trans.name } -func (tf transform) Func() reflect.Value { return tf.trans.fnc } -func (tf transform) Option() Option { return tf.trans } -func (tf transform) isTransform() {} +// Indirect represents pointer indirection on the parent type. +type Indirect struct{ *indirect } +type indirect struct { + pathStep +} -var ( - _ PathStep = StructField(structField{}) - _ PathStep = SliceIndex(sliceIndex{}) - _ PathStep = MapIndex(mapIndex{}) - _ PathStep = Indirect(indirect{}) - _ PathStep = TypeAssertion(typeAssertion{}) - _ PathStep = Transform(transform{}) -) +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 } // isExported reports whether the identifier is exported. func isExported(id string) bool { From b5cce8991b5672867358e36b3821ab1f778c1871 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 20:15:12 -0700 Subject: [PATCH 15/99] Implement specialized diffing for slices (#131) Lists of primitives are a common-enough data structure that it is worth providing specialized diffing for. This provides significantly better readability for strings and byte slices. There is also a heuristic for detecting what a string should be diffed as a multiline string. --- cmp/compare_test.go | 354 ++++++++++++++++++++++++++++++++++---- cmp/internal/diff/diff.go | 9 + cmp/report_compare.go | 10 +- cmp/report_slices.go | 333 +++++++++++++++++++++++++++++++++++ 4 files changed, 673 insertions(+), 33 deletions(-) create mode 100644 cmp/report_slices.go diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 33a4791..ad03f36 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -365,37 +365,8 @@ func comparerTests() []test { y: md5.Sum([]byte{'b'}), wantDiff: ` [16]uint8{ -- 0x0c, -+ 0x92, -- 0xc1, -+ 0xeb, -- 0x75, -+ 0x5f, -- 0xb9, -+ 0xfe, -- 0xc0, -+ 0xe6, -- 0xf1, -+ 0xae, -- 0xb6, -+ 0x2f, -- 0xa8, -+ 0xec, -- 0x31, -+ 0x3a, -- 0xc3, -+ 0xd7, -- 0x99, -+ 0x1c, -- 0xe2, -- 0x69, - 0x77, -+ 0x75, -+ 0x31, -- 0x26, -+ 0x57, -- 0x61, -+ 0x8f, +- 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, } `, }, { @@ -906,6 +877,327 @@ func transformerTests() []test { }} } +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}}, + wantDiff: ` + 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 + } +`, + 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}}, + wantDiff: ` + 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 + } +`, + 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}, + }, + wantDiff: ` + 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{ +- 0x03e8, 0x07d0, 0x0bb8, ++ 0x0bb8, 0x07d0, 0x03e8, + }, + 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, + }, + } +`, + 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]")}, + wantDiff: ` + 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 + } +`, + 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")}, + wantDiff: ` + 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 + } +`, + 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}`)}, + wantDiff: strings.Replace(` + 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 + } +`, "'", "`", -1), + 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"), + }, + wantDiff: ` + 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 + } +`, + reason: "batched per-line diff desired since string looks like multi-line textual data", + }} +} + func embeddedTests() []test { const label = "EmbeddedStruct/" diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 8179011..3d2e426 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -89,6 +89,15 @@ type EqualFunc func(ix int, iy int) Result // 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 } diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 2d782fe..05efb99 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -75,7 +75,11 @@ func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { // 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 { - // TODO: Add specialized formatting for slices of primitives. + // 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) + } // For leaf nodes, format the value based on the reflect.Values alone. if v.MaxDepth == 0 { @@ -240,7 +244,9 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te // Handle unequal records. for _, r := range recs[:ds.NumDiff()] { switch { - // TODO: Add specialized formatting for slices of primitives. + 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) diff --git a/cmp/report_slices.go b/cmp/report_slices.go new file mode 100644 index 0000000..8cb3265 --- /dev/null +++ b/cmp/report_slices.go @@ -0,0 +1,333 @@ +// 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 + } + + 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 { + lastLineIdx = 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.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 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 +} From 6f77996f0c42f7b84e5a2b252227263f93432e9b Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 11 Mar 2019 20:24:27 -0700 Subject: [PATCH 16/99] Improve clarity of compareAny (#132) --- cmp/compare.go | 86 ++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index c81b046..2133562 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -124,8 +124,7 @@ func Equal(x, y interface{}, opts ...Option) bool { // programmatically interpret the difference, consider using a custom Reporter. func Diff(x, y interface{}, opts ...Option) string { r := new(defaultReporter) - opts = Options{Options(opts), Reporter(r)} - eq := Equal(x, y, opts...) + eq := Equal(x, y, Options(opts), Reporter(r)) d := r.String() if (d == "") != eq { panic("inconsistent difference and equality results") @@ -156,9 +155,7 @@ type state struct { func newState(opts []Option) *state { // Always ensure a validator option exists to validate the inputs. s := &state{opts: Options{validator{}}} - for _, opt := range opts { - s.processOption(opt) - } + s.processOption(Options(opts)) return s } @@ -210,8 +207,6 @@ func (s *state) statelessCompare(step PathStep) diff.Result { } func (s *state) compareAny(step PathStep) { - // TODO: Support cyclic data structures. - // Update the path stack. s.curPath.push(step) defer s.curPath.pop() @@ -235,67 +230,34 @@ func (s *state) compareAny(step PathStep) { return } - // Rule 3: Recursively descend into each value's underlying kind. + // Rule 3: Compare based on the underlying kind. switch t.Kind() { case reflect.Bool: s.report(vx.Bool() == vy.Bool(), 0) - return case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: s.report(vx.Int() == vy.Int(), 0) - return case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: s.report(vx.Uint() == vy.Uint(), 0) - return case reflect.Float32, reflect.Float64: s.report(vx.Float() == vy.Float(), 0) - return case reflect.Complex64, reflect.Complex128: s.report(vx.Complex() == vy.Complex(), 0) - return case reflect.String: s.report(vx.String() == vy.String(), 0) - return case reflect.Chan, reflect.UnsafePointer: s.report(vx.Pointer() == vy.Pointer(), 0) - return case reflect.Func: s.report(vx.IsNil() && vy.IsNil(), 0) - return case reflect.Struct: s.compareStruct(t, vx, vy) - return - case reflect.Slice: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - fallthrough - case reflect.Array: + case reflect.Slice, reflect.Array: s.compareSlice(t, vx, vy) - return case reflect.Map: s.compareMap(t, vx, vy) - return case reflect.Ptr: - if vx.IsNil() || vy.IsNil() { - s.report(vx.IsNil() && vy.IsNil(), 0) - return - } - vx, vy = vx.Elem(), vy.Elem() - s.compareAny(Indirect{&indirect{pathStep{t.Elem(), vx, vy}}}) - return + s.comparePtr(t, vx, vy) case reflect.Interface: - 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}}}) - return + s.compareInterface(t, vx, vy) default: panic(fmt.Sprintf("%v kind not handled", t.Kind())) } @@ -423,6 +385,14 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { } 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 + } + + // TODO: Support cyclic data structures. + step := SliceIndex{&sliceIndex{pathStep: pathStep{typ: t.Elem()}}} withIndexes := func(ix, iy int) SliceIndex { if ix >= 0 { @@ -492,7 +462,6 @@ func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { iy++ } } - return } func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { @@ -501,6 +470,8 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { return } + // TODO: Support cyclic data structures. + // 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()}}} @@ -530,6 +501,31 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { } } +func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), 0) + return + } + + // TODO: Support cyclic data structures. + + 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 { From 917e382dab80060fd1f094402bfbb5137ec3c4ff Mon Sep 17 00:00:00 2001 From: David Crawshaw Date: Mon, 27 May 2019 12:30:12 -0400 Subject: [PATCH 17/99] Invoke String when formatting map keys (#142) This reverts a change introduced in commit 2940eda701 where cmp stopped calling the String method when printing map keys. Fixes #141 --- cmp/compare_test.go | 2 +- cmp/report_reflect.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index ad03f36..ed46488 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -518,7 +518,7 @@ func comparerTests() []test { y: map[*pb.Stringer]*pb.Stringer(nil), wantDiff: ` map[*testprotos.Stringer]*testprotos.Stringer( -- {⟪0xdeadf00f⟫: s"world"}, +- {s"hello": s"world"}, + nil, ) `, diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 5521c60..2761b62 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -208,7 +208,6 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t func formatMapKey(v reflect.Value) string { var opts formatOptions opts.TypeMode = elideType - opts.AvoidStringer = true opts.ShallowPointers = true s := opts.FormatValue(v, visitedPointers{}).String() return strings.TrimSpace(s) From 1b316004397f1f336546ca058ddb5b95c41a8772 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 6 Jun 2019 11:30:24 -0700 Subject: [PATCH 18/99] Document the reason for output instability (#145) --- cmp/report_text.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmp/report_text.go b/cmp/report_text.go index 80605d0..0353b50 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -19,6 +19,11 @@ 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 { From 00cb0dc383d0b3bc6ef9b8be55dc8c11f7d2f20a Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 1 Aug 2019 23:29:06 +0200 Subject: [PATCH 19/99] Fixed typo in formatDiffList (#148) I assume this is a typo, as the left and right side of the boolean-or were identical. --- cmp/report_compare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 05efb99..17a05ee 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -168,7 +168,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te var isZero bool switch opts.DiffMode { case diffIdentical: - isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueX) + isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY) case diffRemoved: isZero = value.IsZero(r.Value.ValueX) case diffInserted: From 6d8cafd2f64fe3cd66b7530d95df066b00bdd777 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 1 Aug 2019 23:37:55 +0200 Subject: [PATCH 20/99] Simplify code (#149) - No need to wrap ToUpper & Fields - Simpler concatenation - Easier pointer access --- cmp/cmpopts/util_test.go | 4 ++-- cmp/report_slices.go | 2 +- cmp/report_text.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index c85a282..ed0fbb1 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -806,7 +806,7 @@ func TestOptions(t *testing.T) { x: []string{"foo", "Bar", "BAZ"}, y: []string{"Foo", "BAR", "baz"}, opts: []cmp.Option{ - AcyclicTransformer("", func(s string) string { return strings.ToUpper(s) }), + AcyclicTransformer("", strings.ToUpper), }, wantEqual: true, reason: "equal because of strings.ToUpper; AcyclicTransformer unnecessary, but check this still works", @@ -815,7 +815,7 @@ func TestOptions(t *testing.T) { x: "this is a sentence", y: "this is a sentence", opts: []cmp.Option{ - AcyclicTransformer("", func(s string) []string { return strings.Fields(s) }), + AcyclicTransformer("", strings.Fields), }, wantEqual: true, reason: "equal because acyclic transformer splits on any contiguous whitespace", diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 8cb3265..667a6e3 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -322,7 +322,7 @@ func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStat 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) + *prev = prev.Append(*curr).Append(*next) groups = groups[:len(groups)-1] // Truncate off equal group continue } diff --git a/cmp/report_text.go b/cmp/report_text.go index 0353b50..8b8fcab 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -365,7 +365,7 @@ func (s diffStats) String() string { // Pluralize the name (adjusting for some obscure English grammar rules). name := s.Name if sum > 1 { - name = name + "s" + name += "s" if strings.HasSuffix(name, "ys") { name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries" } From 208900aad7b7057a89131f7e63836689b675a042 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Mon, 5 Aug 2019 18:16:46 +0200 Subject: [PATCH 21/99] Fix updating of maxLineLen (#147) Previously, the line pointlessly updated the lastLineIdx variable since the subsequent line always assigned to it. Change it to update maxLineLen, which was the original intention. --- cmp/report_slices.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 667a6e3..eafcf2e 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -90,7 +90,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } if r == '\n' { if maxLineLen < i-lastLineIdx { - lastLineIdx = i - lastLineIdx + maxLineLen = i - lastLineIdx } lastLineIdx = i + 1 numLines++ From 2d0692c2e9617365a95b295612ac0d4415ba4627 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 5 Aug 2019 13:51:40 -0700 Subject: [PATCH 22/99] cmp/internal/value: fix handling of negative zero for floats (#152) * Fix IsZero to properly report false for IsZero(-0.0) since we define IsZero as whether it is equal to the zero memory value. * Add note to isLess that we don't need to handle -0.0 since we can't possibly have both keys present in the same map. * Use sort.SliceStable in SortedKeys for deterministic output since it is possible to have both -0.0 and +0.0 from two different maps. The zero key from the left left map will be taken over the right map. --- cmp/internal/value/sort.go | 4 +++- cmp/internal/value/zero.go | 9 ++++++--- cmp/internal/value/zero_test.go | 7 +++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmp/internal/value/sort.go b/cmp/internal/value/sort.go index 938f646..24fbae6 100644 --- a/cmp/internal/value/sort.go +++ b/cmp/internal/value/sort.go @@ -19,7 +19,7 @@ func SortKeys(vs []reflect.Value) []reflect.Value { } // Sort the map keys. - sort.Slice(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) + sort.SliceStable(vs, func(i, j int) bool { return isLess(vs[i], vs[j]) }) // Deduplicate keys (fails for NaNs). vs2 := vs[:1] @@ -42,6 +42,8 @@ func isLess(x, y reflect.Value) bool { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return x.Uint() < y.Uint() case reflect.Float32, reflect.Float64: + // NOTE: This does not sort -0 as less than +0 + // since Go maps treat -0 and +0 as equal keys. fx, fy := x.Float(), y.Float() return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) case reflect.Complex64, reflect.Complex128: diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go index d13a12c..06a8ffd 100644 --- a/cmp/internal/value/zero.go +++ b/cmp/internal/value/zero.go @@ -4,7 +4,10 @@ package value -import "reflect" +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. @@ -17,9 +20,9 @@ func IsZero(v reflect.Value) bool { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: - return v.Float() == 0 + return math.Float64bits(v.Float()) == 0 case reflect.Complex64, reflect.Complex128: - return v.Complex() == 0 + return math.Float64bits(real(v.Complex())) == 0 && math.Float64bits(imag(v.Complex())) == 0 case reflect.String: return v.String() == "" case reflect.UnsafePointer: diff --git a/cmp/internal/value/zero_test.go b/cmp/internal/value/zero_test.go index 536d50b..1d6c434 100644 --- a/cmp/internal/value/zero_test.go +++ b/cmp/internal/value/zero_test.go @@ -6,6 +6,7 @@ package value import ( "archive/tar" + "math" "reflect" "testing" ) @@ -32,6 +33,12 @@ func TestIsZero(t *testing.T) { {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 { From b1c9c4891a6525d98001fea424c8926c6d77bb56 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Fri, 30 Aug 2019 00:54:27 +0200 Subject: [PATCH 23/99] cmpopts: add EquateApproxTime (#158) --- cmp/cmpopts/equate.go | 36 ++++++++++++++++++++++ cmp/cmpopts/util_test.go | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 41bbddc..fea57bc 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -8,6 +8,7 @@ package cmpopts import ( "math" "reflect" + "time" "github.com/google/go-cmp/cmp" ) @@ -87,3 +88,38 @@ func areNaNsF64s(x, y float64) bool { func areNaNsF32s(x, y float32) bool { return areNaNsF64s(float64(x), float64(y)) } + +// EquateApproxTime returns a Comparer options that +// determine two time.Time values to be equal if they +// are within the given time interval of one another. +// Note that if both times have a monotonic clock reading, +// the monotonic time difference will be used. +// +// The zero time is treated specially: it is only considered +// equal to another zero time value. +// +// It will panic if margin is negative. +func EquateApproxTime(margin time.Duration) cmp.Option { + if margin < 0 { + panic("negative duration in EquateApproxTime") + } + return cmp.FilterValues(func(x, y time.Time) bool { + return !x.IsZero() && !y.IsZero() + }, cmp.Comparer(timeApproximator{margin}.compare)) +} + +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) +} diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index ed0fbb1..3c01cac 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -450,6 +450,64 @@ func TestOptions(t *testing.T) { }, wantEqual: true, reason: "equal because named type is transformed to float64", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(0)}, + wantEqual: true, + reason: "equal because times are identical", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3 * time.Second)}, + wantEqual: true, + reason: "equal because time is exactly at the allowed margin (negative)", + }, { + label: "EquateApproxTime", + x: time.Date(2009, 11, 10, 23, 0, 3, 0, time.UTC), + y: time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC), + opts: []cmp.Option{EquateApproxTime(3*time.Second - 1)}, + 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)}, + 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)}, + 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)}, + 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)}, + reason: "time difference overflows time.Duration", }, { label: "IgnoreFields", x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, @@ -888,6 +946,12 @@ func TestPanic(t *testing.T) { fnc: EquateApprox, args: args(0.0, math.Inf(+1)), reason: "margin of infinity is valid", + }, { + label: "EquateApproxTime", + fnc: EquateApproxTime, + args: args(time.Duration(-1)), + wantPanic: "negative duration in EquateApproxTime", + reason: "negative duration is invalid", }, { label: "SortSlices", fnc: SortSlices, From 481baca67f935f7af3911c083738e47e583d4be5 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 28 Oct 2019 10:26:31 -0700 Subject: [PATCH 24/99] Make retrieveUnexportedField pass Go 1.14's checkptr validation (#169) Fixes #167 --- cmp/export_unsafe.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go index 59d4ee9..5f6ed93 100644 --- a/cmp/export_unsafe.go +++ b/cmp/export_unsafe.go @@ -19,5 +19,7 @@ const supportAllowUnexported = true // The parent struct, v, must be addressable, while f must be a StructField // describing the field to retrieve. func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value { - return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() + // See https://github.com/google/go-cmp/issues/167 for discussion of the + // following expression. + return reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() } From 776445f29feeb6041579ae3df3c5615aba0fa128 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 4 Nov 2019 16:03:44 -0800 Subject: [PATCH 25/99] Print type name in unexported panic (#171) In the panic message when accessing an unexported field, print the full name of the type for user convenience. --- cmp/compare_test.go | 17 +++++++++++++++++ cmp/options.go | 14 +++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index ed46488..5a74e9a 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -15,6 +15,7 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "sync" "testing" @@ -119,6 +120,10 @@ func comparerTests() []test { Xattrs map[string]string } + type namedWithUnexported struct { + unexported string + } + makeTarHeaders := func(tf byte) (hs []tarHeader) { for i := 0; i < 5; i++ { hs = append(hs, tarHeader{ @@ -673,6 +678,18 @@ func comparerTests() []test { } `, 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", }} } diff --git a/cmp/options.go b/cmp/options.go index 7934481..a20390d 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -226,7 +226,19 @@ func (validator) apply(s *state, vx, vy reflect.Value) { // 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 AllowUnexported or cmpopts.IgnoreUnexported" - panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) + 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") From e1f03df4dcb7b99f29424fd98901b73dc1102937 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 16 Dec 2019 08:56:48 -0800 Subject: [PATCH 26/99] Add Exporter option (#176) Add an Exporter option that accepts a function to determine which struct types to permit access to unexported fields. Treat this as a first-class option and implement AllowUnexported in terms of the new Exporter option. The new Exporter option: * Better matches the existing style of top-level options both by name (e.g., Comparer, Transformer, and Reporter) and by API style (all accept a function). * Is more flexible as it enables users to functionally implement AllowAllUnexported by simply doing: Exporter(func(reflect.Type) bool { return true }) Fixes #40 --- cmp/compare.go | 30 ++++++++++++++++-------------- cmp/export_panic.go | 4 ++-- cmp/export_unsafe.go | 2 +- cmp/options.go | 35 +++++++++++++++++++++-------------- cmp/path.go | 2 +- 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 2133562..8419fcc 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -22,8 +22,8 @@ // 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. +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly +// compared using the Exporter option. package cmp import ( @@ -62,8 +62,8 @@ import ( // // 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 AllowUnexported -// option explicitly permits comparing the unexported field. +// (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. @@ -148,8 +148,8 @@ type state struct { dynChecker dynChecker // These fields, once set by processOption, will not change. - exporters map[reflect.Type]bool // Set of structs with unexported field visibility - opts Options // List of all fundamental and filter options + exporters []exporter // List of exporters for structs with unexported fields + opts Options // List of all fundamental and filter options } func newState(opts []Option) *state { @@ -174,13 +174,8 @@ func (s *state) processOption(opt Option) { panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) } s.opts = append(s.opts, opt) - case visibleStructs: - if s.exporters == nil { - s.exporters = make(map[reflect.Type]bool) - } - for t := range opt { - s.exporters[t] = true - } + case exporter: + s.exporters = append(s.exporters, opt) case reporter: s.reporters = append(s.reporters, opt) default: @@ -354,6 +349,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { 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 @@ -375,7 +371,13 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { vax = makeAddressable(vx) vay = makeAddressable(vy) } - step.mayForce = s.exporters[t] + if !mayForceInit { + for _, xf := range s.exporters { + mayForce = mayForce || xf(t) + } + mayForceInit = true + } + step.mayForce = mayForce step.pvx = vax step.pvy = vay step.field = t.Field(i) diff --git a/cmp/export_panic.go b/cmp/export_panic.go index abc3a1c..dd03235 100644 --- a/cmp/export_panic.go +++ b/cmp/export_panic.go @@ -8,8 +8,8 @@ package cmp import "reflect" -const supportAllowUnexported = false +const supportExporters = false func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value { - panic("retrieveUnexportedField is not implemented") + panic("no support for forcibly accessing unexported fields") } diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go index 5f6ed93..57020e2 100644 --- a/cmp/export_unsafe.go +++ b/cmp/export_unsafe.go @@ -11,7 +11,7 @@ import ( "unsafe" ) -const supportAllowUnexported = true +const supportExporters = true // retrieveUnexportedField uses unsafe to forcibly retrieve any field from // a struct such that the value has read-write permissions. diff --git a/cmp/options.go b/cmp/options.go index a20390d..409e803 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -225,7 +225,7 @@ func (validator) apply(s *state, vx, vy reflect.Value) { // 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 AllowUnexported or cmpopts.IgnoreUnexported" + 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. @@ -372,9 +372,8 @@ func (cm comparer) String() string { return fmt.Sprintf("Comparer(%s)", function.NameOf(cm.fnc)) } -// AllowUnexported returns an Option that forcibly allows operations on -// unexported fields in certain structs, which are specified by passing in a -// value of each struct type. +// Exporter returns an Option that specifies whether Equal is allowed to +// introspect into the unexported fields of certain struct types. // // Users of this option must understand that comparing on unexported fields // from external packages is not safe since changes in the internal @@ -398,10 +397,24 @@ func (cm comparer) String() string { // // In other cases, the cmpopts.IgnoreUnexported option can be used to ignore // all unexported fields on specified struct types. -func AllowUnexported(types ...interface{}) Option { - if !supportAllowUnexported { - panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS") +func Exporter(f func(reflect.Type) bool) Option { + if !supportExporters { + panic("Exporter is not supported on purego builds") } + return exporter(f) +} + +type exporter func(reflect.Type) bool + +func (exporter) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { + panic("not implemented") +} + +// AllowUnexported returns an Options that allows Equal to forcibly introspect +// unexported fields of the specified struct types. +// +// See Exporter for the proper use of this option. +func AllowUnexported(types ...interface{}) Option { m := make(map[reflect.Type]bool) for _, typ := range types { t := reflect.TypeOf(typ) @@ -410,13 +423,7 @@ func AllowUnexported(types ...interface{}) Option { } m[t] = true } - return visibleStructs(m) -} - -type visibleStructs map[reflect.Type]bool - -func (visibleStructs) filter(_ *state, _ reflect.Type, _, _ reflect.Value) applicableOption { - panic("not implemented") + return exporter(func(t reflect.Type) bool { return m[t] }) } // Result represents the comparison result for a single node and diff --git a/cmp/path.go b/cmp/path.go index 96fffd2..d7a420d 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -41,7 +41,7 @@ type PathStep interface { // 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 - // AllowUnexported to traverse unexported fields. + // 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 From 3838af334ff48ab62a68998c9a4ee9d847017617 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 16 Dec 2019 08:58:15 -0800 Subject: [PATCH 27/99] Adjust style of EquateApproxTime (#177) Adjust coding style of EquateApproxTime to match EquateApprox. --- cmp/cmpopts/equate.go | 25 ++++++++--------- cmp/cmpopts/util_test.go | 59 ++++++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index fea57bc..343dcf9 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -89,23 +89,20 @@ func areNaNsF32s(x, y float32) bool { return areNaNsF64s(float64(x), float64(y)) } -// EquateApproxTime returns a Comparer options that -// determine two time.Time values to be equal if they -// are within the given time interval of one another. -// Note that if both times have a monotonic clock reading, -// the monotonic time difference will be used. -// -// The zero time is treated specially: it is only considered -// equal to another zero time value. -// -// It will panic if margin is negative. +// 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("negative duration in EquateApproxTime") + panic("margin must be a non-negative number") } - return cmp.FilterValues(func(x, y time.Time) bool { - return !x.IsZero() && !y.IsZero() - }, cmp.Comparer(timeApproximator{margin}.compare)) + 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 { diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 3c01cac..e80c963 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -472,17 +472,19 @@ func TestOptions(t *testing.T) { 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)}, - 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)}, - reason: "not equal because time is outside 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{}, @@ -491,23 +493,26 @@ func TestOptions(t *testing.T) { 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)}, - reason: "not equal because zero time is always not equal not non-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)}, - 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)}, - reason: "time difference overflows time.Duration", + 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: "IgnoreFields", x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, @@ -950,7 +955,7 @@ func TestPanic(t *testing.T) { label: "EquateApproxTime", fnc: EquateApproxTime, args: args(time.Duration(-1)), - wantPanic: "negative duration in EquateApproxTime", + wantPanic: "margin must be a non-negative number", reason: "negative duration is invalid", }, { label: "SortSlices", From 340f1ebe299ef6712c79da23ad5bc6e3efad8250 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 16 Dec 2019 09:05:41 -0800 Subject: [PATCH 28/99] Add EquateErrors helper (#178) The EquateErrors helper equates errors according to errors.Is. We also declare a sentinel AnyError value that matches any non-nil error value. This adds a dependency on golang.org/x/xerrors so that we can continue to suppport go1.8, which is our current minimally supported version of Go. Fixes #89 --- cmp/cmpopts/equate.go | 34 ++++++++ cmp/cmpopts/util_test.go | 164 +++++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 + 4 files changed, 202 insertions(+) create mode 100644 go.sum diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 343dcf9..e102849 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" ) func equateAlways(_, _ interface{}) bool { return true } @@ -120,3 +121,36 @@ func (a timeApproximator) compare(x, y time.Time) bool { // 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/util_test.go b/cmp/cmpopts/util_test.go index e80c963..d0fd888 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -6,6 +6,7 @@ package cmpopts import ( "bytes" + "errors" "fmt" "io" "math" @@ -16,6 +17,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" ) type ( @@ -513,6 +515,168 @@ func TestOptions(t *testing.T) { 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}}}}, diff --git a/go.mod b/go.mod index 6c0e40e..5391dee 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/google/go-cmp go 1.8 + +require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3ab73ea --- /dev/null +++ b/go.sum @@ -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= From 5a6f75716e1203a923a78c9efb94089d857df0f6 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 16 Dec 2019 13:18:14 -0800 Subject: [PATCH 29/99] Add support for comparing graphs (#85) Previously, trying to call Equal on a graph would result in a stack-overflow due to infinite recursion traversing cycles on a graph. While a vast majority of Go values are trees or acyclic graphs, there exist a small number of cases where graph equality is required. As such, we add cycle detection to Equal and define what it means for two graphs to be equal. Contrary to reflect.DeepEqual, which declares two graphs to be equal so long any cycle were encountered, we require two graphs to have equivalent graph structures. Mathematically speaking, a graph G is a tuple (V, E) consisting of the set of vertices and edges in that graph. Graphs G1 and G2 are equal if V1 == V2, E1 == E2, and both have the same root vertex (entry point into the graph). When traversing G1 and G2, we remember a stack of previously visited edges ES1 and ES2. If the current edge e1 is in ES1 or e2 is in ES2, then we know that a cycle exists. The graphs have the same structure when the previously encountered edge ep1 and ep2 were encountered together. Note that edges and vertices unreachable from the root vertex are ignored. Appreciation goes to Eyal Posener (@posener), who proposed a different (but semantically equivalent) approach in #79, which served as inspiration. Fixes #74 --- cmp/compare.go | 53 +++++- cmp/compare_test.go | 439 ++++++++++++++++++++++++++++++++++++++++++++ cmp/options.go | 6 + cmp/path.go | 69 +++++++ 4 files changed, 559 insertions(+), 8 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 8419fcc..c9a63ce 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -80,6 +80,11 @@ import ( // 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 { vx := reflect.ValueOf(x) vy := reflect.ValueOf(y) @@ -137,6 +142,7 @@ type state struct { // 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 @@ -155,6 +161,7 @@ type state struct { 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 } @@ -187,9 +194,9 @@ func (s *state) processOption(opt Option) { // 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 the curPath because all of the compareX - // methods should properly push and pop from the path. - // It is an implementation bug if the contents of curPath differs from + // 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 @@ -211,9 +218,17 @@ func (s *state) compareAny(step PathStep) { } s.recChecker.Check(s.curPath) - // Obtain the current type and values. + // 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) { @@ -393,9 +408,21 @@ func (s *state) compareSlice(t reflect.Type, vx, vy reflect.Value) { return } - // TODO: Support cyclic data structures. + // 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()}}} + 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 @@ -472,7 +499,12 @@ func (s *state) compareMap(t reflect.Type, vx, vy reflect.Value) { return } - // TODO: Support cyclic data structures. + // 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. @@ -509,7 +541,12 @@ func (s *state) comparePtr(t reflect.Type, vx, vy reflect.Value) { return } - // TODO: Support cyclic data structures. + // 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}}}) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 5a74e9a..e910bd1 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -52,6 +52,7 @@ func TestDiff(t *testing.T) { tests = append(tests, transformerTests()...) tests = append(tests, embeddedTests()...) tests = append(tests, methodTests()...) + tests = append(tests, cycleTests()...) tests = append(tests, project1Tests()...) tests = append(tests, project2Tests()...) tests = append(tests, project3Tests()...) @@ -2234,6 +2235,444 @@ func methodTests() []test { }} } +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 + wantDiff string + reason string + }{{ + in: func() XY { + x := new(P) + *x = x + y := new(P) + *y = y + return XY{x, y} + }(), + }, { + in: func() XY { + x := new(P) + *x = x + y1, y2 := new(P), new(P) + *y1 = y2 + *y2 = y1 + return XY{x, y1} + }(), + wantDiff: ` + &&cmp_test.P( +- &⟪0xdeadf00f⟫, ++ &&⟪0xdeadf00f⟫, + ) +`, + }, { + in: func() XY { + x := S{nil} + x[0] = x + y := S{nil} + y[0] = y + return XY{x, y} + }(), + }, { + 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} + }(), + wantDiff: ` + cmp_test.S{ +- {{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}, ++ {{{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}}, + } +`, + }, { + in: func() XY { + x := M{0: nil} + x[0] = x + y := M{0: nil} + y[0] = y + return XY{x, y} + }(), + }, { + 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} + }(), + wantDiff: ` + cmp_test.M{ +- 0: {0: ⟪0xdeadf00f⟫}, ++ 0: {0: {0: ⟪0xdeadf00f⟫}}, + } +`, + }, { + in: XY{makeGraph(), makeGraph()}, + }, { + 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} + }(), + wantDiff: ` + 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⟫}}}}}, + }, + }, + }, + } +`, + }, { + in: func() XY { + x := makeGraph() + y := makeGraph() + x["Buzz"].Bravos["BuzzBarBravo"] = &CycleBravo{ + Name: "BuzzBarBravo", + ID: 103, + } + return XY{x, y} + }(), + wantDiff: ` + 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⟫}}}}}}}}, + } +`, + }} { + tests = append(tests, test{ + label: label, + x: tt.in.x, + y: tt.in.y, + wantDiff: tt.wantDiff, + reason: tt.reason, + }) + } + return tests +} + func project1Tests() []test { const label = "Project1" diff --git a/cmp/options.go b/cmp/options.go index 409e803..abbd2a6 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -455,6 +455,11 @@ 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 ( @@ -465,6 +470,7 @@ const ( reportByIgnore reportByMethod reportByFunc + reportByCycle ) // Reporter is an Option that can be passed to Equal. When Equal traverses diff --git a/cmp/path.go b/cmp/path.go index d7a420d..509d6b8 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -10,6 +10,8 @@ import ( "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 @@ -207,6 +209,7 @@ 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 } @@ -301,6 +304,72 @@ func (tf Transform) Func() reflect.Value { return tf.trans.fnc } // 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) From 5915021f6d960523d973d5e6d745bebcbd684cc3 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 27 Feb 2020 10:32:33 -0800 Subject: [PATCH 30/99] Update README.md to use go.dev for documentation (#190) The color of the shield banner is "Gopher Blue" as defined by brand book referenced by the Go blog. See https://blog.golang.org/go-brand --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61c9c4c..ed0eb9b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Package for equality of Go values -[![GoDoc](https://godoc.org/github.com/google/go-cmp/cmp?status.svg)][godoc] +[![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 @@ -24,11 +24,11 @@ The primary features of `cmp` are: by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explicitly compared using the `AllowUnexported` option. -See the [GoDoc documentation][godoc] for more information. +See the [documentation][godev] for more information. This is not an official Google product. -[godoc]: https://godoc.org/github.com/google/go-cmp/cmp +[godev]: https://pkg.go.dev/github.com/google/go-cmp/cmp [travis]: https://travis-ci.org/google/go-cmp ## Install From 6fdcbe1f13348761e9b660308f95e71af072ae48 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 27 Feb 2020 10:37:02 -0800 Subject: [PATCH 31/99] Update tested Go versions (#188) --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index ae1878d..efb4782 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,12 @@ matrix: 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 ./... From f6dc95b586bc4e5c03cc308129693d9df2819e1c Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 27 Feb 2020 10:53:46 -0800 Subject: [PATCH 32/99] Document the test-only intentions of this package (#189) This package was never intended to be used in production code. Document the expectation that this package was only intended to be used for writing tests. --- cmp/compare.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmp/compare.go b/cmp/compare.go index c9a63ce..7ad400f 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -6,6 +6,10 @@ // // 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: // From cb8c7f84fcfb230736f1e5922b3132f47bc88500 Mon Sep 17 00:00:00 2001 From: Chris Morrow Date: Sat, 28 Mar 2020 21:24:57 -0400 Subject: [PATCH 33/99] Fix typo on example (#193) Remove redundant "be". --- cmp/example_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/example_test.go b/cmp/example_test.go index 5954780..2689efb 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -219,7 +219,7 @@ 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 be +// If the Equal method defined on a type is not suitable, the type can be // dynamically transformed to be stripped of the Equal method (or any method // for that matter). func ExampleOption_avoidEqualMethod() { From 049b73f65ccf77e9b278cef9beb6a20a9f55f8e7 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba.git@gmail.com> Date: Thu, 14 May 2020 04:00:48 +0900 Subject: [PATCH 34/99] Add reporterTests to TestDiff (#198) --- cmp/compare_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index e910bd1..0eec33b 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -50,6 +50,7 @@ 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()...) From 0c08307de36daab62772d2ef30aa623780f80c5d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 13 May 2020 15:08:07 -0700 Subject: [PATCH 35/99] Refactor tests to use golden test files (#200) Refactor the unit tests to read the diffs from a file rather than being stored as a Go string literal. The advantage of using a golden file is to ease updating the diffs whenever the reporter output is changed. --- cmp/compare_test.go | 2096 ++++++++++++------------------------------- cmp/testdata/diffs | 1171 ++++++++++++++++++++++++ 2 files changed, 1754 insertions(+), 1513 deletions(-) create mode 100644 cmp/testdata/diffs diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 0eec33b..a08df1b 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -8,8 +8,10 @@ import ( "bytes" "crypto/md5" "encoding/json" + "flag" "fmt" "io" + "io/ioutil" "math" "math/rand" "reflect" @@ -33,6 +35,70 @@ 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 } @@ -41,7 +107,7 @@ type test struct { label string // Test name x, y interface{} // Input values to compare opts []cmp.Option // Input options - wantDiff string // The exact difference string + wantEqual bool // Whether any difference is expected wantPanic string // Sub-string of an expected panic message reason string // The reason for the expected outcome } @@ -59,10 +125,15 @@ func TestDiff(t *testing.T) { tests = append(tests, project3Tests()...) tests = append(tests, project4Tests()...) + const goldenFile = "testdata/diffs" + gotDiffs := []struct{ Name, Data string }{} + wantDiffs := mustParseGolden(goldenFile) for _, tt := range tests { tt := tt t.Run(tt.label, func(t *testing.T) { - t.Parallel() + if !*update { + t.Parallel() + } var gotDiff, gotPanic string func() { defer func() { @@ -76,14 +147,25 @@ func TestDiff(t *testing.T) { }() 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) } - tt.wantDiff = strings.TrimPrefix(tt.wantDiff, "\n") - if gotDiff != tt.wantDiff { - t.Fatalf("difference message:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, tt.wantDiff, 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) { @@ -92,6 +174,10 @@ func TestDiff(t *testing.T) { } }) } + + if *update { + mustFormatGolden(goldenFile, gotDiffs) + } } func comparerTests() []test { @@ -140,13 +226,15 @@ func comparerTests() []test { } return []test{{ - label: label, - x: nil, - y: nil, + label: label, + x: nil, + y: nil, + wantEqual: true, }, { - label: label, - x: 1, - y: 1, + label: label, + x: 1, + y: 1, + wantEqual: true, }, { label: label, x: 1, @@ -185,45 +273,36 @@ func comparerTests() []test { 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}, + 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}, - wantDiff: ` - struct{ A int; B int; C int }{ - A: 1, - B: 2, -- C: 3, -+ C: 4, - } -`, + 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)}, + 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)}, - wantDiff: ` - &struct{ A *int }{ -- A: &4, -+ A: &5, - } -`, + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + wantEqual: false, }, { label: label, x: &struct{ A *int }{intPtr(4)}, @@ -231,6 +310,7 @@ func comparerTests() []test { opts: []cmp.Option{ cmp.Comparer(func(x, y int) bool { return true }), }, + wantEqual: true, }, { label: label, x: &struct{ A *int }{intPtr(4)}, @@ -238,20 +318,17 @@ func comparerTests() []test { 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 }{}, + 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 }{}, - wantDiff: ` - &struct{ R *bytes.Buffer }{ -- R: s"", -+ R: nil, - } -`, + 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)}, @@ -259,6 +336,7 @@ func comparerTests() []test { opts: []cmp.Option{ cmp.Comparer(func(x, y io.Reader) bool { return true }), }, + wantEqual: true, }, { label: label, x: &struct{ R bytes.Buffer }{}, @@ -280,6 +358,7 @@ func comparerTests() []test { 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*")}, @@ -295,6 +374,7 @@ func comparerTests() []test { } return x.String() == y.String() })}, + wantEqual: true, }, { label: label, x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, @@ -305,13 +385,7 @@ func comparerTests() []test { } return x.String() == y.String() })}, - wantDiff: ` - []*regexp.Regexp{ - nil, -- s"a*b*c*", -+ s"a*b*d*", - } -`, + wantEqual: false, }, { label: label, x: func() ***int { @@ -326,6 +400,7 @@ func comparerTests() []test { c := &b return &c }(), + wantEqual: true, }, { label: label, x: func() ***int { @@ -340,109 +415,39 @@ func comparerTests() []test { c := &b return &c }(), - wantDiff: ` - &&&int( -- 0, -+ 1, - ) -`, + wantEqual: false, }, { - label: label, - x: []int{1, 2, 3, 4, 5}[:3], - y: []int{1, 2, 3}, + 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() })}, + 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() })}, - wantDiff: ` - struct{ fmt.Stringer }( -- s"hello", -+ s"hello2", - ) -`, + 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'}), - wantDiff: ` - [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, - } -`, + label: label, + x: md5.Sum([]byte{'a'}), + y: md5.Sum([]byte{'b'}), + wantEqual: false, }, { - label: label, - x: new(fmt.Stringer), - y: nil, - wantDiff: ` - interface{}( -- &fmt.Stringer(nil), - ) -`, + label: label, + x: new(fmt.Stringer), + y: nil, + wantEqual: false, }, { - label: label, - x: makeTarHeaders('0'), - y: makeTarHeaders('\x00'), - wantDiff: ` - []cmp_test.tarHeader{ - { - ... // 4 identical fields - Size: 1, - ModTime: s"2009-11-10 23:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, - Linkname: "", - Uname: "user", - ... // 6 identical fields - }, - { - ... // 4 identical fields - Size: 2, - ModTime: s"2009-11-11 00:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, - Linkname: "", - Uname: "user", - ... // 6 identical fields - }, - { - ... // 4 identical fields - Size: 4, - ModTime: s"2009-11-11 01:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, - Linkname: "", - Uname: "user", - ... // 6 identical fields - }, - { - ... // 4 identical fields - Size: 8, - ModTime: s"2009-11-11 02:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, - Linkname: "", - Uname: "user", - ... // 6 identical fields - }, - { - ... // 4 identical fields - Size: 16, - ModTime: s"2009-11-11 03:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, - Linkname: "", - Uname: "user", - ... // 6 identical fields - }, - } -`, + label: label, + x: makeTarHeaders('0'), + y: makeTarHeaders('\x00'), + wantEqual: false, }, { label: label, x: make([]int, 1000), @@ -494,50 +499,18 @@ func comparerTests() []test { return math.NaN() }), }, - wantDiff: ` - []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)), - } -`, + 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), - wantDiff: ` - map[*testprotos.Stringer]*testprotos.Stringer( -- {s"hello": s"world"}, -+ nil, - ) -`, + 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`}}, - wantDiff: strings.Replace(` - interface{}( -- []*testprotos.Stringer{s'multi\nline\nline\nline'}, - ) -`, "'", "`", -1), + label: label, + x: []*pb.Stringer{{`multi\nline\nline\nline`}}, + wantEqual: false, }, { label: label, x: struct{ I Iface2 }{}, @@ -547,6 +520,7 @@ func comparerTests() []test { return x == nil && y == nil }), }, + wantEqual: true, }, { label: label, x: struct{ I Iface2 }{}, @@ -556,6 +530,7 @@ func comparerTests() []test { return v == nil }), }, + wantEqual: true, }, { label: label, x: struct{ I Iface2 }{}, @@ -565,26 +540,12 @@ func comparerTests() []test { 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"}}, - wantDiff: ` - []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"), - }, - } -`, + 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{ @@ -593,12 +554,7 @@ func comparerTests() []test { y: map[*int]string{ new(int): "world", }, - wantDiff: ` - map[*int]string{ -- ⟪0xdeadf00f⟫: "hello", -+ ⟪0xdeadf00f⟫: "world", - } -`, + wantEqual: false, }, { label: label, x: intPtr(0), @@ -606,13 +562,8 @@ func comparerTests() []test { opts: []cmp.Option{ cmp.Comparer(func(x, y *int) bool { return x == y }), }, - // TODO: This output is unhelpful and should show the address. - wantDiff: ` - (*int)( -- &0, -+ &0, - ) -`, + // TODO: This diff output is unhelpful and should show the address. + wantEqual: false, }, { label: label, x: [2][]int{ @@ -635,18 +586,8 @@ func comparerTests() []test { return false }, cmp.Ignore()), }, - wantDiff: ` - [2][]int{ - {..., 1, 2, 3, ..., 4, 5, 6, 7, ..., 8, ..., 9, ...}, - { - ... // 6 ignored and 1 identical elements -- 20, -+ 2, - ... // 3 ignored elements - }, - } -`, - reason: "all zero slice elements are ignored (even if missing)", + wantEqual: false, + reason: "all zero slice elements are ignored (even if missing)", }, { label: label, x: [2]map[string]int{ @@ -669,17 +610,8 @@ func comparerTests() []test { return false }, cmp.Ignore()), }, - wantDiff: ` - [2]map[string]int{ - {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, - { - ... // 2 ignored entries - "keep1": 1, -+ "keep2": 2, - }, - } -`, - reason: "all zero map entries are ignored (even if missing)", + wantEqual: false, + reason: "all zero map entries are ignored (even if missing)", }, { label: label, x: namedWithUnexported{}, @@ -724,12 +656,7 @@ func transformerTests() []test { cmp.Transformer("λ", func(in uint16) uint32 { return uint32(in) }), cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), }, - wantDiff: ` - uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( -- 0x00, -+ 0x01, - ))))))) -`, + wantEqual: false, }, { label: label, x: 0, @@ -753,16 +680,7 @@ func transformerTests() []test { cmp.Transformer("λ", func(in int) int64 { return int64(in) }), ), }, - wantDiff: ` - []int{ - Inverse(λ, int64(0)), -- Inverse(λ, int64(-5)), -+ Inverse(λ, int64(3)), - Inverse(λ, int64(0)), -- Inverse(λ, int64(-1)), -+ Inverse(λ, int64(-5)), - } -`, + wantEqual: false, }, { label: label, x: 0, @@ -775,12 +693,7 @@ func transformerTests() []test { return float64(in) }), }, - wantDiff: ` - int(Inverse(λ, interface{}( -- string("zero"), -+ float64(1), - ))) -`, + wantEqual: false, }, { label: label, x: `{ @@ -819,33 +732,7 @@ func transformerTests() []test { return m }), }, - wantDiff: ` - 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, - })) -`, + wantEqual: false, }, { label: label, x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")}, @@ -854,29 +741,7 @@ func transformerTests() []test { transformOnce("SplitString", func(s string) []string { return strings.Split(s, "\n") }), transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }), }, - wantDiff: ` - 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 - }, - })), - } -`, + wantEqual: false, }, { x: "a\nb\nc\n", y: "a\nb\nc\n", @@ -928,51 +793,17 @@ func reporterTests() []test { ) 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}}, - wantDiff: ` - 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 - } -`, - reason: "unbatched diffing desired since few elements differ", + 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}}, - wantDiff: ` - 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 - } -`, - reason: "batched diffing desired since many 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{ @@ -1003,141 +834,26 @@ func reporterTests() []test { FloatsB: []MyFloat{6.5, 5.5, 4.5}, FloatsC: MyFloats{9.5, 8.5, 7.5}, }, - wantDiff: ` - 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{ -- 0x03e8, 0x07d0, 0x0bb8, -+ 0x0bb8, 0x07d0, 0x03e8, - }, - 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, - }, - } -`, - reason: "batched diffing available for both named and unnamed slices", + 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]")}, - wantDiff: ` - 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 - } -`, - reason: "binary diff in hexdump form since data is binary data", + 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")}, - wantDiff: ` - 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 - } -`, - reason: "binary diff desired since string looks like 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}`)}, - wantDiff: strings.Replace(` - 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 - } -`, "'", "`", -1), - reason: "batched textual diff desired since bytes looks like textual 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{ @@ -1187,33 +903,8 @@ 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"), }, - wantDiff: ` - 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 - } -`, - reason: "batched per-line diff desired since string looks like multi-line textual data", + wantEqual: false, + reason: "batched per-line diff desired since string looks like multi-line textual data", }} } @@ -1330,6 +1021,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructA{}), }, + wantEqual: true, }, { label: label + "ParentStructA", x: createStructA(0), @@ -1345,6 +1037,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructA", x: createStructA(0), @@ -1352,16 +1045,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, - wantDiff: ` - teststructs.ParentStructA{ - privateStruct: teststructs.privateStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - } -`, + wantEqual: false, }, { label: label + "ParentStructB", x: ts.ParentStructB{}, @@ -1378,6 +1062,7 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.ParentStructB{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructB", x: createStructB(0), @@ -1393,6 +1078,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructB", x: createStructB(0), @@ -1400,16 +1086,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, - wantDiff: ` - teststructs.ParentStructB{ - PublicStruct: teststructs.PublicStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - } -`, + wantEqual: false, }, { label: label + "ParentStructC", x: ts.ParentStructC{}, @@ -1422,6 +1099,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructC{}), }, + wantEqual: true, }, { label: label + "ParentStructC", x: createStructC(0), @@ -1437,6 +1115,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructC", x: createStructC(0), @@ -1444,20 +1123,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, - wantDiff: ` - teststructs.ParentStructC{ - privateStruct: teststructs.privateStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, -- Public: 3, -+ Public: 4, -- private: 4, -+ private: 5, - } -`, + wantEqual: false, }, { label: label + "ParentStructD", x: ts.ParentStructD{}, @@ -1474,6 +1140,7 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.ParentStructD{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructD", x: createStructD(0), @@ -1489,6 +1156,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructD", x: createStructD(0), @@ -1496,20 +1164,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, - wantDiff: ` - teststructs.ParentStructD{ - PublicStruct: teststructs.PublicStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, -- Public: 3, -+ Public: 4, -- private: 4, -+ private: 5, - } -`, + wantEqual: false, }, { label: label + "ParentStructE", x: ts.ParentStructE{}, @@ -1526,6 +1181,7 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.ParentStructE{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructE", x: createStructE(0), @@ -1549,6 +1205,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructE", x: createStructE(0), @@ -1556,22 +1213,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` - teststructs.ParentStructE{ - privateStruct: teststructs.privateStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - PublicStruct: teststructs.PublicStruct{ -- Public: 3, -+ Public: 4, -- private: 4, -+ private: 5, - }, - } -`, + wantEqual: false, }, { label: label + "ParentStructF", x: ts.ParentStructF{}, @@ -1588,6 +1230,7 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.ParentStructF{}), cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructF", x: createStructF(0), @@ -1611,6 +1254,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructF", x: createStructF(0), @@ -1618,31 +1262,13 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` - 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, - } -`, + 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{}, @@ -1650,6 +1276,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructG{}), }, + wantEqual: true, }, { label: label + "ParentStructG", x: createStructG(0), @@ -1665,6 +1292,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructG", x: createStructG(0), @@ -1672,20 +1300,12 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, - wantDiff: ` - &teststructs.ParentStructG{ - privateStruct: &teststructs.privateStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - } -`, + wantEqual: false, }, { - label: label + "ParentStructH", - x: ts.ParentStructH{}, - y: ts.ParentStructH{}, + label: label + "ParentStructH", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + wantEqual: true, }, { label: label + "ParentStructH", x: createStructH(0), @@ -1698,6 +1318,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructH{}), }, + wantEqual: true, }, { label: label + "ParentStructH", x: createStructH(0), @@ -1713,6 +1334,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructH", x: createStructH(0), @@ -1720,21 +1342,13 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, - wantDiff: ` - &teststructs.ParentStructH{ - PublicStruct: &teststructs.PublicStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - } -`, + 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{}, @@ -1742,6 +1356,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}), }, + wantEqual: true, }, { label: label + "ParentStructI", x: createStructI(0), @@ -1757,6 +1372,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructI", x: createStructI(0), @@ -1772,6 +1388,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructI", x: createStructI(0), @@ -1779,22 +1396,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` - &teststructs.ParentStructI{ - privateStruct: &teststructs.privateStruct{ -- Public: 1, -+ Public: 2, -- private: 2, -+ private: 3, - }, - PublicStruct: &teststructs.PublicStruct{ -- Public: 3, -+ Public: 4, -- private: 4, -+ private: 5, - }, - } -`, + wantEqual: false, }, { label: label + "ParentStructJ", x: ts.ParentStructJ{}, @@ -1815,6 +1417,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), }, + wantEqual: true, }, { label: label + "ParentStructJ", x: createStructJ(0), @@ -1830,6 +1433,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, + wantEqual: true, }, { label: label + "ParentStructJ", x: createStructJ(0), @@ -1837,34 +1441,7 @@ func embeddedTests() []test { opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, - wantDiff: ` - &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, - }, - } -`, + wantEqual: false, }} } @@ -1899,340 +1476,284 @@ func methodTests() []test { // returns true, while the underlying data are fundamentally different. // Since the method should be called, these are expected to be equal. return []test{{ - label: label + "StructA", - x: ts.StructA{X: "NotEqual"}, - y: ts.StructA{X: "not_equal"}, - }, { - label: label + "StructA", - x: &ts.StructA{X: "NotEqual"}, - y: &ts.StructA{X: "not_equal"}, - }, { - label: label + "StructB", - x: ts.StructB{X: "NotEqual"}, - y: ts.StructB{X: "not_equal"}, - wantDiff: ` - teststructs.StructB{ -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructB", - x: ts.StructB{X: "NotEqual"}, - y: ts.StructB{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB", - x: &ts.StructB{X: "NotEqual"}, - y: &ts.StructB{X: "not_equal"}, - }, { - label: label + "StructC", - x: ts.StructC{X: "NotEqual"}, - y: ts.StructC{X: "not_equal"}, - }, { - label: label + "StructC", - x: &ts.StructC{X: "NotEqual"}, - y: &ts.StructC{X: "not_equal"}, - }, { - label: label + "StructD", - x: ts.StructD{X: "NotEqual"}, - y: ts.StructD{X: "not_equal"}, - wantDiff: ` - teststructs.StructD{ -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructD", - x: ts.StructD{X: "NotEqual"}, - y: ts.StructD{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructD", - x: &ts.StructD{X: "NotEqual"}, - y: &ts.StructD{X: "not_equal"}, - }, { - label: label + "StructE", - x: ts.StructE{X: "NotEqual"}, - y: ts.StructE{X: "not_equal"}, - wantDiff: ` - teststructs.StructE{ -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructE", - x: ts.StructE{X: "NotEqual"}, - y: ts.StructE{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructE", - x: &ts.StructE{X: "NotEqual"}, - y: &ts.StructE{X: "not_equal"}, - }, { - label: label + "StructF", - x: ts.StructF{X: "NotEqual"}, - y: ts.StructF{X: "not_equal"}, - wantDiff: ` - teststructs.StructF{ -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructF", - x: &ts.StructF{X: "NotEqual"}, - y: &ts.StructF{X: "not_equal"}, - }, { - label: label + "StructA1", - x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, - y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA1", - x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructA1{ - StructA: teststructs.StructA{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructA1", - x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, - y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA1", - x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - &teststructs.StructA1{ - StructA: teststructs.StructA{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructB1", - x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, - y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB1", - x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: ` - teststructs.StructB1{ - StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructB1", - x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, - y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructB1", - x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - wantDiff: ` - &teststructs.StructB1{ - StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})), -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructC1", - x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructC1", - x: &ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD1", - x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructD1{ -- StructD: teststructs.StructD{X: "NotEqual"}, -+ StructD: teststructs.StructD{X: "not_equal"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructD1", - x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructD1", - x: &ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE1", - x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructE1{ -- StructE: teststructs.StructE{X: "NotEqual"}, -+ StructE: teststructs.StructE{X: "not_equal"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructE1", - x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, - }, { - label: label + "StructE1", - x: &ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF1", - x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructF1{ -- StructF: teststructs.StructF{X: "NotEqual"}, -+ StructF: teststructs.StructF{X: "not_equal"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructF1", - x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructA2", - x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, - y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA2", - x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructA2{ - StructA: &teststructs.StructA{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructA2", - x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, - y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructA2", - x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - &teststructs.StructA2{ - StructA: &teststructs.StructA{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructB2", - x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, - y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructB2", - x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - teststructs.StructB2{ - StructB: &teststructs.StructB{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructB2", - x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, - y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, - }, { - label: label + "StructB2", - x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, - wantDiff: ` - &teststructs.StructB2{ - StructB: &teststructs.StructB{X: "NotEqual"}, -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "StructC2", - x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructC2", - x: &ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD2", - x: ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructD2", - x: &ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE2", - x: ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructE2", - x: &ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF2", - x: ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructF2", - x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, - y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, - }, { - label: label + "StructNo", - x: ts.StructNo{X: "NotEqual"}, - y: ts.StructNo{X: "not_equal"}, - wantDiff: ` - teststructs.StructNo{ -- X: "NotEqual", -+ X: "not_equal", - } -`, - }, { - label: label + "AssignA", - x: ts.AssignA(func() int { return 0 }), - y: ts.AssignA(func() int { return 1 }), - }, { - label: label + "AssignB", - x: ts.AssignB(struct{ A int }{0}), - y: ts.AssignB(struct{ A int }{1}), - }, { - label: label + "AssignC", - x: ts.AssignC(make(chan bool)), - y: ts.AssignC(make(chan bool)), - }, { - label: label + "AssignD", - x: ts.AssignD(make(chan bool)), - y: ts.AssignD(make(chan bool)), + label: label + "StructA", + 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, }} } @@ -2317,9 +1838,9 @@ func cycleTests() []test { var tests []test type XY struct{ x, y interface{} } for _, tt := range []struct { - in XY - wantDiff string - reason string + in XY + wantEqual bool + reason string }{{ in: func() XY { x := new(P) @@ -2328,6 +1849,7 @@ func cycleTests() []test { *y = y return XY{x, y} }(), + wantEqual: true, }, { in: func() XY { x := new(P) @@ -2337,12 +1859,7 @@ func cycleTests() []test { *y2 = y1 return XY{x, y1} }(), - wantDiff: ` - &&cmp_test.P( -- &⟪0xdeadf00f⟫, -+ &&⟪0xdeadf00f⟫, - ) -`, + wantEqual: false, }, { in: func() XY { x := S{nil} @@ -2351,6 +1868,7 @@ func cycleTests() []test { y[0] = y return XY{x, y} }(), + wantEqual: true, }, { in: func() XY { x := S{nil} @@ -2360,12 +1878,7 @@ func cycleTests() []test { y2[0] = y1 return XY{x, y1} }(), - wantDiff: ` - cmp_test.S{ -- {{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}, -+ {{{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}}, - } -`, + wantEqual: false, }, { in: func() XY { x := M{0: nil} @@ -2374,6 +1887,7 @@ func cycleTests() []test { y[0] = y return XY{x, y} }(), + wantEqual: true, }, { in: func() XY { x := M{0: nil} @@ -2383,14 +1897,10 @@ func cycleTests() []test { y2[0] = y1 return XY{x, y1} }(), - wantDiff: ` - cmp_test.M{ -- 0: {0: ⟪0xdeadf00f⟫}, -+ 0: {0: {0: ⟪0xdeadf00f⟫}}, - } -`, + wantEqual: false, }, { - in: XY{makeGraph(), makeGraph()}, + in: XY{makeGraph(), makeGraph()}, + wantEqual: true, }, { in: func() XY { x := makeGraph() @@ -2400,120 +1910,7 @@ func cycleTests() []test { y["Bar"].Bravos["BuzzBarBravo"].ID = 0 return XY{x, y} }(), - wantDiff: ` - 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⟫}}}}}, - }, - }, - }, - } -`, + wantEqual: false, }, { in: func() XY { x := makeGraph() @@ -2524,151 +1921,14 @@ func cycleTests() []test { } return XY{x, y} }(), - wantDiff: ` - 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⟫}}}}}}}}, - } -`, + wantEqual: false, }} { tests = append(tests, test{ - label: label, - x: tt.in.x, - y: tt.in.y, - wantDiff: tt.wantDiff, - reason: tt.reason, + label: label, + x: tt.in.x, + y: tt.in.y, + wantEqual: tt.wantEqual, + reason: tt.reason, }) } return tests @@ -2750,7 +2010,8 @@ func project1Tests() []test { y: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, - opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: true, }, { label: label, x: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { @@ -2759,37 +2020,14 @@ func project1Tests() []test { y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata2"}}, }}}, - opts: []cmp.Option{cmp.Comparer(pb.Equal)}, - wantDiff: ` - 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 - } -`, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantEqual: false, }, { - label: label, - x: createEagle(), - y: createEagle(), - opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + label: label, + x: createEagle(), + y: createEagle(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: true, }, { label: label, x: func() ts.Eagle { @@ -2805,80 +2043,8 @@ func project1Tests() []test { eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices = devs[:1] return eg }(), - opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, - wantDiff: ` - teststructs.Eagle{ - ... // 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: 0x00, - 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 - } -`, + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantEqual: false, }} } @@ -2943,10 +2109,11 @@ func project2Tests() []test { y: createBatch(), wantPanic: "cannot handle unexported field", }, { - label: label, - x: createBatch(), - y: createBatch(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + label: label, + x: createBatch(), + y: createBatch(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, }, { label: label, x: createBatch(), @@ -2956,23 +2123,8 @@ func project2Tests() []test { s[0], s[1], s[2] = s[1], s[2], s[0] return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, - wantDiff: ` - teststructs.GermBatch{ - DirtyGerms: 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 - } -`, + opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, + wantEqual: false, }, { label: label, x: createBatch(), @@ -2982,7 +2134,8 @@ func project2Tests() []test { s[0], s[1], s[2] = s[1], s[2], s[0] return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: true, }, { label: label, x: func() ts.GermBatch { @@ -2997,34 +2150,8 @@ func project2Tests() []test { gb.GermStrain = 22 return gb }(), - opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, - wantDiff: ` - 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: &errors.errorString{s: "EOF"}}, -- 1: nil, -+ 1: &{err: &errors.errorString{s: "unexpected EOF"}}, - 2: &{pb: &testprotos.Dish{Stringer: testprotos.Stringer{X: "dish"}}}, - }, - HasPreviousResult: true, - DirtyID: 10, - CleanID: 0, -- GermStrain: 421, -+ GermStrain: 22, - TotalDirtyGerms: 0, - InfectedAt: s"2009-11-10 23:00:00 +0000 UTC", - } -`, + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantEqual: false, }} } @@ -3073,10 +2200,11 @@ func project3Tests() []test { 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}, + 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 { @@ -3093,26 +2221,8 @@ func project3Tests() []test { }) return d }(), - opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, - wantDiff: ` - teststructs.Dirt{ -- table: &teststructs.MockTable{state: []string{"a", "c"}}, -+ 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 - } -`, + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantEqual: false, }} } @@ -3166,10 +2276,11 @@ func project4Tests() []test { 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)}, + label: label, + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: true, }, { label: label, x: func() ts.Cartel { @@ -3189,49 +2300,8 @@ func project4Tests() []test { d.SetPublicMessage([]byte{1, 2, 4, 3, 5}) return d }(), - opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, - wantDiff: ` - teststructs.Cartel{ - Headquarter: teststructs.Headquarter{ - id: 0x05, - 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"}, - }, - } -`, + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantEqual: false, }} } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs new file mode 100644 index 0000000..5471f81 --- /dev/null +++ b/cmp/testdata/diffs @@ -0,0 +1,1171 @@ +<<< 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: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 2, + ModTime: s"2009-11-11 00:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 4, + ModTime: s"2009-11-11 01:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 8, + ModTime: s"2009-11-11 02:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + Linkname: "", + Uname: "user", + ... // 6 identical fields + }, + { + ... // 4 identical fields + Size: 16, + ModTime: s"2009-11-11 03:00:00 +0000 UTC", +- Typeflag: 0x30, ++ Typeflag: 0x00, + 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( +- 0x00, ++ 0x01, + ))))))) +>>> 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{ +- 0x03e8, 0x07d0, 0x0bb8, ++ 0x0bb8, 0x07d0, 0x03e8, + }, + 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/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: 0x00, + 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: &errors.errorString{s: "EOF"}}, +- 1: nil, ++ 1: &{err: &errors.errorString{s: "unexpected EOF"}}, + 2: &{pb: &testprotos.Dish{Stringer: testprotos.Stringer{X: "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: 0x05, + 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 From 7e5cb83929c528b29e5a8ac1244eab0436f79bce Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba.git@gmail.com> Date: Fri, 15 May 2020 13:39:44 +0900 Subject: [PATCH 36/99] Format units in decimal except bytes (#199) In general, decimal formatting is preferred except for []byte where hexadecimal is preferred for individual elements. Fixes #185 --- cmp/report_compare.go | 17 +++++++++++------ cmp/report_reflect.go | 23 +++++++++++++---------- cmp/report_slices.go | 4 +++- cmp/testdata/diffs | 32 ++++++++++++++++---------------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 17a05ee..d3fa154 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -81,14 +81,19 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { 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, visitedPointers{}) - outy := opts.FormatValue(v.ValueY, visitedPointers{}) + 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() { @@ -101,8 +106,8 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { // Format unequal. assert(opts.DiffMode == diffUnknown) var list textList - outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{}) - outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{}) + 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}) } @@ -111,9 +116,9 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { } return opts.WithTypeMode(emitType).FormatType(v.Type, list) case diffRemoved: - return opts.FormatValue(v.ValueX, visitedPointers{}) + return opts.FormatValue(v.ValueX, withinSlice, visitedPointers{}) case diffInserted: - return opts.FormatValue(v.ValueY, visitedPointers{}) + return opts.FormatValue(v.ValueY, withinSlice, visitedPointers{}) default: panic("invalid diff mode") } diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 2761b62..8f10883 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -74,7 +74,7 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { // 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, m visitedPointers) (out textNode) { +func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visitedPointers) (out textNode) { if !v.IsValid() { return nil } @@ -108,12 +108,15 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t 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.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - // Unnamed uints are usually bytes or words, so use hexadecimal. - if t.PkgPath() == "" || t.Kind() == reflect.Uintptr { + 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: @@ -129,7 +132,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t if value.IsZero(vv) { continue // Elide fields with zero values } - s := opts.WithTypeMode(autoType).FormatValue(vv, m) + s := opts.WithTypeMode(autoType).FormatValue(vv, false, m) list = append(list, textRecord{Key: t.Field(i).Name, Value: s}) } return textWrap{"{", list, "}"} @@ -156,7 +159,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t continue } } - s := opts.WithTypeMode(elideType).FormatValue(vi, m) + s := opts.WithTypeMode(elideType).FormatValue(vi, true, m) list = append(list, textRecord{Value: s}) } return textWrap{ptr + "{", list, "}"} @@ -171,7 +174,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t var list textList for _, k := range value.SortKeys(v.MapKeys()) { sk := formatMapKey(k) - sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), m) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m) list = append(list, textRecord{Key: sk, Value: sv}) } if opts.PrintAddresses { @@ -189,7 +192,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t ptr = formatPointer(v) } skipType = true // Let the underlying value print the type instead - return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), m), ""} + return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), false, m), ""} case reflect.Interface: if v.IsNil() { return textNil @@ -197,7 +200,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out t // 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(), m) + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), false, m) default: panic(fmt.Sprintf("%v kind not handled", v.Kind())) } @@ -209,7 +212,7 @@ func formatMapKey(v reflect.Value) string { var opts formatOptions opts.TypeMode = elideType opts.ShallowPointers = true - s := opts.FormatValue(v, visitedPointers{}).String() + s := opts.FormatValue(v, false, visitedPointers{}).String() return strings.TrimSpace(s) } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index eafcf2e..6f0847e 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -172,7 +172,9 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { 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.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + 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())) diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 5471f81..533edf6 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -54,8 +54,8 @@ ... // 4 identical fields Size: 1, ModTime: s"2009-11-10 23:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, +- Typeflag: 48, ++ Typeflag: 0, Linkname: "", Uname: "user", ... // 6 identical fields @@ -64,8 +64,8 @@ ... // 4 identical fields Size: 2, ModTime: s"2009-11-11 00:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, +- Typeflag: 48, ++ Typeflag: 0, Linkname: "", Uname: "user", ... // 6 identical fields @@ -74,8 +74,8 @@ ... // 4 identical fields Size: 4, ModTime: s"2009-11-11 01:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, +- Typeflag: 48, ++ Typeflag: 0, Linkname: "", Uname: "user", ... // 6 identical fields @@ -84,8 +84,8 @@ ... // 4 identical fields Size: 8, ModTime: s"2009-11-11 02:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, +- Typeflag: 48, ++ Typeflag: 0, Linkname: "", Uname: "user", ... // 6 identical fields @@ -94,8 +94,8 @@ ... // 4 identical fields Size: 16, ModTime: s"2009-11-11 03:00:00 +0000 UTC", -- Typeflag: 0x30, -+ Typeflag: 0x00, +- Typeflag: 48, ++ Typeflag: 0, Linkname: "", Uname: "user", ... // 6 identical fields @@ -188,8 +188,8 @@ >>> TestDiff/Comparer#46 <<< TestDiff/Transformer uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( -- 0x00, -+ 0x01, +- 0, ++ 1, ))))))) >>> TestDiff/Transformer <<< TestDiff/Transformer#02 @@ -323,8 +323,8 @@ + -9, -8, -7, }, UintsA: []uint16{ -- 0x03e8, 0x07d0, 0x0bb8, -+ 0x0bb8, 0x07d0, 0x03e8, +- 1000, 2000, 3000, ++ 3000, 2000, 1000, }, UintsB: []cmp_test.MyUint{ - 4000, 5000, 6000, @@ -1029,7 +1029,7 @@ Slaps: []teststructs.Slap{ { ... // 6 identical fields - Homeland: 0x00, + Homeland: 0, FunnyPrank: "", Immutable: &teststructs.SlapImmutable{ ID: "immutableSlap", @@ -1130,7 +1130,7 @@ <<< TestDiff/Project4#03 teststructs.Cartel{ Headquarter: teststructs.Headquarter{ - id: 0x05, + id: 5, location: "moon", subDivisions: []string{ - "alpha", From aa7c82a3f2093118656c72614a55b7746c369301 Mon Sep 17 00:00:00 2001 From: "A. Ishikawa" Date: Sat, 16 May 2020 21:11:53 -0700 Subject: [PATCH 37/99] Do not use custom format for nil slice (#201) The custom diff output for slices does not accurately reflect the minute differences between a nil slice and an empty slice. Avoid the custom diffing if either value is nil. --- cmp/compare_test.go | 38 ++++++++++++++++++++++++++++ cmp/report_slices.go | 2 ++ cmp/testdata/diffs | 60 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index a08df1b..f305b80 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -905,6 +905,44 @@ 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", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 6f0847e..344cbac 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -28,6 +28,8 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { 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() { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 533edf6..0393e4d 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -439,6 +439,66 @@ ... // 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{ From d08c604e6f3e6b88ac9497ab252e6488f268d260 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 20 May 2020 13:22:30 -0700 Subject: [PATCH 38/99] Permit use of IgnoreFields with unexported fields (#203) Modify IgnoreFields to permit the specification of unexported fields. To avoid a bug with reflect.Type.FieldByName, we disallow forwarding on unexported fields. See https://golang.org/issue/4876 for details. Fixes #196 --- cmp/cmpopts/struct_filter.go | 17 ++++++++----- cmp/cmpopts/util_test.go | 49 +++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index 97f7079..dae7ced 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -160,14 +160,19 @@ func canonicalName(t reflect.Type, sel string) ([]string, error) { // Find the canonical name for this current field name. // If the field exists in an embedded struct, then it will be expanded. + sf, _ := t.FieldByName(name) if !isExported(name) { - // Disallow unexported fields: - // * To discourage people from actually touching unexported fields - // * FieldByName is buggy (https://golang.org/issue/4876) - return []string{name}, fmt.Errorf("name must be exported") + // Avoid using reflect.Type.FieldByName for unexported fields due to + // buggy behavior with regard to embeddeding and unexported fields. + // See https://golang.org/issue/4876 for details. + sf = reflect.StructField{} + for i := 0; i < t.NumField() && sf.Name == ""; i++ { + if t.Field(i).Name == name { + sf = t.Field(i) + } + } } - sf, ok := t.FieldByName(name) - if !ok { + if sf.Name == "" { return []string{name}, fmt.Errorf("does not exist") } var ss []string diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index d0fd888..37704c8 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -771,6 +771,39 @@ func TestOptions(t *testing.T) { opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")}, wantEqual: false, reason: "not equal because highest-level field is not ignored: Foo3", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{})}, + wantEqual: false, + reason: "not equal because unexported fields mismatch", + }, { + label: "IgnoreFields", + x: ParentStruct{ + privateStruct: &privateStruct{private: 1}, + PublicStruct: &PublicStruct{private: 2}, + private: 3, + }, + y: ParentStruct{ + privateStruct: &privateStruct{private: 10}, + PublicStruct: &PublicStruct{private: 20}, + private: 30, + }, + opts: []cmp.Option{ + cmp.AllowUnexported(ParentStruct{}, PublicStruct{}, privateStruct{}), + IgnoreFields(ParentStruct{}, "PublicStruct.private", "privateStruct.private", "private"), + }, + wantEqual: true, + reason: "equal because mismatching unexported fields are ignored", }, { label: "IgnoreTypes", x: []interface{}{5, "same"}, @@ -1192,12 +1225,22 @@ func TestPanic(t *testing.T) { 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(Foo1{}, "unexported"), - wantPanic: "name must be exported", - reason: "unexported fields must not be specified", + args: args(struct{ privateStruct }{}, "private"), + wantPanic: "does not exist", + reason: "private field not permitted since it is a forwarded field that is unexported", }, { label: "IgnoreTypes", fnc: IgnoreTypes, From 4a83f562775624b78b8b83b7492758099439ca10 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 26 May 2020 13:29:20 -0700 Subject: [PATCH 39/99] Optimize Diff for frequent equality (#204) Diff is most often used in tests where the expected outcome is equality (and thus no reported difference). Optimize for this situation by performing a fast-pass equality check and only fall back on constructing a report if inequal. --- cmp/compare.go | 70 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 7ad400f..a58ada5 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -90,30 +90,8 @@ import ( // 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 { - 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() - } - s := newState(opts) - s.compareAny(&pathStep{t, vx, vy}) + s.compareAny(rootStep(x, y)) return s.result.Equal() } @@ -132,15 +110,57 @@ func Equal(x, y interface{}, opts ...Option) bool { // 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) - eq := Equal(x, y, Options(opts), Reporter(r)) + s.reporters = append(s.reporters, reporter{r}) + s.compareAny(rootStep(x, y)) d := r.String() - if (d == "") != eq { + if (d == "") != s.result.Equal() { panic("inconsistent difference and equality results") } return d } +// rootStep constructs the first path step. If x and y have differing types, +// then they are stored within an empty interface type. +func rootStep(x, y interface{}) PathStep { + vx := reflect.ValueOf(x) + vy := reflect.ValueOf(y) + + // If the inputs are different types, auto-wrap them in an empty interface + // so that they have the same parent type. + var t reflect.Type + if !vx.IsValid() || !vy.IsValid() || vx.Type() != vy.Type() { + t = 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. From 1776240f8f841dfa00cb72d811301dbb0298f983 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 8 Jun 2020 14:41:23 -0700 Subject: [PATCH 40/99] Forcibly export fields for use by the reporter The choice to avoid traversing unexported fields by default makes sense for the semantics of normal comparisons. However, the goal of the reporter is to prioritize humanly readable output. As such, it seems appropriate to forcibly export fields. This allows the String method to be called on such values. --- cmp/report_reflect.go | 7 ++++++- cmp/testdata/diffs | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 8f10883..7e965b6 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -127,13 +127,18 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit 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) + } s := opts.WithTypeMode(autoType).FormatValue(vv, false, m) - list = append(list, textRecord{Key: t.Field(i).Name, Value: s}) + list = append(list, textRecord{Key: sf.Name, Value: s}) } return textWrap{"{", list, "}"} case reflect.Slice: diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 0393e4d..56a9a32 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1154,10 +1154,10 @@ CleanGerms: nil, GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"}, DishMap: map[int32]*teststructs.Dish{ - 0: &{err: &errors.errorString{s: "EOF"}}, + 0: &{err: e"EOF"}, - 1: nil, -+ 1: &{err: &errors.errorString{s: "unexpected EOF"}}, - 2: &{pb: &testprotos.Dish{Stringer: testprotos.Stringer{X: "dish"}}}, ++ 1: &{err: e"unexpected EOF"}, + 2: &{pb: s"dish"}, }, HasPreviousResult: true, DirtyID: 10, From 11c4583a280337c7ac34c591b4321552981a7cc0 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 8 Jun 2020 16:37:31 -0700 Subject: [PATCH 41/99] Avoid leaking implementation details of the exporter (#206) The current implementation for forcibly exporting fields relies on the reflect.Value.Addr method for the parent struct value, where it copies a non-addressable struct onto to the heap so that it is addressable. However, this action leaks a subtle detail of how the internal implementation works since the accessed field for within a struct is only addressable if and only if the parent struct is also addressable. Modify the implementation to avoid leaking this implementation detail by shallow copying the accessed unexported field to remove any notion of addressability if the parent struct is also unaddressable. Fixes #181 --- cmp/compare.go | 3 +++ cmp/compare_test.go | 29 +++++++++++++++++++++++++++++ cmp/export_panic.go | 2 +- cmp/export_unsafe.go | 17 ++++++++++++----- cmp/path.go | 7 ++++--- cmp/report_reflect.go | 2 +- 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index a58ada5..c82c062 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -386,6 +386,7 @@ func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { } 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 @@ -407,6 +408,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { // 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) } @@ -417,6 +419,7 @@ func (s *state) compareStruct(t reflect.Type, vx, vy reflect.Value) { mayForceInit = true } step.mayForce = mayForce + step.paddr = addr step.pvx = vax step.pvy = vay step.field = t.Field(i) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index f305b80..a70103f 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -624,6 +624,35 @@ func comparerTests() []test { 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 }{new(bytes.Buffer)}, + 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", }} } diff --git a/cmp/export_panic.go b/cmp/export_panic.go index dd03235..dfa5d21 100644 --- a/cmp/export_panic.go +++ b/cmp/export_panic.go @@ -10,6 +10,6 @@ import "reflect" const supportExporters = false -func retrieveUnexportedField(reflect.Value, reflect.StructField) reflect.Value { +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 index 57020e2..de59478 100644 --- a/cmp/export_unsafe.go +++ b/cmp/export_unsafe.go @@ -17,9 +17,16 @@ const supportExporters = true // a struct such that the value has read-write permissions. // // The parent struct, v, must be addressable, while f must be a StructField -// describing the field to retrieve. -func retrieveUnexportedField(v reflect.Value, f reflect.StructField) reflect.Value { - // See https://github.com/google/go-cmp/issues/167 for discussion of the - // following expression. - return reflect.NewAt(f.Type, unsafe.Pointer(uintptr(unsafe.Pointer(v.UnsafeAddr()))+f.Offset)).Elem() +// 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. + ve = reflect.ValueOf(ve.Interface()).Convert(f.Type) + } + return ve } diff --git a/cmp/path.go b/cmp/path.go index 509d6b8..603dbb0 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -177,7 +177,8 @@ type structField struct { // pvx, pvy, and field are only valid if unexported is true. unexported bool mayForce bool // Forcibly allow visibility - pvx, pvy reflect.Value // Parent values + paddr bool // Was parent addressable? + pvx, pvy reflect.Value // Parent values (always addressible) field reflect.StructField // Field information } @@ -189,8 +190,8 @@ func (sf StructField) Values() (vx, vy reflect.Value) { // Forcibly obtain read-write access to an unexported struct field. if sf.mayForce { - vx = retrieveUnexportedField(sf.pvx, sf.field) - vy = retrieveUnexportedField(sf.pvy, sf.field) + 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 diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 7e965b6..642622e 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -135,7 +135,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit } sf := t.Field(i) if supportExporters && !isExported(sf.Name) { - vv = retrieveUnexportedField(v, sf) + vv = retrieveUnexportedField(v, sf, true) } s := opts.WithTypeMode(autoType).FormatValue(vv, false, m) list = append(list, textRecord{Key: sf.Name, Value: s}) From 23a2b5646fe0b6a0b4b19b6ef0b0965b182f2e83 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 9 Jun 2020 00:21:01 -0700 Subject: [PATCH 42/99] Fix exporter to handle nil interface values (#207) A shallow copy with reflect.ValueOf(v.Interface()) does not work if v is a nil interface value. Special case the edge case by checking for a nil value and create a new one use reflect.Zero. --- cmp/compare_test.go | 2 +- cmp/export_unsafe.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index a70103f..4ffa0eb 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -627,7 +627,7 @@ func comparerTests() []test { }, { label: label, x: struct{ s fmt.Stringer }{new(bytes.Buffer)}, - y: 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 { diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go index de59478..351f1a3 100644 --- a/cmp/export_unsafe.go +++ b/cmp/export_unsafe.go @@ -26,7 +26,10 @@ func retrieveUnexportedField(v reflect.Value, f reflect.StructField, addr bool) // 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. - ve = reflect.ValueOf(ve.Interface()).Convert(f.Type) + if ve.Kind() == reflect.Interface && ve.IsNil() { + return reflect.Zero(f.Type) + } + return reflect.ValueOf(ve.Interface()).Convert(f.Type) } return ve } From 367e530b4ed74b514889e7945f0b18145337eff3 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 10:36:17 -0700 Subject: [PATCH 43/99] Mention minimally supported Go version in TODO (#209) Specify the exact minimumally supported version of Go required in order to address certain TODOs. This makes it easier to filter out inactionable TODOs. --- cmp/cmpopts/equate.go | 2 +- cmp/compare.go | 2 +- cmp/compare_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index e102849..8667908 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -151,6 +151,6 @@ func areConcreteErrors(x, y interface{}) bool { 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. + // TODO(≥go1.13): Use standard definition of errors.Is. return xerrors.Is(xe, ye) || xerrors.Is(ye, xe) } diff --git a/cmp/compare.go b/cmp/compare.go index c82c062..580ae20 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -376,7 +376,7 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { // 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). + // TODO(≥go1.10): 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() diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 4ffa0eb..43ac8f9 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1068,7 +1068,7 @@ func embeddedTests() []test { return s } - // TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122). + // TODO(≥go1.10): Workaround for reflect bug (https://golang.org/issue/21122). wantPanicNotGo110 := func(s string) string { if !flags.AtLeastGo110 { return "" From 9b300311a803504267fb9fc5040a430aa7013956 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 11:00:30 -0700 Subject: [PATCH 44/99] Batch reporter output for simple lists of textLine elements (#208) Rather than having a single element on each line, which hurts readability due to the need for scrolling quite a bit. Batch multiple elements for simple lists to be on a single line. Fixes #170 --- cmp/compare_test.go | 5 +++++ cmp/report_text.go | 38 +++++++++++++++++++++++++++++++++++++- cmp/testdata/diffs | 15 +++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 43ac8f9..22cf153 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -833,6 +833,11 @@ func reporterTests() []test { 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 + "/BatchedLong", + x: MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127}}, + wantEqual: false, + reason: "batched output desired for a single slice of primitives unique to one of the inputs", }, { label: label, x: MyComposite{ diff --git a/cmp/report_text.go b/cmp/report_text.go index 8b8fcab..7849d65 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -16,6 +16,8 @@ import ( var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 +const maxColumnLength = 80 + type indentMode int func (n indentMode) appendIndent(b []byte, d diffMode) []byte { @@ -221,7 +223,7 @@ func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { } // Force multi-lined output when printing a removed/inserted node that // is sufficiently long. - if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 { + if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength { multiLine = true } if !multiLine { @@ -246,6 +248,40 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { func(r textRecord) int { return len(r.Value.(textLine)) }, ) + // Format lists of simple lists in a batched form. + // If the list is sequence of only textLine values, + // then batch multiple values on a single line. + var isSimple bool + for _, r := range s { + _, isLine := r.Value.(textLine) + isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil + if !isSimple { + break + } + } + if isSimple { + n++ + var batch []byte + emitBatch := func() { + if len(batch) > 0 { + b = n.appendIndent(append(b, '\n'), d) + b = append(b, bytes.TrimRight(batch, " ")...) + batch = batch[:0] + } + } + for _, r := range s { + line := r.Value.(textLine) + if len(batch)+len(line)+len(", ") > maxColumnLength { + emitBatch() + } + batch = append(batch, line...) + batch = append(batch, ", "...) + } + emitBatch() + n-- + return n.appendIndent(append(b, '\n'), d) + } + // Format the list as a multi-lined output. n++ for i, r := range s { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 56a9a32..2e556c9 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -294,6 +294,21 @@ ... // 6 identical fields } >>> TestDiff/Reporter#01 +<<< TestDiff/Reporter/BatchedLong + interface{}( +- cmp_test.MyComposite{ +- IntsA: []int8{ +- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, +- 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, +- 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, +- 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, +- 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, +- 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, +- 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, +- }, +- }, + ) +>>> TestDiff/Reporter/BatchedLong <<< TestDiff/Reporter#02 cmp_test.MyComposite{ StringA: "", From 88849e8bc9a647e7dde31569a79f5ea1f624ef2b Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 11:08:11 -0700 Subject: [PATCH 45/99] Allow batched diffing of slices with a custom comparer (#210) For correctness, cmp checks applicability of the options for every element in a slice. For large []byte, this is a significant performance detriment. The workaround is to specify Comparer(bytes.Equal). However, we would still like to have the batched diffing if the slices are different. Specialize for this situation. --- cmp/compare_test.go | 9 +++++++++ cmp/report_slices.go | 18 +++++++++++++++--- cmp/testdata/diffs | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 22cf153..a7f78e8 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -833,6 +833,15 @@ func reporterTests() []test { y: MyComposite{IntsA: []int8{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, wantEqual: false, reason: "batched diffing desired since many elements differ", + }, { + label: label + "/BatchedWithComparer", + x: MyComposite{BytesA: []byte{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, + y: MyComposite{BytesA: []byte{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, + wantEqual: false, + opts: []cmp.Option{ + cmp.Comparer(bytes.Equal), + }, + reason: "batched diffing desired since many elements differ", }, { label: label + "/BatchedLong", x: MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127}}, diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 344cbac..356786f 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -23,13 +23,25 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { 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 + case v.NumIgnored > 0: + return false // Some ignore option was used + case v.NumTransformed > 0: + return false // Some transform option was used + case v.NumCompared > 1: + return false // More than one comparison was used + case v.NumCompared == 1 && v.Type.Name() != "": + // The need for cmp to check applicability of options on every element + // in a slice is a significant performance detriment for large []byte. + // The workaround is to specify Comparer(bytes.Equal), + // which enables cmp to compare []byte more efficiently. + // If they differ, we still want to provide batched diffing. + // The logic disallows named types since they tend to have their own + // String method, with nicer formatting than what this provides. + return false } switch t := v.Type; t.Kind() { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 2e556c9..4d8e71d 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -294,6 +294,22 @@ ... // 6 identical fields } >>> TestDiff/Reporter#01 +<<< TestDiff/Reporter/BatchedWithComparer + cmp_test.MyComposite{ + StringA: "", + StringB: "", + BytesA: []uint8{ +- 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, // -|.......| ++ 0x0c, 0x1d, 0x0d, 0x1b, 0x16, 0x17, // +|......| + 0x11, 0x12, 0x13, 0x14, 0x15, // |.....| +- 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // -|........| ++ 0x0a, 0x1a, 0x10, 0x19, 0x1c, 0x0b, 0x0f, 0x18, 0x0e, // +|.........| + }, + BytesB: nil, + BytesC: nil, + ... // 9 identical fields + } +>>> TestDiff/Reporter/BatchedWithComparer <<< TestDiff/Reporter/BatchedLong interface{}( - cmp_test.MyComposite{ From a171aa74446ac6ce47f4f09b10deb7d9afc7dc20 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 11:11:33 -0700 Subject: [PATCH 46/99] Use raw string literal syntax only for valid UTF-8 (#211) --- cmp/compare_test.go | 5 +++++ cmp/report_reflect.go | 3 ++- cmp/testdata/diffs | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index a7f78e8..7f0a395 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -822,6 +822,11 @@ func reporterTests() []test { ) return []test{{ + label: "/InvalidUTF8", + x: MyString("\xed\xa0\x80"), + wantEqual: false, + reason: "invalid UTF-8 should format as quoted string", + }, { 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}}, diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 642622e..9bc5eb3 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "unicode" + "unicode/utf8" "github.com/google/go-cmp/cmp/internal/flags" "github.com/google/go-cmp/cmp/internal/value" @@ -235,7 +236,7 @@ func formatString(s string) string { rawInvalid := func(r rune) bool { return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t') } - if strings.IndexFunc(s, rawInvalid) < 0 { + if utf8.ValidString(s) && strings.IndexFunc(s, rawInvalid) < 0 { return "`" + s + "`" } return qs diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 4d8e71d..2be80af 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -258,6 +258,11 @@ })), } >>> TestDiff/Transformer#05 +<<< TestDiff//InvalidUTF8 + interface{}( +- cmp_test.MyString("\xed\xa0\x80"), + ) +>>> TestDiff//InvalidUTF8 <<< TestDiff/Reporter cmp_test.MyComposite{ ... // 3 identical fields From 0cd6169de14f4f3dc7550a908d8e4a5c69f85fd2 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 12:12:46 -0700 Subject: [PATCH 47/99] Use custom triple-quote syntax for diffing string literals (#212) Using strings.Join to denote differences in a multi-line string is visually noisy due to extensive use of quotes and escape sequences. Add a custom triple-quote syntax that unambiguously shows line differences with less visual noise. If the triple-quote syntax cannot unmabiguously show differences, then the reporter falls back on using the strings.Join format, which is never ambiguous. Fixes #195 --- cmp/compare_test.go | 72 +++++++++++++++ cmp/report_slices.go | 77 +++++++++++++++- cmp/report_text.go | 15 ++-- cmp/testdata/diffs | 210 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 8 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 7f0a395..7a21455 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -902,6 +902,78 @@ func reporterTests() []test { y: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, wantEqual: false, reason: "batched textual diff desired since bytes looks like textual data", + }, { + label: label + "/TripleQuote", + x: MyComposite{StringA: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + y: MyComposite{StringA: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n"}, + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/TripleQuoteSlice", + x: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []string{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of strings", + }, { + label: label + "/TripleQuoteNamedTypes", + x: MyComposite{ + StringB: MyString("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + y: MyComposite{ + StringB: MyString("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + BytesC: MyBytes("aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nSSS\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz"), + }, + wantEqual: false, + reason: "use triple-quote syntax for named types", + }, { + label: label + "/TripleQuoteSliceNamedTypes", + x: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + y: []MyString{ + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + }, + wantEqual: false, + reason: "use triple-quote syntax for slices of named strings", + }, { + label: label + "/TripleQuoteEndlines", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n\r", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\r\nhhh\n\riii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteAmbiguousQuotes", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\n\"\"\"\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous triple quotes", + }, { + label: label + "/AvoidTripleQuoteAmbiguousEllipsis", + x: "aaa\nbbb\nccc\n...\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to presence of ambiguous ellipsis", + }, { + label: label + "/AvoidTripleQuoteNonPrintable", + x: "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nCCC\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\no\roo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "use triple-quote syntax", + }, { + label: label + "/AvoidTripleQuoteIdenticalWhitespace", + x: "aaa\nbbb\nccc\n ddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + wantEqual: false, + reason: "avoid triple-quote syntax due to visual equivalence of differences", }, { label: label, x: MyComposite{ diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 356786f..3da92bc 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "reflect" + "strconv" "strings" "unicode" "unicode/utf8" @@ -96,7 +97,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } if isText || isBinary { var numLines, lastLineIdx, maxLineLen int - isBinary = false + isBinary = !utf8.ValidString(sx) || !utf8.ValidString(sy) for i, r := range sx + sy { if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError { isBinary = true @@ -131,6 +132,78 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { }, ) delim = "\n" + + // If possible, use a custom triple-quote (""") syntax for printing + // differences in a string literal. This format is more readable, + // but has edge-cases where differences are visually indistinguishable. + // This format is avoided under the following conditions: + // • A line starts with `"""` + // • A line starts with "..." + // • A line contains non-printable characters + // • Adjacent different lines differ only by whitespace + // + // For example: + // """ + // ... // 3 identical lines + // foo + // bar + // - baz + // + BAZ + // """ + isTripleQuoted := true + prevDiffLines := map[string]bool{} + var list2 textList + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + for _, r := range list { + if !r.Value.Equal(textEllipsis) { + line, _ := strconv.Unquote(string(r.Value.(textLine))) + line = strings.TrimPrefix(strings.TrimSuffix(line, "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + normLine := strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 // drop whitespace to avoid visually indistinguishable output + } + return r + }, line) + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + isTripleQuoted = isTripleQuoted && + !strings.HasPrefix(line, `"""`) && + !strings.HasPrefix(line, "...") && + strings.TrimFunc(line, isPrintable) == "" && + (r.Diff == 0 || !prevDiffLines[normLine]) + if !isTripleQuoted { + break + } + r.Value = textLine(line) + r.ElideComma = true + prevDiffLines[normLine] = true + } + if r.Diff == 0 { + prevDiffLines = map[string]bool{} // start a new non-adjacent difference group + } + list2 = append(list2, r) + } + if r := list2[len(list2)-1]; r.Diff == diffIdentical && len(r.Value.(textLine)) == 0 { + list2 = list2[:len(list2)-1] // elide single empty line at the end + } + list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) + if isTripleQuoted { + var out textNode = textWrap{"(", list2, ")"} + switch t.Kind() { + case reflect.String: + if t != reflect.TypeOf(string("")) { + out = opts.FormatType(t, out) + } + case reflect.Slice: + // Always emit type for slices since the triple-quote syntax + // looks like a string (not a slice). + opts = opts.WithTypeMode(emitType) + out = opts.FormatType(t, out) + } + return out + } + // If the text appears to be single-lined text, // then perform differencing in approximately fixed-sized chunks. // The output is printed as quoted strings. @@ -143,6 +216,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { }, ) delim = "" + // If the text appears to be binary data, // then perform differencing in approximately fixed-sized chunks. // The output is inspired by hexdump. @@ -159,6 +233,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { 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. diff --git a/cmp/report_text.go b/cmp/report_text.go index 7849d65..d3a3084 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -138,10 +138,11 @@ func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { // 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" + Diff diffMode // e.g., 0 or '-' or '+' + Key string // e.g., "MyField" + Value textNode // textWrap | textLine + ElideComma bool // avoid trailing comma + Comment fmt.Stringer // e.g., "6 identical fields" } // AppendEllipsis appends a new ellipsis node to the list if none already @@ -151,9 +152,9 @@ 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}) + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) } else { - *s = append(*s, textRecord{Value: textEllipsis}) + *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true}) } return } @@ -292,7 +293,7 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { b = alignKeyLens[i].appendChar(b, ' ') b = r.Value.formatExpandedTo(b, d|r.Diff, n) - if !r.Value.Equal(textEllipsis) { + if !r.ElideComma { b = append(b, ',') } b = alignValueLens[i].appendChar(b, ' ') diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 2be80af..60fcbb8 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -449,6 +449,216 @@ ... // 9 identical fields } >>> TestDiff/Reporter#05 +<<< TestDiff/Reporter/TripleQuote + cmp_test.MyComposite{ + StringA: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 6 identical lines + """ + ), + StringB: "", + BytesA: nil, + ... // 11 identical fields + } +>>> TestDiff/Reporter/TripleQuote +<<< TestDiff/Reporter/TripleQuoteSlice + []string{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + } +>>> TestDiff/Reporter/TripleQuoteSlice +<<< TestDiff/Reporter/TripleQuoteNamedTypes + cmp_test.MyComposite{ + StringA: "", + StringB: ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + BytesA: nil, + BytesB: nil, + BytesC: cmp_test.MyBytes( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR +- sss ++ rrr ++ SSS + ttt + uuu + ... // 5 identical lines + """ + ), + IntsA: nil, + IntsB: nil, + ... // 7 identical fields + } +>>> TestDiff/Reporter/TripleQuoteNamedTypes +<<< TestDiff/Reporter/TripleQuoteSliceNamedTypes + []cmp_test.MyString{ + ( + """ + ... // 23 identical lines + xxx + yyy +- zzz + """ + ), + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + } +>>> TestDiff/Reporter/TripleQuoteSliceNamedTypes +<<< TestDiff/Reporter/TripleQuoteEndlines + ( + """ + aaa + bbb +- ccc ++ CCC + ddd + eee + ... // 10 identical lines + ppp + qqq +- RRR ++ rrr + sss + ttt + ... // 4 identical lines + yyy + zzz +- + """ + ) +>>> TestDiff/Reporter/TripleQuoteEndlines +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", +- "fff", ++ `"""`, + "ggg", + "hhh", + ... // 7 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousQuotes +<<< TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis + strings.Join({ + "aaa", + "bbb", +- "ccc", +- "...", ++ "CCC", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteAmbiguousEllipsis +<<< TestDiff/Reporter/AvoidTripleQuoteNonPrintable + strings.Join({ + "aaa", + "bbb", +- "ccc", ++ "CCC", + "ddd", + "eee", + ... // 7 identical lines + "mmm", + "nnn", +- "ooo", ++ "o\roo", + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteNonPrintable +<<< TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace + strings.Join({ + "aaa", + "bbb", +- "ccc", +- " ddd", ++ "ccc ", ++ "ddd", + "eee", + "fff", + ... // 9 identical lines + "ppp", + "qqq", +- "RRR", ++ "rrr", + "sss", + "ttt", + ... // 7 identical lines + }, "\n") +>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace <<< TestDiff/Reporter#06 cmp_test.MyComposite{ StringA: strings.Join({ From 7c9a834557ca73ca54b2f367316f4bd747217741 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 16:44:26 -0700 Subject: [PATCH 48/99] Introduce deliberate instability to difference output (#214) The reporter output is documented as unstable. The API for custom reporters also specifies that the diffing of slices is unstable. Introduce deliberate instability to the diffing algorithm so that we have the flexibility to improve it in the future. The current algorithm optimizes for speed, rather than optimality, so there is much room for improvement. --- cmp/internal/diff/diff.go | 22 +++++++++++++++++++++- cmp/internal/diff/diff_test.go | 6 ++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 3d2e426..730e223 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -12,6 +12,13 @@ // is more important than obtaining a minimal Levenshtein distance. package diff +import ( + "math/rand" + "time" + + "github.com/google/go-cmp/cmp/internal/flags" +) + // EditType represents a single operation within an edit-script. type EditType uint8 @@ -112,6 +119,8 @@ func (r Result) Similar() bool { return r.NumSame+1 >= r.NumDiff } +var randInt = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) + // Difference reports whether two lists of lengths nx and ny are equal // given the definition of equality provided as f. // @@ -159,6 +168,17 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // 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. + // To ensure flexibility in changing the algorithm in the future, + // introduce some degree of deliberate instability. + // This is achieved by fiddling the zigzag iterator to start searching + // the graph starting from the bottom-right versus than the top-left. + // The result may differ depending on the starting search location, + // but still produces a valid edit script. + zigzagInit := randInt // either 0 or 1 + if flags.Deterministic { + zigzagInit = 0 + } + // Invariants: // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny @@ -209,7 +229,7 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { break } - for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + for stop1, stop2, i := false, false, zigzagInit; !(stop1 && stop2) && searchBudget > 0; i++ { // Search in a diagonal pattern for a match. z := zigzag(i) p := point{fwdFrontier.X + z, fwdFrontier.Y - z} diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go index ef39077..35e8642 100644 --- a/cmp/internal/diff/diff_test.go +++ b/cmp/internal/diff/diff_test.go @@ -10,8 +10,14 @@ import ( "strings" "testing" "unicode" + + "github.com/google/go-cmp/cmp/internal/flags" ) +func init() { + flags.Deterministic = true +} + func TestDifference(t *testing.T) { tests := []struct { // Before passing x and y to Difference, we strip all spaces so that From 0d296f9f534978cc25de69216b23b74bbc10fad9 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 10 Jun 2020 16:51:30 -0700 Subject: [PATCH 49/99] Limit number of printed differences for variable-length composites (#213) For large slices, arrays, and maps, the reporter can be unreadable if there are many differences. Limit the number of results to some reasonable maximum. --- cmp/compare_test.go | 28 +++++++++++ cmp/report_compare.go | 12 ++++- cmp/report_slices.go | 16 +++++- cmp/report_text.go | 7 ++- cmp/testdata/diffs | 113 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 3 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 7a21455..d23c31f 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -974,6 +974,34 @@ func reporterTests() []test { y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", wantEqual: false, reason: "avoid triple-quote syntax due to visual equivalence of differences", + }, { + label: label + "/LimitMaximumBytesDiffs", + x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="), + y: []byte("\\====|\x96\xe7SB\xa0\xab=====\xf0\xbd\xa5q\xab\x17;======\xabP\x00=======\xeb====\xa5\x14\xe6O(\xe4=====(======/c@?===========\xd9x\xed\x13=====J\xfc\x918B\x8d======a8A\xebs\x04\xae=======\aC====\x1c===========\x91\"=======uؾ====s\xec\x845\a=====;\xabS9t======\x1f\x1b=======\x80\xab/\xed+:;====\xeaI===========\xabl=======\xb9\xe9\xfdH\x93\x8e\u007f====ח\xe5=====Ig\x88m\xf5\x01V=============\xf7+4\xb0\x92E====\x9fj\xf8&\xd0h\xf9=====\xeeΨ\r\xbf============="), + wantEqual: false, + reason: "total bytes difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumStringDiffs", + x: "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", + y: "aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", + wantEqual: false, + reason: "total string difference output is truncated due to excessive number of differences", + }, { + label: label + "/LimitMaximumSliceDiffs", + x: func() (out []struct{ S string }) { + for _, s := range strings.Split("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + y: func() (out []struct{ S string }) { + for _, s := range strings.Split("aa\nb\ncc\nd\nee\nf\ngg\nh\nii\nj\nkk\nl\nmm\nn\noo\np\nqq\nr\nss\nt\nuu\nv\nww\nx\nyy\nz\nAA\nB\nCC\nD\nEE\nF\nGG\nH\nII\nJ\nKK\nL\nMM\nN\nOO\nP\nQQ\nR\nSS\nT\nUU\nV\nWW\nX\nYY\nZ\n", "\n") { + out = append(out, struct{ S string }{s}) + } + return out + }(), + wantEqual: false, + reason: "total slice difference output is truncated due to excessive number of differences", }, { label: label, x: MyComposite{ diff --git a/cmp/report_compare.go b/cmp/report_compare.go index d3fa154..8ecdd28 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -207,7 +207,13 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te // Handle differencing. var list textList groups := coalesceAdjacentRecords(name, recs) + maxGroup := diffStats{Name: name} for i, ds := range groups { + if len(list) >= maxDiffElements { + maxGroup = maxGroup.Append(ds) + continue + } + // Handle equal records. if ds.NumDiff() == 0 { // Compute the number of leading and trailing records to print. @@ -268,7 +274,11 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te } recs = recs[ds.NumDiff():] } - assert(len(recs) == 0) + if maxGroup.IsZero() { + assert(len(recs) == 0) + } else { + list.AppendEllipsis(maxGroup) + } return textWrap{"{", list, "}"} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 3da92bc..7a35f39 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -16,6 +16,10 @@ import ( "github.com/google/go-cmp/cmp/internal/diff" ) +// maxDiffElements is the maximum number of difference elements to format +// before the remaining differences are coalesced together. +const maxDiffElements = 32 + // CanFormatDiffSlice reports whether we support custom formatting for nodes // that are slices of primitive kinds or strings. func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { @@ -335,7 +339,13 @@ func (opts formatOptions) formatDiffSlice( groups := coalesceAdjacentEdits(name, es) groups = coalesceInterveningIdentical(groups, chunkSize/4) + maxGroup := diffStats{Name: name} for i, ds := range groups { + if len(list) >= maxDiffElements { + maxGroup = maxGroup.Append(ds) + continue + } + // Print equal. if ds.NumDiff() == 0 { // Compute the number of leading and trailing equal bytes to print. @@ -369,7 +379,11 @@ func (opts formatOptions) formatDiffSlice( 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) + if maxGroup.IsZero() { + assert(vx.Len() == 0 && vy.Len() == 0) + } else { + list.AppendEllipsis(maxGroup) + } return list } diff --git a/cmp/report_text.go b/cmp/report_text.go index d3a3084..17a376e 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -149,7 +149,7 @@ type textRecord struct { // 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{} + hasStats := !ds.IsZero() if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { if hasStats { *s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds}) @@ -369,6 +369,11 @@ type diffStats struct { NumModified int } +func (s diffStats) IsZero() bool { + s.Name = "" + return s == diffStats{} +} + func (s diffStats) NumDiff() int { return s.NumRemoved + s.NumInserted + s.NumModified } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 60fcbb8..703ea5f 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -659,6 +659,119 @@ ... // 7 identical lines }, "\n") >>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace +<<< TestDiff/Reporter/LimitMaximumBytesDiffs + []uint8{ +- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| ++ 0x5c, 0x3d, 0x3d, 0x3d, 0x3d, 0x7c, 0x96, 0xe7, 0x53, 0x42, 0xa0, 0xab, // +|\====|..SB..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x1d, 0xdf, 0x61, 0xae, 0x98, 0x9f, 0x48, // -|..a...H| ++ 0xf0, 0xbd, 0xa5, 0x71, 0xab, 0x17, 0x3b, // +|...q..;| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0xc7, 0xb0, 0xb7, // -|...| ++ 0xab, 0x50, 0x00, // +|.P.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0xef, 0x3d, 0x3d, 0x3d, 0x3d, 0x3a, 0x5c, 0x94, 0xe6, 0x4a, 0xc7, // -|.====:\..J.| ++ 0xeb, 0x3d, 0x3d, 0x3d, 0x3d, 0xa5, 0x14, 0xe6, 0x4f, 0x28, 0xe4, // +|.====...O(.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0xb4, // -|.| ++ 0x28, // +|(| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x0a, 0x0a, 0xf7, 0x94, // -|....| ++ 0x2f, 0x63, 0x40, 0x3f, // +|/c@?| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xf2, 0x9c, 0xc0, 0x66, // -|...f| ++ 0xd9, 0x78, 0xed, 0x13, // +|.x..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x34, 0xf6, 0xf1, 0xc3, 0x17, 0x82, // -|4.....| ++ 0x4a, 0xfc, 0x91, 0x38, 0x42, 0x8d, // +|J..8B.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x6e, 0x16, 0x60, 0x91, 0x44, 0xc6, 0x06, // -|n.`.D..| ++ 0x61, 0x38, 0x41, 0xeb, 0x73, 0x04, 0xae, // +|a8A.s..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x1c, 0x45, 0x3d, 0x3d, 0x3d, 0x3d, 0x2e, // -|.E====.| ++ 0x07, 0x43, 0x3d, 0x3d, 0x3d, 0x3d, 0x1c, // +|.C====.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xc4, 0x18, // -|..| ++ 0x91, 0x22, // +|."| + ... // 95 identical, 61 removed, and 61 inserted bytes + } +>>> TestDiff/Reporter/LimitMaximumBytesDiffs +<<< TestDiff/Reporter/LimitMaximumStringDiffs + ( + """ +- a ++ aa + b +- c ++ cc + d +- e ++ ee + f +- g ++ gg + h +- i ++ ii + j +- k ++ kk + l +- m ++ mm + n +- o ++ oo + p +- q ++ qq + r +- s ++ ss + t +- u ++ uu + ... // 17 identical, 15 removed, and 15 inserted lines + """ + ) +>>> TestDiff/Reporter/LimitMaximumStringDiffs +<<< TestDiff/Reporter/LimitMaximumSliceDiffs + []struct{ S string }{ +- {S: "a"}, ++ {S: "aa"}, + {S: "b"}, +- {S: "c"}, ++ {S: "cc"}, + {S: "d"}, +- {S: "e"}, ++ {S: "ee"}, + {S: "f"}, +- {S: "g"}, ++ {S: "gg"}, + {S: "h"}, +- {S: "i"}, ++ {S: "ii"}, + {S: "j"}, +- {S: "k"}, ++ {S: "kk"}, + {S: "l"}, +- {S: "m"}, ++ {S: "mm"}, + {S: "n"}, +- {S: "o"}, ++ {S: "oo"}, + {S: "p"}, +- {S: "q"}, ++ {S: "qq"}, + {S: "r"}, +- {S: "s"}, ++ {S: "ss"}, + {S: "t"}, +- {S: "u"}, ++ {S: "uu"}, + ... // 17 identical and 15 modified elements + } +>>> TestDiff/Reporter/LimitMaximumSliceDiffs <<< TestDiff/Reporter#06 cmp_test.MyComposite{ StringA: strings.Join({ From f1780cfdde930250f45fbe0bb6e107be5b4e9514 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 11 Jun 2020 18:28:52 -0700 Subject: [PATCH 50/99] Limit verbosity of reporter output (#215) A common complaint is that the reporter it prints out too much irrelevant information, resulting in a low signal-to-noise ratio. Improve this metric by imposing a verbosity limit. For nodes that are equal, we set the verbosity level is a lower value than when the nodes are inequal. Other minor changes: * Adjust heuristic for triple-quote usage to operate on more cases. * Elide type more aggressively for equal nodes. * Printing the address for a slice includes the length and capacity. * The pointed-at value for map keys are printed. --- cmp/compare_test.go | 4 +- cmp/example_test.go | 2 +- cmp/report_compare.go | 46 +++++- cmp/report_reflect.go | 79 +++++++++-- cmp/report_slices.go | 38 +++-- cmp/testdata/diffs | 315 ++++++++++++++++++++---------------------- 6 files changed, 287 insertions(+), 197 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index d23c31f..11979d0 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -159,8 +159,8 @@ func TestDiff(t *testing.T) { } } else { wantDiff := wantDiffs[t.Name()] - if gotDiff != wantDiff { - t.Fatalf("Diff:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, wantDiff, tt.reason) + if diff := cmp.Diff(wantDiff, gotDiff); diff != "" { + t.Fatalf("Diff:\ngot:\n%s\nwant:\n%s\ndiff: (-want +got)\n%s\nreason: %v", gotDiff, wantDiff, diff, tt.reason) } } gotEqual := gotDiff == "" diff --git a/cmp/example_test.go b/cmp/example_test.go index 2689efb..d4f7391 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -37,7 +37,7 @@ func ExampleDiff_testing() { // SSID: "CoffeeShopWiFi", // - IPAddress: s"192.168.0.2", // + IPAddress: s"192.168.0.1", - // NetMask: net.IPMask{0xff, 0xff, 0x00, 0x00}, + // NetMask: {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"}, diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 8ecdd28..2ac3cc6 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -11,10 +11,6 @@ import ( "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? @@ -71,10 +67,31 @@ func (opts formatOptions) WithTypeMode(t typeMode) formatOptions { opts.TypeMode = t return opts } +func (opts formatOptions) WithVerbosity(level int) formatOptions { + opts.VerbosityLevel = level + opts.LimitVerbosity = true + return opts +} +func (opts formatOptions) verbosity() uint { + switch { + case opts.VerbosityLevel < 0: + return 0 + case opts.VerbosityLevel > 16: + return 16 // some reasonable maximum to avoid shift overflow + default: + return uint(opts.VerbosityLevel) + } +} // 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 { + if opts.DiffMode == diffIdentical { + opts = opts.WithVerbosity(1) + } else { + opts = opts.WithVerbosity(3) + } + // Check whether we have specialized formatting for this node. // This is not necessary, but helpful for producing more readable outputs. if opts.CanFormatDiffSlice(v) { @@ -124,6 +141,8 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { } } + // TODO: Print cycle reference for pointers, maps, and elements of a slice. + // Descend into the child value node. if v.TransformerName != "" { out := opts.WithTypeMode(emitType).FormatDiff(v.Value) @@ -162,12 +181,27 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te formatKey = formatMapKey } + maxLen := -1 + if opts.LimitVerbosity { + if opts.DiffMode == diffIdentical { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + } else { + maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc... + } + opts.VerbosityLevel-- + } + // Handle unification. switch opts.DiffMode { case diffIdentical, diffRemoved, diffInserted: var list textList var deferredEllipsis bool // Add final "..." to indicate records were dropped for _, r := range recs { + if len(list) == maxLen { + deferredEllipsis = true + break + } + // Elide struct fields that are zero value. if k == reflect.Struct { var isZero bool @@ -205,11 +239,12 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te } // Handle differencing. + var numDiffs int var list textList groups := coalesceAdjacentRecords(name, recs) maxGroup := diffStats{Name: name} for i, ds := range groups { - if len(list) >= maxDiffElements { + if maxLen >= 0 && numDiffs >= maxLen { maxGroup = maxGroup.Append(ds) continue } @@ -273,6 +308,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te } } recs = recs[ds.NumDiff():] + numDiffs += ds.NumDiff() } if maxGroup.IsZero() { assert(len(recs) == 0) diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 9bc5eb3..ecc71f0 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -21,14 +21,23 @@ type formatValueOptions struct { // methods like error.Error or fmt.Stringer.String. AvoidStringer bool - // ShallowPointers controls whether to avoid descending into pointers. + // PrintShallowPointer controls whether to print the next pointer. // Useful when printing map keys, where pointer comparison is performed // on the pointer address rather than the pointed-at value. - ShallowPointers bool + PrintShallowPointer bool // PrintAddresses controls whether to print the address of all pointers, // slice elements, and maps. PrintAddresses bool + + // VerbosityLevel controls the amount of output to produce. + // A higher value produces more output. A value of zero or lower produces + // no output (represented using an ellipsis). + // If LimitVerbosity is false, then the level is treated as infinite. + VerbosityLevel int + + // LimitVerbosity specifies that formatting should respect VerbosityLevel. + LimitVerbosity bool } // FormatType prints the type as if it were wrapping s. @@ -45,6 +54,9 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { default: return s } + if opts.DiffMode == diffIdentical { + return s // elide type for identical nodes + } case elideType: return s } @@ -86,11 +98,22 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit // Avoid calling Error or String methods on nil receivers since many // implementations crash when doing so. if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { + var prefix, strVal string switch v := v.Interface().(type) { case error: - return textLine("e" + formatString(v.Error())) + prefix, strVal = "e", v.Error() case fmt.Stringer: - return textLine("s" + formatString(v.String())) + prefix, strVal = "s", v.String() + } + if prefix != "" { + maxLen := len(strVal) + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + } + if len(strVal) > maxLen+len(textEllipsis) { + return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis)) + } + return textLine(prefix + formatString(strVal)) } } } @@ -123,17 +146,33 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit case reflect.Complex64, reflect.Complex128: return textLine(fmt.Sprint(v.Complex())) case reflect.String: + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + } + if v.Len() > maxLen+len(textEllipsis) { + return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis)) + } 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 + maxLen := v.NumField() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } for i := 0; i < v.NumField(); i++ { vv := v.Field(i) if value.IsZero(vv) { continue // Elide fields with zero values } + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } sf := t.Field(i) if supportExporters && !isExported(sf.Name) { vv = retrieveUnexportedField(v, sf, true) @@ -147,12 +186,21 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit return textNil } if opts.PrintAddresses { - ptr = formatPointer(v) + ptr = fmt.Sprintf("⟪ptr:0x%x, len:%d, cap:%d⟫", pointerValue(v), v.Len(), v.Cap()) } fallthrough case reflect.Array: + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } var list textList for i := 0; i < v.Len(); i++ { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } vi := v.Index(i) if vi.CanAddr() { // Check for cyclic elements p := vi.Addr() @@ -177,8 +225,17 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit return textLine(formatPointer(v)) } + maxLen := v.Len() + if opts.LimitVerbosity { + maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc... + opts.VerbosityLevel-- + } var list textList for _, k := range value.SortKeys(v.MapKeys()) { + if len(list) == maxLen { + list.AppendEllipsis(diffStats{}) + break + } sk := formatMapKey(k) sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m) list = append(list, textRecord{Key: sk, Value: sv}) @@ -191,11 +248,12 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit if v.IsNil() { return textNil } - if m.Visit(v) || opts.ShallowPointers { + if m.Visit(v) { return textLine(formatPointer(v)) } - if opts.PrintAddresses { + if opts.PrintAddresses || opts.PrintShallowPointer { ptr = formatPointer(v) + opts.PrintShallowPointer = false } skipType = true // Let the underlying value print the type instead return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), false, m), ""} @@ -217,7 +275,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit func formatMapKey(v reflect.Value) string { var opts formatOptions opts.TypeMode = elideType - opts.ShallowPointers = true + opts.PrintShallowPointer = true s := opts.FormatValue(v, false, visitedPointers{}).String() return strings.TrimSpace(s) } @@ -268,11 +326,14 @@ func formatHex(u uint64) string { // formatPointer prints the address of the pointer. func formatPointer(v reflect.Value) string { + return fmt.Sprintf("⟪0x%x⟫", pointerValue(v)) +} +func pointerValue(v reflect.Value) uintptr { p := v.Pointer() if flags.Deterministic { p = 0xdeadf00f // Only used for stable testing purposes } - return fmt.Sprintf("⟪0x%x⟫", p) + return p } type visitedPointers map[value.Pointer]struct{} diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 7a35f39..cfd1b60 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -16,10 +16,6 @@ import ( "github.com/google/go-cmp/cmp/internal/diff" ) -// maxDiffElements is the maximum number of difference elements to format -// before the remaining differences are coalesced together. -const maxDiffElements = 32 - // CanFormatDiffSlice reports whether we support custom formatting for nodes // that are slices of primitive kinds or strings. func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { @@ -155,7 +151,8 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // + BAZ // """ isTripleQuoted := true - prevDiffLines := map[string]bool{} + prevRemoveLines := map[string]bool{} + prevInsertLines := map[string]bool{} var list2 textList list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) for _, r := range list { @@ -171,20 +168,24 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { isPrintable := func(r rune) bool { return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable } - isTripleQuoted = isTripleQuoted && - !strings.HasPrefix(line, `"""`) && - !strings.HasPrefix(line, "...") && - strings.TrimFunc(line, isPrintable) == "" && - (r.Diff == 0 || !prevDiffLines[normLine]) + isTripleQuoted = !strings.HasPrefix(line, `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" + switch r.Diff { + case diffRemoved: + isTripleQuoted = isTripleQuoted && !prevInsertLines[normLine] + prevRemoveLines[normLine] = true + case diffInserted: + isTripleQuoted = isTripleQuoted && !prevRemoveLines[normLine] + prevInsertLines[normLine] = true + } if !isTripleQuoted { break } r.Value = textLine(line) r.ElideComma = true - prevDiffLines[normLine] = true } - if r.Diff == 0 { - prevDiffLines = map[string]bool{} // start a new non-adjacent difference group + if !(r.Diff == diffRemoved || r.Diff == diffInserted) { // start a new non-adjacent difference group + prevRemoveLines = map[string]bool{} + prevInsertLines = map[string]bool{} } list2 = append(list2, r) } @@ -337,11 +338,18 @@ func (opts formatOptions) formatDiffSlice( return n0 - v.Len() } + var numDiffs int + maxLen := -1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + opts.VerbosityLevel-- + } + groups := coalesceAdjacentEdits(name, es) groups = coalesceInterveningIdentical(groups, chunkSize/4) maxGroup := diffStats{Name: name} for i, ds := range groups { - if len(list) >= maxDiffElements { + if maxLen >= 0 && numDiffs >= maxLen { maxGroup = maxGroup.Append(ds) continue } @@ -374,10 +382,12 @@ func (opts formatOptions) formatDiffSlice( } // Print unequal. + len0 := len(list) nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved) vx = vx.Slice(nx, vx.Len()) ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted) vy = vy.Slice(ny, vy.Len()) + numDiffs += len(list) - len0 } if maxGroup.IsZero() { assert(vx.Len() == 0 && vy.Len() == 0) diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 703ea5f..d960785 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -155,8 +155,8 @@ >>> TestDiff/Comparer#42 <<< TestDiff/Comparer#43 map[*int]string{ -- ⟪0xdeadf00f⟫: "hello", -+ ⟪0xdeadf00f⟫: "world", +- &⟪0xdeadf00f⟫0: "hello", ++ &⟪0xdeadf00f⟫0: "world", } >>> TestDiff/Comparer#43 <<< TestDiff/Comparer#44 @@ -167,7 +167,7 @@ >>> TestDiff/Comparer#44 <<< TestDiff/Comparer#45 [2][]int{ - {..., 1, 2, 3, ..., 4, 5, 6, 7, ..., 8, ..., 9, ...}, + {..., 1, 2, 3, ...}, { ... // 6 ignored and 1 identical elements - 20, @@ -246,7 +246,7 @@ }), Bytes: []uint8(Inverse(SplitBytes, [][]uint8{ {0x73, 0x6f, 0x6d, 0x65}, - {0x6d, 0x75, 0x6c, 0x74, 0x69}, + {0x6d, 0x75, 0x6c, 0x74, ...}, {0x6c, 0x69, 0x6e, 0x65}, { - 0x62, @@ -317,17 +317,7 @@ >>> TestDiff/Reporter/BatchedWithComparer <<< TestDiff/Reporter/BatchedLong interface{}( -- cmp_test.MyComposite{ -- IntsA: []int8{ -- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, -- 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, -- 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -- 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, -- 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, -- 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, -- 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, -- }, -- }, +- cmp_test.MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, ...}}, ) >>> TestDiff/Reporter/BatchedLong <<< TestDiff/Reporter#02 @@ -486,7 +476,7 @@ - zzz """ ), - "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., } >>> TestDiff/Reporter/TripleQuoteSlice <<< TestDiff/Reporter/TripleQuoteNamedTypes @@ -549,7 +539,7 @@ - zzz """ ), - "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nRRR\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", + "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\n"..., } >>> TestDiff/Reporter/TripleQuoteSliceNamedTypes <<< TestDiff/Reporter/TripleQuoteEndlines @@ -661,39 +651,53 @@ >>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace <<< TestDiff/Reporter/LimitMaximumBytesDiffs []uint8{ -- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| -+ 0x5c, 0x3d, 0x3d, 0x3d, 0x3d, 0x7c, 0x96, 0xe7, 0x53, 0x42, 0xa0, 0xab, // +|\====|..SB..| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| -- 0x1d, 0xdf, 0x61, 0xae, 0x98, 0x9f, 0x48, // -|..a...H| -+ 0xf0, 0xbd, 0xa5, 0x71, 0xab, 0x17, 0x3b, // +|...q..;| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| -- 0xc7, 0xb0, 0xb7, // -|...| -+ 0xab, 0x50, 0x00, // +|.P.| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| -- 0xef, 0x3d, 0x3d, 0x3d, 0x3d, 0x3a, 0x5c, 0x94, 0xe6, 0x4a, 0xc7, // -|.====:\..J.| -+ 0xeb, 0x3d, 0x3d, 0x3d, 0x3d, 0xa5, 0x14, 0xe6, 0x4f, 0x28, 0xe4, // +|.====...O(.| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| -- 0xb4, // -|.| -+ 0x28, // +|(| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| -- 0x0a, 0x0a, 0xf7, 0x94, // -|....| -+ 0x2f, 0x63, 0x40, 0x3f, // +|/c@?| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| -- 0xf2, 0x9c, 0xc0, 0x66, // -|...f| -+ 0xd9, 0x78, 0xed, 0x13, // +|.x..| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| -- 0x34, 0xf6, 0xf1, 0xc3, 0x17, 0x82, // -|4.....| -+ 0x4a, 0xfc, 0x91, 0x38, 0x42, 0x8d, // +|J..8B.| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| -- 0x6e, 0x16, 0x60, 0x91, 0x44, 0xc6, 0x06, // -|n.`.D..| -+ 0x61, 0x38, 0x41, 0xeb, 0x73, 0x04, 0xae, // +|a8A.s..| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| -- 0x1c, 0x45, 0x3d, 0x3d, 0x3d, 0x3d, 0x2e, // -|.E====.| -+ 0x07, 0x43, 0x3d, 0x3d, 0x3d, 0x3d, 0x1c, // +|.C====.| - 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| -- 0xc4, 0x18, // -|..| -+ 0x91, 0x22, // +|."| - ... // 95 identical, 61 removed, and 61 inserted bytes +- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| ++ 0x5c, 0x3d, 0x3d, 0x3d, 0x3d, 0x7c, 0x96, 0xe7, 0x53, 0x42, 0xa0, 0xab, // +|\====|..SB..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x1d, 0xdf, 0x61, 0xae, 0x98, 0x9f, 0x48, // -|..a...H| ++ 0xf0, 0xbd, 0xa5, 0x71, 0xab, 0x17, 0x3b, // +|...q..;| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0xc7, 0xb0, 0xb7, // -|...| ++ 0xab, 0x50, 0x00, // +|.P.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0xef, 0x3d, 0x3d, 0x3d, 0x3d, 0x3a, 0x5c, 0x94, 0xe6, 0x4a, 0xc7, // -|.====:\..J.| ++ 0xeb, 0x3d, 0x3d, 0x3d, 0x3d, 0xa5, 0x14, 0xe6, 0x4f, 0x28, 0xe4, // +|.====...O(.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0xb4, // -|.| ++ 0x28, // +|(| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x0a, 0x0a, 0xf7, 0x94, // -|....| ++ 0x2f, 0x63, 0x40, 0x3f, // +|/c@?| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xf2, 0x9c, 0xc0, 0x66, // -|...f| ++ 0xd9, 0x78, 0xed, 0x13, // +|.x..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x34, 0xf6, 0xf1, 0xc3, 0x17, 0x82, // -|4.....| ++ 0x4a, 0xfc, 0x91, 0x38, 0x42, 0x8d, // +|J..8B.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x6e, 0x16, 0x60, 0x91, 0x44, 0xc6, 0x06, // -|n.`.D..| ++ 0x61, 0x38, 0x41, 0xeb, 0x73, 0x04, 0xae, // +|a8A.s..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x1c, 0x45, 0x3d, 0x3d, 0x3d, 0x3d, 0x2e, // -|.E====.| ++ 0x07, 0x43, 0x3d, 0x3d, 0x3d, 0x3d, 0x1c, // +|.C====.| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |===========| +- 0xc4, 0x18, // -|..| ++ 0x91, 0x22, // +|."| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=======| +- 0x8a, 0x8d, 0x0e, 0x3d, 0x3d, 0x3d, 0x3d, 0x87, 0xb1, 0xa5, 0x8e, 0xc3, 0x3d, 0x3d, 0x3d, 0x3d, // -|...====.....====| +- 0x3d, 0x7a, 0x0f, 0x31, 0xae, 0x55, 0x3d, // -|=z.1.U=| ++ 0x75, 0xd8, 0xbe, 0x3d, 0x3d, 0x3d, 0x3d, 0x73, 0xec, 0x84, 0x35, 0x07, 0x3d, 0x3d, 0x3d, 0x3d, // +|u..====s..5.====| ++ 0x3d, 0x3b, 0xab, 0x53, 0x39, 0x74, // +|=;.S9t| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |=====| +- 0x47, 0x2c, 0x3d, // -|G,=| ++ 0x3d, 0x1f, 0x1b, // +|=..| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |======| +- 0x35, 0xe7, 0x35, 0xee, 0x82, 0xf4, 0xce, 0x3d, 0x3d, 0x3d, 0x3d, 0x11, 0x72, 0x3d, // -|5.5....====.r=| ++ 0x3d, 0x80, 0xab, 0x2f, 0xed, 0x2b, 0x3a, 0x3b, 0x3d, 0x3d, 0x3d, 0x3d, 0xea, 0x49, // +|=../.+:;====.I| + 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, 0x3d, // |==========| +- 0xaf, 0x5d, 0x3d, // -|.]=| ++ 0x3d, 0xab, 0x6c, // +|=.l| + ... // 51 identical, 34 removed, and 35 inserted bytes } >>> TestDiff/Reporter/LimitMaximumBytesDiffs <<< TestDiff/Reporter/LimitMaximumStringDiffs @@ -731,7 +735,22 @@ t - u + uu - ... // 17 identical, 15 removed, and 15 inserted lines + v +- w ++ ww + x +- y ++ yy + z +- A ++ AA + B +- C ++ CC + D +- E ++ EE + ... // 12 identical, 10 removed, and 10 inserted lines """ ) >>> TestDiff/Reporter/LimitMaximumStringDiffs @@ -769,30 +788,47 @@ {S: "t"}, - {S: "u"}, + {S: "uu"}, - ... // 17 identical and 15 modified elements + {S: "v"}, +- {S: "w"}, ++ {S: "ww"}, + {S: "x"}, +- {S: "y"}, ++ {S: "yy"}, + {S: "z"}, +- {S: "A"}, ++ {S: "AA"}, + {S: "B"}, +- {S: "C"}, ++ {S: "CC"}, + {S: "D"}, +- {S: "E"}, ++ {S: "EE"}, + ... // 12 identical and 10 modified elements } >>> TestDiff/Reporter/LimitMaximumSliceDiffs <<< TestDiff/Reporter#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", + StringA: ( + """ +- Package cmp determines equality of values. ++ Package cmp determines equality of value. + + This package is intended to be a more powerful and safer alternative to ... // 6 identical lines - "For example, an equality function may report floats as equal so long as they", - "are within some tolerance of each other.", -- "", -- "• Types that have an Equal method may use that method to determine equality.", -- "This allows package authors to determine the equality operation for the types", -- "that they define.", - "", - "• If no custom equality functions are used and no Equal method is defined,", + 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"), + by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly compared + using the AllowUnexported option. +- + """ + ), StringB: "", BytesA: nil, ... // 11 identical fields @@ -1032,28 +1068,28 @@ >>> TestDiff/EqualMethod/StructF <<< TestDiff/EqualMethod/StructA1#01 teststructs.StructA1{ - StructA: teststructs.StructA{X: "NotEqual"}, + StructA: {X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } >>> TestDiff/EqualMethod/StructA1#01 <<< TestDiff/EqualMethod/StructA1#03 &teststructs.StructA1{ - StructA: teststructs.StructA{X: "NotEqual"}, + 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"})), + 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"})), + StructB: Inverse(Ref, &teststructs.StructB{X: "NotEqual"}), - X: "NotEqual", + X: "not_equal", } @@ -1084,28 +1120,28 @@ >>> TestDiff/EqualMethod/StructF1 <<< TestDiff/EqualMethod/StructA2#01 teststructs.StructA2{ - StructA: &teststructs.StructA{X: "NotEqual"}, + StructA: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } >>> TestDiff/EqualMethod/StructA2#01 <<< TestDiff/EqualMethod/StructA2#03 &teststructs.StructA2{ - StructA: &teststructs.StructA{X: "NotEqual"}, + StructA: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } >>> TestDiff/EqualMethod/StructA2#03 <<< TestDiff/EqualMethod/StructB2#01 teststructs.StructB2{ - StructB: &teststructs.StructB{X: "NotEqual"}, + StructB: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } >>> TestDiff/EqualMethod/StructB2#01 <<< TestDiff/EqualMethod/StructB2#03 &teststructs.StructB2{ - StructB: &teststructs.StructB{X: "NotEqual"}, + StructB: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } @@ -1145,17 +1181,17 @@ 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⟫}}, + "Bar": &{Name: "Bar", Bravos: {...}}, "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⟫}}, + "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, "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⟫}}}, + Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, }, }, }, @@ -1167,7 +1203,7 @@ 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⟫}}, + "Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ @@ -1176,9 +1212,9 @@ + 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⟫}}}, + Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, }, - "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⟫}}, + "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: {...}}, }, }, }, @@ -1197,17 +1233,17 @@ "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⟫}}, + "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, "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⟫}}}, + Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, }, }, }, - "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: {...}}, }, }, "BuzzBarBravo": &{ @@ -1224,12 +1260,12 @@ + 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⟫}}}, + Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, }, - "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⟫}}, + "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: {...}}, }, }, - "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: {...}}, }, }, }, @@ -1242,7 +1278,7 @@ + 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⟫}}}}}, + Alphas: {"Foo": &{Name: "Foo", Bravos: {...}}}, }, }, }, @@ -1258,11 +1294,11 @@ 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⟫}}, + "Bar": &{Name: "Bar", Bravos: {...}}, "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⟫}}, + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, "BuzzBarBravo": &{ ID: 103, Name: "BuzzBarBravo", @@ -1270,31 +1306,13 @@ - 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⟫, -+ }, ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{...}, "BuzzBarBravo": &{...}}, ++ }, ++ "Buzz": &{ ++ Name: "Buzz", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}, + }, -+ "Buzz": ⟪0xdeadf00f⟫, + }, }, }, @@ -1306,35 +1324,18 @@ 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⟫}}, + "Bar": &{Name: "Bar", Bravos: {...}}, "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⟫}}}}, + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}, - "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⟫, ++ "Bar": &{Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{...}}, ++ "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{...}}, + }, + }, }, @@ -1346,7 +1347,7 @@ "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⟫}}}}, + "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &{Name: "Bar", Bravos: {"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}, "BuzzBarBravo": &{ ID: 103, Name: "BuzzBarBravo", @@ -1354,36 +1355,18 @@ - 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⟫, -+ }, ++ Name: "Bar", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{...}, "BuzzBarBravo": &{...}}, ++ }, ++ "Buzz": &{ ++ Name: "Buzz", ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪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⟫}}}}}}}}, + "Foo": &{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &{Name: "Foo", Bravos: {...}}}}}}, } >>> TestDiff/Cycle#08 <<< TestDiff/Project1#02 @@ -1468,7 +1451,7 @@ - "bar", - "baz", }, - ChangeType: []testprotos.SummerType{1, 2, 3}, + ChangeType: {1, 2, 3}, ... // 1 ignored field }, ... // 1 ignored field @@ -1496,7 +1479,7 @@ }, }, CleanGerms: nil, - GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"}, + GermMap: {13: s"germ13", 21: s"germ21"}, ... // 7 identical fields } >>> TestDiff/Project2#02 @@ -1511,7 +1494,7 @@ }), }, CleanGerms: nil, - GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"}, + GermMap: {13: s"germ13", 21: s"germ21"}, DishMap: map[int32]*teststructs.Dish{ 0: &{err: e"EOF"}, - 1: nil, From 44914b370698a5a9ce868549d62d79473faebacc Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Fri, 12 Jun 2020 15:29:35 -0700 Subject: [PATCH 51/99] Disambiguate reporter output (#216) The reporter tries to aggresively elide data that is not interesting to the user. However, doing so many result in an output that does not visually indicate the difference between semantically different objects. This CL modifies the reporter to try increasingly verbose presets until two different objects are formatted differently. This CL includes a custom implementation of reflect.Type.String that can print the type with fully qualified names to disambiguate types that happen to have the same base package name. Fixes #194 --- cmp/compare_test.go | 135 +++++++++++++++++++---- cmp/internal/teststructs/foo1/foo.go | 10 ++ cmp/internal/teststructs/foo2/foo.go | 10 ++ cmp/internal/value/name.go | 157 +++++++++++++++++++++++++++ cmp/internal/value/name_test.go | 144 ++++++++++++++++++++++++ cmp/report_compare.go | 74 ++++++++++++- cmp/report_reflect.go | 15 ++- cmp/report_slices.go | 4 +- cmp/report_text.go | 5 +- cmp/testdata/diffs | 87 +++++++++++++-- 10 files changed, 597 insertions(+), 44 deletions(-) create mode 100644 cmp/internal/teststructs/foo1/foo.go create mode 100644 cmp/internal/teststructs/foo2/foo.go create mode 100644 cmp/internal/value/name.go create mode 100644 cmp/internal/value/name_test.go diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 11979d0..be8a2f6 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -29,6 +29,8 @@ import ( pb "github.com/google/go-cmp/cmp/internal/testprotos" ts "github.com/google/go-cmp/cmp/internal/teststructs" + foo1 "github.com/google/go-cmp/cmp/internal/teststructs/foo1" + foo2 "github.com/google/go-cmp/cmp/internal/teststructs/foo2" ) func init() { @@ -101,7 +103,12 @@ func mustFormatGolden(path string, in []struct{ Name, Data string }) { var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC) -func intPtr(n int) *int { return &n } +func newInt(n int) *int { return &n } + +type Stringer string + +func newStringer(s string) fmt.Stringer { return (*Stringer)(&s) } +func (s Stringer) String() string { return string(s) } type test struct { label string // Test name @@ -149,6 +156,7 @@ func TestDiff(t *testing.T) { }() // TODO: Require every test case to provide a reason. + // TODO: Forbid any test cases with the same name. if tt.wantPanic == "" { if gotPanic != "" { t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) @@ -295,26 +303,26 @@ func comparerTests() []test { wantPanic: "cannot handle unexported field", }, { label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(4)}, + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(4)}, wantEqual: true, }, { label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, wantEqual: false, }, { label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y int) bool { return true }), }, wantEqual: true, }, { label: label, - x: &struct{ A *int }{intPtr(4)}, - y: &struct{ A *int }{intPtr(5)}, + x: &struct{ A *int }{newInt(4)}, + y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), }, @@ -555,15 +563,6 @@ func comparerTests() []test { 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{ @@ -822,6 +821,100 @@ func reporterTests() []test { ) return []test{{ + label: label + "/AmbiguousType", + x: foo1.Bar{}, + y: foo2.Bar{}, + wantEqual: false, + reason: "reporter should display the qualified type name to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointer", + x: newInt(0), + y: newInt(0), + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two values", + }, { + label: label + "/AmbiguousPointerStruct", + x: struct{ I *int }{newInt(0)}, + y: struct{ I *int }{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousPointerSlice", + x: []*int{newInt(0)}, + y: []*int{newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousPointerMap", + x: map[string]*int{"zero": newInt(0)}, + y: map[string]*int{"zero": newInt(0)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x == y }), + }, + wantEqual: false, + reason: "reporter should display the address to disambiguate between the two map values", + }, { + label: label + "/AmbiguousStringer", + x: Stringer("hello"), + y: newStringer("hello"), + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two values", + }, { + label: label + "/AmbiguousStringerStruct", + x: struct{ S fmt.Stringer }{Stringer("hello")}, + y: struct{ S fmt.Stringer }{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two struct fields", + }, { + label: label + "/AmbiguousStringerSlice", + x: []fmt.Stringer{Stringer("hello")}, + y: []fmt.Stringer{newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two slice elements", + }, { + label: label + "/AmbiguousStringerMap", + x: map[string]fmt.Stringer{"zero": Stringer("hello")}, + y: map[string]fmt.Stringer{"zero": newStringer("hello")}, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map values", + }, { + label: label + "/AmbiguousSliceHeader", + x: make([]int, 0, 5), + y: make([]int, 0, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(x, y []int) bool { return cap(x) == cap(y) }), + }, + wantEqual: false, + reason: "reporter should display the slice header to disambiguate between the two slice values", + }, { + label: label + "/AmbiguousStringerMapKey", + x: map[interface{}]string{ + nil: "nil", + Stringer("hello"): "goodbye", + foo1.Bar{"fizz"}: "buzz", + }, + y: map[interface{}]string{ + newStringer("hello"): "goodbye", + foo2.Bar{"fizz"}: "buzz", + }, + wantEqual: false, + reason: "reporter should avoid calling String to disambiguate between the two map keys", + }, { + label: label + "/NonAmbiguousStringerMapKey", + x: map[interface{}]string{Stringer("hello"): "goodbye"}, + y: map[interface{}]string{newStringer("fizz"): "buzz"}, + wantEqual: false, + reason: "reporter should call String as there is no ambiguity between the two map keys", + }, { label: "/InvalidUTF8", x: MyString("\xed\xa0\x80"), wantEqual: false, @@ -2146,7 +2239,7 @@ func project1Tests() []test { Target: "corporation", Immutable: &ts.GoatImmutable{ ID: "southbay", - State: (*pb.Goat_States)(intPtr(5)), + State: (*pb.Goat_States)(newInt(5)), Started: now, }, }, @@ -2174,7 +2267,7 @@ func project1Tests() []test { Immutable: &ts.EagleImmutable{ ID: "eagleID", Birthday: now, - MissingCall: (*pb.Eagle_MissingCalls)(intPtr(55)), + MissingCall: (*pb.Eagle_MissingCalls)(newInt(55)), }, } } @@ -2219,7 +2312,7 @@ func project1Tests() []test { x: func() ts.Eagle { eg := createEagle() eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.ID = "southbay2" - eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(intPtr(6)) + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(newInt(6)) eg.Slaps[0].Immutable.MildSlap = false return eg }(), diff --git a/cmp/internal/teststructs/foo1/foo.go b/cmp/internal/teststructs/foo1/foo.go new file mode 100644 index 0000000..c769dfb --- /dev/null +++ b/cmp/internal/teststructs/foo1/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/cmp/internal/teststructs/foo2/foo.go b/cmp/internal/teststructs/foo2/foo.go new file mode 100644 index 0000000..c769dfb --- /dev/null +++ b/cmp/internal/teststructs/foo2/foo.go @@ -0,0 +1,10 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package foo is deliberately named differently than the parent directory. +// It contain declarations that have ambiguity in their short names, +// relative to a different package also called foo. +package foo + +type Bar struct{ S string } diff --git a/cmp/internal/value/name.go b/cmp/internal/value/name.go new file mode 100644 index 0000000..8228e7d --- /dev/null +++ b/cmp/internal/value/name.go @@ -0,0 +1,157 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "reflect" + "strconv" +) + +// TypeString is nearly identical to reflect.Type.String, +// but has an additional option to specify that full type names be used. +func TypeString(t reflect.Type, qualified bool) string { + return string(appendTypeName(nil, t, qualified, false)) +} + +func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte { + // BUG: Go reflection provides no way to disambiguate two named types + // of the same name and within the same package, + // but declared within the namespace of different functions. + + // Named type. + if t.Name() != "" { + if qualified && t.PkgPath() != "" { + b = append(b, '"') + b = append(b, t.PkgPath()...) + b = append(b, '"') + b = append(b, '.') + b = append(b, t.Name()...) + } else { + b = append(b, t.String()...) + } + return b + } + + // Unnamed type. + switch k := t.Kind(); k { + case reflect.Bool, reflect.String, reflect.UnsafePointer, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: + b = append(b, k.String()...) + case reflect.Chan: + if t.ChanDir() == reflect.RecvDir { + b = append(b, "<-"...) + } + b = append(b, "chan"...) + if t.ChanDir() == reflect.SendDir { + b = append(b, "<-"...) + } + b = append(b, ' ') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Func: + if !elideFunc { + b = append(b, "func"...) + } + b = append(b, '(') + for i := 0; i < t.NumIn(); i++ { + if i > 0 { + b = append(b, ", "...) + } + if i == t.NumIn()-1 && t.IsVariadic() { + b = append(b, "..."...) + b = appendTypeName(b, t.In(i).Elem(), qualified, false) + } else { + b = appendTypeName(b, t.In(i), qualified, false) + } + } + b = append(b, ')') + switch t.NumOut() { + case 0: + // Do nothing + case 1: + b = append(b, ' ') + b = appendTypeName(b, t.Out(0), qualified, false) + default: + b = append(b, " ("...) + for i := 0; i < t.NumOut(); i++ { + if i > 0 { + b = append(b, ", "...) + } + b = appendTypeName(b, t.Out(i), qualified, false) + } + b = append(b, ')') + } + case reflect.Struct: + b = append(b, "struct{ "...) + for i := 0; i < t.NumField(); i++ { + if i > 0 { + b = append(b, "; "...) + } + sf := t.Field(i) + if !sf.Anonymous { + if qualified && sf.PkgPath != "" { + b = append(b, '"') + b = append(b, sf.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, sf.Name...) + b = append(b, ' ') + } + b = appendTypeName(b, sf.Type, qualified, false) + if sf.Tag != "" { + b = append(b, ' ') + b = strconv.AppendQuote(b, string(sf.Tag)) + } + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + case reflect.Slice, reflect.Array: + b = append(b, '[') + if k == reflect.Array { + b = strconv.AppendUint(b, uint64(t.Len()), 10) + } + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Map: + b = append(b, "map["...) + b = appendTypeName(b, t.Key(), qualified, false) + b = append(b, ']') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Ptr: + b = append(b, '*') + b = appendTypeName(b, t.Elem(), qualified, false) + case reflect.Interface: + b = append(b, "interface{ "...) + for i := 0; i < t.NumMethod(); i++ { + if i > 0 { + b = append(b, "; "...) + } + m := t.Method(i) + if qualified && m.PkgPath != "" { + b = append(b, '"') + b = append(b, m.PkgPath...) + b = append(b, '"') + b = append(b, '.') + } + b = append(b, m.Name...) + b = appendTypeName(b, m.Type, qualified, true) + } + if b[len(b)-1] == ' ' { + b = b[:len(b)-1] + } else { + b = append(b, ' ') + } + b = append(b, '}') + default: + panic("invalid kind: " + k.String()) + } + return b +} diff --git a/cmp/internal/value/name_test.go b/cmp/internal/value/name_test.go new file mode 100644 index 0000000..ddb31d4 --- /dev/null +++ b/cmp/internal/value/name_test.go @@ -0,0 +1,144 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "reflect" + "strings" + "testing" +) + +type Named struct{} + +var pkgPath = reflect.TypeOf(Named{}).PkgPath() + +func TestTypeString(t *testing.T) { + tests := []struct { + in interface{} + want string + }{{ + in: bool(false), + want: "bool", + }, { + in: int(0), + want: "int", + }, { + in: float64(0), + want: "float64", + }, { + in: string(""), + want: "string", + }, { + in: Named{}, + want: "$PackagePath.Named", + }, { + in: (chan Named)(nil), + want: "chan $PackagePath.Named", + }, { + in: (<-chan Named)(nil), + want: "<-chan $PackagePath.Named", + }, { + in: (chan<- Named)(nil), + want: "chan<- $PackagePath.Named", + }, { + in: (func())(nil), + want: "func()", + }, { + in: (func(Named))(nil), + want: "func($PackagePath.Named)", + }, { + in: (func() Named)(nil), + want: "func() $PackagePath.Named", + }, { + in: (func(int, Named) (int, error))(nil), + want: "func(int, $PackagePath.Named) (int, error)", + }, { + in: (func(...Named))(nil), + want: "func(...$PackagePath.Named)", + }, { + in: struct{}{}, + want: "struct{}", + }, { + in: struct{ Named }{}, + want: "struct{ $PackagePath.Named }", + }, { + in: struct { + Named `tag` + }{}, + want: "struct{ $PackagePath.Named \"tag\" }", + }, { + in: struct{ Named Named }{}, + want: "struct{ Named $PackagePath.Named }", + }, { + in: struct { + Named Named `tag` + }{}, + want: "struct{ Named $PackagePath.Named \"tag\" }", + }, { + in: struct { + Int int + Named Named + }{}, + want: "struct{ Int int; Named $PackagePath.Named }", + }, { + in: struct { + _ int + x Named + }{}, + want: "struct{ $FieldPrefix._ int; $FieldPrefix.x $PackagePath.Named }", + }, { + in: []Named(nil), + want: "[]$PackagePath.Named", + }, { + in: []*Named(nil), + want: "[]*$PackagePath.Named", + }, { + in: [10]Named{}, + want: "[10]$PackagePath.Named", + }, { + in: [10]*Named{}, + want: "[10]*$PackagePath.Named", + }, { + in: map[string]string(nil), + want: "map[string]string", + }, { + in: map[Named]Named(nil), + want: "map[$PackagePath.Named]$PackagePath.Named", + }, { + in: (*Named)(nil), + want: "*$PackagePath.Named", + }, { + in: (*interface{})(nil), + want: "*interface{}", + }, { + in: (*interface{ Read([]byte) (int, error) })(nil), + want: "*interface{ Read([]uint8) (int, error) }", + }, { + in: (*interface { + F1() + F2(Named) + F3() Named + F4(int, Named) (int, error) + F5(...Named) + })(nil), + want: "*interface{ F1(); F2($PackagePath.Named); F3() $PackagePath.Named; F4(int, $PackagePath.Named) (int, error); F5(...$PackagePath.Named) }", + }} + + for _, tt := range tests { + typ := reflect.TypeOf(tt.in) + wantShort := tt.want + wantShort = strings.Replace(wantShort, "$PackagePath", "value", -1) + wantShort = strings.Replace(wantShort, "$FieldPrefix.", "", -1) + if gotShort := TypeString(typ, false); gotShort != wantShort { + t.Errorf("TypeString(%v, false) mismatch:\ngot: %v\nwant: %v", typ, gotShort, wantShort) + } + wantQualified := tt.want + wantQualified = strings.Replace(wantQualified, "$PackagePath", `"`+pkgPath+`"`, -1) + wantQualified = strings.Replace(wantQualified, "$FieldPrefix", `"`+pkgPath+`"`, -1) + if gotQualified := TypeString(typ, true); gotQualified != wantQualified { + t.Errorf("TypeString(%v, true) mismatch:\ngot: %v\nwant: %v", typ, gotQualified, wantQualified) + } + } +} diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 2ac3cc6..be03a25 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -11,10 +11,6 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) -// 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 @@ -83,6 +79,22 @@ func (opts formatOptions) verbosity() uint { } } +const maxVerbosityPreset = 3 + +// verbosityPreset modifies the verbosity settings given an index +// between 0 and maxVerbosityPreset, inclusive. +func verbosityPreset(opts formatOptions, i int) formatOptions { + opts.VerbosityLevel = int(opts.verbosity()) + 2*i + if i > 0 { + opts.AvoidStringer = true + } + if i >= maxVerbosityPreset { + opts.PrintAddresses = true + opts.QualifiedNames = true + } + return opts +} + // FormatDiff converts a valueNode tree into a textNode tree, where the later // is a textual representation of the differences detected in the former. func (opts formatOptions) FormatDiff(v *valueNode) textNode { @@ -125,6 +137,11 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { var list textList outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, withinSlice, visitedPointers{}) outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, withinSlice, visitedPointers{}) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) + outx = opts2.FormatValue(v.ValueX, withinSlice, visitedPointers{}) + outy = opts2.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + } if outx != nil { list = append(list, textRecord{Diff: '-', Value: outx}) } @@ -178,7 +195,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te case reflect.Map: name = "entry" opts = opts.WithTypeMode(elideType) - formatKey = formatMapKey + formatKey = func(v reflect.Value) string { return formatMapKey(v, false) } } maxLen := -1 @@ -241,6 +258,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te // Handle differencing. var numDiffs int var list textList + var keys []reflect.Value // invariant: len(list) == len(keys) groups := coalesceAdjacentRecords(name, recs) maxGroup := diffStats{Name: name} for i, ds := range groups { @@ -274,14 +292,19 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te for _, r := range recs[:numLo] { out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) } if numEqual > numLo+numHi { ds.NumIdentical -= numLo + numHi list.AppendEllipsis(ds) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } } for _, r := range recs[numEqual-numHi : numEqual] { out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) } recs = recs[numEqual:] continue @@ -293,18 +316,27 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te case opts.CanFormatDiffSlice(r.Value): out := opts.FormatDiffSlice(r.Value) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) case r.Value.NumChildren == r.Value.MaxDepth: outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value) outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value) + for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { + opts2 := verbosityPreset(opts, i) + outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value) + outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value) + } if outx != nil { list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) + keys = append(keys, r.Key) } if outy != nil { list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) + keys = append(keys, r.Key) } default: out := opts.FormatDiff(r.Value) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) + keys = append(keys, r.Key) } } recs = recs[ds.NumDiff():] @@ -314,7 +346,39 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te assert(len(recs) == 0) } else { list.AppendEllipsis(maxGroup) + for len(keys) < len(list) { + keys = append(keys, reflect.Value{}) + } + } + assert(len(list) == len(keys)) + + // For maps, the default formatting logic uses fmt.Stringer which may + // produce ambiguous output. Avoid calling String to disambiguate. + if k == reflect.Map { + var ambiguous bool + seenKeys := map[string]reflect.Value{} + for i, currKey := range keys { + if currKey.IsValid() { + strKey := list[i].Key + prevKey, seen := seenKeys[strKey] + if seen && prevKey.CanInterface() && currKey.CanInterface() { + ambiguous = prevKey.Interface() != currKey.Interface() + if ambiguous { + break + } + } + seenKeys[strKey] = currKey + } + } + if ambiguous { + for i, k := range keys { + if k.IsValid() { + list[i].Key = formatMapKey(k, true) + } + } + } } + return textWrap{"{", list, "}"} } diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index ecc71f0..8b4325d 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -30,6 +30,10 @@ type formatValueOptions struct { // slice elements, and maps. PrintAddresses bool + // QualifiedNames controls whether FormatType uses the fully qualified name + // (including the full package path as opposed to just the package name). + QualifiedNames bool + // VerbosityLevel controls the amount of output to produce. // A higher value produces more output. A value of zero or lower produces // no output (represented using an ellipsis). @@ -62,7 +66,7 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { } // Determine the type label, applying special handling for unnamed types. - typeName := t.String() + typeName := value.TypeString(t, opts.QualifiedNames) if t.Name() == "" { // According to Go grammar, certain type literals contain symbols that // do not strongly bind to the next lexicographical token (e.g., *T). @@ -70,8 +74,6 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { 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. @@ -236,7 +238,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit list.AppendEllipsis(diffStats{}) break } - sk := formatMapKey(k) + sk := formatMapKey(k, false) sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m) list = append(list, textRecord{Key: sk, Value: sv}) } @@ -272,10 +274,13 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit // 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 { +func formatMapKey(v reflect.Value, disambiguate bool) string { var opts formatOptions + opts.DiffMode = diffIdentical opts.TypeMode = elideType opts.PrintShallowPointer = true + opts.AvoidStringer = disambiguate + opts.QualifiedNames = disambiguate s := opts.FormatValue(v, false, visitedPointers{}).String() return strings.TrimSpace(s) } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index cfd1b60..49fc5ec 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -26,8 +26,8 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { return false // No differences detected 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 + case v.Type.Kind() == reflect.Slice && (v.ValueX.Len() == 0 || v.ValueY.Len() == 0): + return false // Both slice values have to be non-empty case v.NumIgnored > 0: return false // Some ignore option was used case v.NumTransformed > 0: diff --git a/cmp/report_text.go b/cmp/report_text.go index 17a376e..b8ec9d2 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -10,6 +10,7 @@ import ( "math/rand" "strings" "time" + "unicode/utf8" "github.com/google/go-cmp/cmp/internal/flags" ) @@ -239,14 +240,14 @@ func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { _, isLine := r.Value.(textLine) return r.Key == "" || !isLine }, - func(r textRecord) int { return len(r.Key) }, + func(r textRecord) int { return utf8.RuneCountInString(r.Key) }, ) alignValueLens := s.alignLens( func(r textRecord) bool { _, isLine := r.Value.(textLine) return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil }, - func(r textRecord) int { return len(r.Value.(textLine)) }, + func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) }, ) // Format lists of simple lists in a batched form. diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index d960785..c161591 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -160,12 +160,6 @@ } >>> TestDiff/Comparer#43 <<< TestDiff/Comparer#44 - (*int)( -- &0, -+ &0, - ) ->>> TestDiff/Comparer#44 -<<< TestDiff/Comparer#45 [2][]int{ {..., 1, 2, 3, ...}, { @@ -175,8 +169,8 @@ ... // 3 ignored elements }, } ->>> TestDiff/Comparer#45 -<<< TestDiff/Comparer#46 +>>> TestDiff/Comparer#44 +<<< TestDiff/Comparer#45 [2]map[string]int{ {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, { @@ -185,7 +179,7 @@ + "keep2": 2, }, } ->>> TestDiff/Comparer#46 +>>> TestDiff/Comparer#45 <<< TestDiff/Transformer uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( - 0, @@ -258,6 +252,81 @@ })), } >>> TestDiff/Transformer#05 +<<< TestDiff/Reporter/AmbiguousType + interface{}( +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{}, + ) +>>> TestDiff/Reporter/AmbiguousType +<<< TestDiff/Reporter/AmbiguousPointer + (*int)( +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + ) +>>> TestDiff/Reporter/AmbiguousPointer +<<< TestDiff/Reporter/AmbiguousPointerStruct + struct{ I *int }{ +- I: &⟪0xdeadf00f⟫0, ++ I: &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerStruct +<<< TestDiff/Reporter/AmbiguousPointerSlice + []*int{ +- &⟪0xdeadf00f⟫0, ++ &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerSlice +<<< TestDiff/Reporter/AmbiguousPointerMap + map[string]*int{ +- "zero": &⟪0xdeadf00f⟫0, ++ "zero": &⟪0xdeadf00f⟫0, + } +>>> TestDiff/Reporter/AmbiguousPointerMap +<<< TestDiff/Reporter/AmbiguousStringer + interface{}( +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + ) +>>> TestDiff/Reporter/AmbiguousStringer +<<< TestDiff/Reporter/AmbiguousStringerStruct + struct{ S fmt.Stringer }{ +- S: cmp_test.Stringer("hello"), ++ S: &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerStruct +<<< TestDiff/Reporter/AmbiguousStringerSlice + []fmt.Stringer{ +- cmp_test.Stringer("hello"), ++ &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerSlice +<<< TestDiff/Reporter/AmbiguousStringerMap + map[string]fmt.Stringer{ +- "zero": cmp_test.Stringer("hello"), ++ "zero": &cmp_test.Stringer("hello"), + } +>>> TestDiff/Reporter/AmbiguousStringerMap +<<< TestDiff/Reporter/AmbiguousSliceHeader + []int( +- ⟪ptr:0xdeadf00f, len:0, cap:5⟫{}, ++ ⟪ptr:0xdeadf00f, len:0, cap:1000⟫{}, + ) +>>> TestDiff/Reporter/AmbiguousSliceHeader +<<< TestDiff/Reporter/AmbiguousStringerMapKey + map[interface{}]string{ +- nil: "nil", ++ &⟪0xdeadf00f⟫"github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", +- "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{S: "fizz"}: "buzz", ++ "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{S: "fizz"}: "buzz", + } +>>> TestDiff/Reporter/AmbiguousStringerMapKey +<<< TestDiff/Reporter/NonAmbiguousStringerMapKey + map[interface{}]string{ ++ s"fizz": "buzz", +- s"hello": "goodbye", + } +>>> TestDiff/Reporter/NonAmbiguousStringerMapKey <<< TestDiff//InvalidUTF8 interface{}( - cmp_test.MyString("\xed\xa0\x80"), From 12277310d373db3799bcc3f14684adee17319122 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 17 Jun 2020 14:23:43 -0700 Subject: [PATCH 52/99] Fix documentation on IgnoreFields (#220) The function now handles unexported fields since #203. --- cmp/cmpopts/ignore.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index ff8e785..afd36be 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -14,14 +14,13 @@ import ( "github.com/google/go-cmp/cmp/internal/function" ) -// IgnoreFields returns an Option that ignores exported fields of the -// given names on a single struct type. +// IgnoreFields returns an Option that ignores fields of the +// given names on a single struct type. It respects the names of exported fields +// that are forwarded due to struct embedding. // The struct type is specified by passing in a value of that type. // // The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a // specific sub-field that is embedded or nested within the parent struct. -// -// This does not handle unexported fields; use IgnoreUnexported instead. func IgnoreFields(typ interface{}, names ...string) cmp.Option { sf := newStructFilter(typ, names...) return cmp.FilterPath(sf.filter, cmp.Ignore()) From c49bfce0ac9115b09320b47c3b9534cc5afd4579 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 17 Jun 2020 14:41:50 -0700 Subject: [PATCH 53/99] Update test case names (#218) Provide a unique name for every test case. Provide a reason for every test case. The purpose of a unique name is so that insertion/removal of a case does not cause all subsequent names to suddenly shift, causing a larger number of differences in testdata/diffs. --- cmp/compare_test.go | 638 +++++++++++++++++++++++++++++--------------- cmp/testdata/diffs | 276 +++++++++---------- 2 files changed, 557 insertions(+), 357 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index be8a2f6..c94f6c0 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -155,9 +155,12 @@ func TestDiff(t *testing.T) { gotDiff = cmp.Diff(tt.x, tt.y, tt.opts...) }() - // TODO: Require every test case to provide a reason. - // TODO: Forbid any test cases with the same name. - if tt.wantPanic == "" { + switch { + case strings.Contains(t.Name(), "#"): + panic("unique test name must be provided") + case tt.reason == "": + panic("reason must be provided") + case tt.wantPanic == "": if gotPanic != "" { t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason) } @@ -175,7 +178,7 @@ func TestDiff(t *testing.T) { if gotEqual != tt.wantEqual { t.Fatalf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) } - } else { + default: if !strings.Contains(gotPanic, tt.wantPanic) { t.Fatalf("panic message:\ngot: %s\nwant: %s\nreason: %v", gotPanic, tt.wantPanic, tt.reason) } @@ -234,35 +237,40 @@ func comparerTests() []test { } return []test{{ - label: label, + label: label + "/Nil", x: nil, y: nil, wantEqual: true, + reason: "nils are equal", }, { - label: label, + label: label + "/Integer", x: 1, y: 1, wantEqual: true, + reason: "identical integers are equal", }, { - label: label, + label: label + "/UnfilteredIgnore", x: 1, y: 1, opts: []cmp.Option{cmp.Ignore()}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/UnfilteredCompare", x: 1, y: 1, opts: []cmp.Option{cmp.Comparer(func(_, _ interface{}) bool { return true })}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/UnfilteredTransform", x: 1, y: 1, opts: []cmp.Option{cmp.Transformer("λ", func(x interface{}) interface{} { return x })}, wantPanic: "cannot use an unfiltered option", + reason: "unfiltered options are functionally useless", }, { - label: label, + label: label + "/AmbiguousOptions", x: 1, y: 1, opts: []cmp.Option{ @@ -270,8 +278,9 @@ func comparerTests() []test { cmp.Transformer("λ", func(x int) float64 { return float64(x) }), }, wantPanic: "ambiguous set of applicable options", + reason: "both options apply on int, leading to ambiguity", }, { - label: label, + label: label + "/IgnorePrecedence", x: 1, y: 1, opts: []cmp.Option{ @@ -282,84 +291,98 @@ func comparerTests() []test { cmp.Transformer("λ", func(x int) float64 { return float64(x) }), }, wantEqual: true, + reason: "ignore takes precedence over other options", }, { - label: label, + label: label + "/UnknownOption", opts: []cmp.Option{struct{ cmp.Option }{}}, wantPanic: "unknown option", + reason: "use of unknown option should panic", }, { - label: label, + label: label + "/StructEqual", x: struct{ A, B, C int }{1, 2, 3}, y: struct{ A, B, C int }{1, 2, 3}, wantEqual: true, + reason: "struct comparison with all equal fields", }, { - label: label, + label: label + "/StructInequal", x: struct{ A, B, C int }{1, 2, 3}, y: struct{ A, B, C int }{1, 2, 4}, wantEqual: false, + reason: "struct comparison with inequal C field", }, { - label: label, + label: label + "/StructUnexported", x: struct{ a, b, c int }{1, 2, 3}, y: struct{ a, b, c int }{1, 2, 4}, wantPanic: "cannot handle unexported field", + reason: "unexported fields result in a panic by default", }, { - label: label, + label: label + "/PointerStructEqual", x: &struct{ A *int }{newInt(4)}, y: &struct{ A *int }{newInt(4)}, wantEqual: true, + reason: "comparison of pointer to struct with equal A field", }, { - label: label, + label: label + "/PointerStructInequal", x: &struct{ A *int }{newInt(4)}, y: &struct{ A *int }{newInt(5)}, wantEqual: false, + reason: "comparison of pointer to struct with inequal A field", }, { - label: label, + label: label + "/PointerStructTrueComparer", x: &struct{ A *int }{newInt(4)}, y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y int) bool { return true }), }, wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with always equal comparer", }, { - label: label, + label: label + "/PointerStructNonNilComparer", x: &struct{ A *int }{newInt(4)}, y: &struct{ A *int }{newInt(5)}, opts: []cmp.Option{ cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), }, wantEqual: true, + reason: "comparison of pointer to struct with inequal A field, but treated as equal with comparer checking pointers for nilness", }, { - label: label, + label: label + "/StructNestedPointerEqual", x: &struct{ R *bytes.Buffer }{}, y: &struct{ R *bytes.Buffer }{}, wantEqual: true, + reason: "equal since both pointers in R field are nil", }, { - label: label, + label: label + "/StructNestedPointerInequal", x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, y: &struct{ R *bytes.Buffer }{}, wantEqual: false, + reason: "inequal since R field is inequal", }, { - label: label, + label: label + "/StructNestedPointerTrueComparer", x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, y: &struct{ R *bytes.Buffer }{}, opts: []cmp.Option{ cmp.Comparer(func(x, y io.Reader) bool { return true }), }, wantEqual: true, + reason: "equal despite inequal R field values since the comparer always reports true", }, { - label: label, + label: label + "/StructNestedValueUnexportedPanic1", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer contains unexported fields", }, { - label: label, + label: label + "/StructNestedValueUnexportedPanic2", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, opts: []cmp.Option{ cmp.Comparer(func(x, y io.Reader) bool { return true }), }, wantPanic: "cannot handle unexported field", + reason: "bytes.Buffer value does not implement io.Reader", }, { - label: label, + label: label + "/StructNestedValueEqual", x: &struct{ R bytes.Buffer }{}, y: &struct{ R bytes.Buffer }{}, opts: []cmp.Option{ @@ -367,13 +390,15 @@ func comparerTests() []test { cmp.Comparer(func(x, y io.Reader) bool { return true }), }, wantEqual: true, + reason: "bytes.Buffer pointer due to shallow copy does implement io.Reader", }, { - label: label, + label: label + "/RegexpUnexportedPanic", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, wantPanic: "cannot handle unexported field", + reason: "regexp.Regexp contains unexported fields", }, { - label: label, + label: label + "/RegexpEqual", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { @@ -383,8 +408,9 @@ func comparerTests() []test { return x.String() == y.String() })}, wantEqual: true, + reason: "comparer for *regexp.Regexp applied with equal regexp strings", }, { - label: label, + label: label + "/RegexpInequal", x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*d*")}, opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { @@ -394,8 +420,9 @@ func comparerTests() []test { return x.String() == y.String() })}, wantEqual: false, + reason: "comparer for *regexp.Regexp applied with inequal regexp strings", }, { - label: label, + label: label + "/TriplePointerEqual", x: func() ***int { a := 0 b := &a @@ -409,8 +436,9 @@ func comparerTests() []test { return &c }(), wantEqual: true, + reason: "three layers of pointers to the same value", }, { - label: label, + label: label + "/TriplePointerInequal", x: func() ***int { a := 0 b := &a @@ -424,40 +452,47 @@ func comparerTests() []test { return &c }(), wantEqual: false, + reason: "three layers of pointers to different values", }, { - label: label, + label: label + "/SliceWithDifferingCapacity", x: []int{1, 2, 3, 4, 5}[:3], y: []int{1, 2, 3}, wantEqual: true, + reason: "elements past the slice length are not compared", }, { - label: label, + label: label + "/StringerEqual", x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, y: struct{ fmt.Stringer }{regexp.MustCompile("hello")}, opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, wantEqual: true, + reason: "comparer for fmt.Stringer used to compare differing types with same string", }, { - label: label, + label: label + "/StringerInequal", x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, wantEqual: false, + reason: "comparer for fmt.Stringer used to compare differing types with different strings", }, { - label: label, + label: label + "/DifferingHash", x: md5.Sum([]byte{'a'}), y: md5.Sum([]byte{'b'}), wantEqual: false, + reason: "hash differs", }, { - label: label, + label: label + "/NilStringer", x: new(fmt.Stringer), y: nil, wantEqual: false, + reason: "by default differing types are always inequal", }, { - label: label, + label: label + "/TarHeaders", x: makeTarHeaders('0'), y: makeTarHeaders('\x00'), wantEqual: false, + reason: "type flag differs between the headers", }, { - label: label, + label: label + "/NonDeterministicComparer", x: make([]int, 1000), y: make([]int, 1000), opts: []cmp.Option{ @@ -466,8 +501,9 @@ func comparerTests() []test { }), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic comparer", }, { - label: label, + label: label + "/NonDeterministicFilter", x: make([]int, 1000), y: make([]int, 1000), opts: []cmp.Option{ @@ -476,8 +512,9 @@ func comparerTests() []test { }, cmp.Ignore()), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "non-deterministic filter", }, { - label: label, + label: label + "/AssymetricComparer", 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{ @@ -486,8 +523,9 @@ func comparerTests() []test { }), }, wantPanic: "non-deterministic or non-symmetric function detected", + reason: "asymmetric comparer", }, { - label: label, + label: label + "/NonDeterministicTransformer", x: make([]string, 1000), y: make([]string, 1000), opts: []cmp.Option{ @@ -496,10 +534,9 @@ func comparerTests() []test { }), }, wantPanic: "non-deterministic function detected", + reason: "non-deterministic transformer", }, { - // Make sure the dynamic checks don't raise a false positive for - // non-reflexive comparisons. - label: label, + label: label + "/IrreflexiveComparison", x: make([]int, 10), y: make([]int, 10), opts: []cmp.Option{ @@ -508,19 +545,20 @@ func comparerTests() []test { }), }, wantEqual: false, + reason: "dynamic checks should not panic for non-reflexive comparisons", }, { - // Ensure reasonable Stringer formatting of map keys. - label: label, + label: label + "/StringerMapKey", x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}}, y: map[*pb.Stringer]*pb.Stringer(nil), wantEqual: false, + reason: "stringer should be used to format the map key", }, { - // Ensure Stringer avoids double-quote escaping if possible. - label: label, + label: label + "/StringerBacktick", x: []*pb.Stringer{{`multi\nline\nline\nline`}}, wantEqual: false, + reason: "stringer should use backtick quoting if more readable", }, { - label: label, + label: label + "/AvoidPanicAssignableConverter", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -529,8 +567,9 @@ func comparerTests() []test { }), }, wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", }, { - label: label, + label: label + "/AvoidPanicAssignableTransformer", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -539,8 +578,9 @@ func comparerTests() []test { }), }, wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", }, { - label: label, + label: label + "/AvoidPanicAssignableFilter", x: struct{ I Iface2 }{}, y: struct{ I Iface2 }{}, opts: []cmp.Option{ @@ -549,13 +589,15 @@ func comparerTests() []test { }, cmp.Ignore()), }, wantEqual: true, + reason: "function call using Go reflection should automatically convert assignable interfaces; see https://golang.org/issues/22143", }, { - label: label, + label: label + "/DynamicMap", x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}}, y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}}, wantEqual: false, + reason: "dynamic map with differing types (but semantically equivalent values) should be inequal", }, { - label: label, + label: label + "/MapKeyPointer", x: map[*int]string{ new(int): "hello", }, @@ -563,8 +605,9 @@ func comparerTests() []test { new(int): "world", }, wantEqual: false, + reason: "map keys should use shallow (rather than deep) pointer comparison", }, { - label: label, + label: label + "/IgnoreSliceElements", x: [2][]int{ {0, 0, 0, 1, 2, 3, 0, 0, 4, 5, 6, 7, 8, 0, 9, 0, 0}, {0, 1, 0, 0, 0, 20}, @@ -588,7 +631,7 @@ func comparerTests() []test { wantEqual: false, reason: "all zero slice elements are ignored (even if missing)", }, { - label: label, + label: label + "/IgnoreMapEntries", x: [2]map[string]int{ {"ignore1": 0, "ignore2": 0, "keep1": 1, "keep2": 2, "KEEP3": 3, "IGNORE3": 0}, {"keep1": 1, "ignore1": 0}, @@ -612,19 +655,19 @@ func comparerTests() []test { wantEqual: false, reason: "all zero map entries are ignored (even if missing)", }, { - label: label, + label: label + "/PanicUnexportedNamed", x: namedWithUnexported{}, y: namedWithUnexported{}, wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".namedWithUnexported", reason: "panic on named struct type with unexported field", }, { - label: label, + label: label + "/PanicUnexportedUnnamed", x: struct{ a int }{}, y: struct{ a int }{}, wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".(struct { a int })", reason: "panic on unnamed struct type with unexported field", }, { - label: label, + label: label + "/UnaddressableStruct", x: struct{ s fmt.Stringer }{new(bytes.Buffer)}, y: struct{ s fmt.Stringer }{nil}, opts: []cmp.Option{ @@ -676,7 +719,7 @@ func transformerTests() []test { } return []test{{ - label: label, + label: label + "/Uints", x: uint8(0), y: uint8(1), opts: []cmp.Option{ @@ -685,8 +728,9 @@ func transformerTests() []test { cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }), }, wantEqual: false, + reason: "transform uint8 -> uint16 -> uint32 -> uint64", }, { - label: label, + label: label + "/Ambiguous", x: 0, y: 1, opts: []cmp.Option{ @@ -694,8 +738,9 @@ func transformerTests() []test { cmp.Transformer("λ", func(in int) int { return in }), }, wantPanic: "ambiguous set of applicable options", + reason: "both transformers apply on int", }, { - label: label, + label: label + "/Filtered", x: []int{0, -5, 0, -1}, y: []int{1, 3, 0, -5}, opts: []cmp.Option{ @@ -709,8 +754,9 @@ func transformerTests() []test { ), }, wantEqual: false, + reason: "disjoint transformers filtered based on the values", }, { - label: label, + label: label + "/DisjointOutput", x: 0, y: 1, opts: []cmp.Option{ @@ -722,8 +768,9 @@ func transformerTests() []test { }), }, wantEqual: false, + reason: "output type differs based on input value", }, { - label: label, + label: label + "/JSON", x: `{ "firstName": "John", "lastName": "Smith", @@ -761,8 +808,9 @@ func transformerTests() []test { }), }, wantEqual: false, + reason: "transformer used to parse JSON input", }, { - label: label, + label: label + "/AcyclicString", x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")}, y: StringBytes{String: "some\nmulti\nline\nstring", Bytes: []byte("some\nmulti\nline\nBytes")}, opts: []cmp.Option{ @@ -770,22 +818,27 @@ func transformerTests() []test { transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }), }, wantEqual: false, + reason: "string -> []string and []byte -> [][]byte transformer only applied once", }, { - x: "a\nb\nc\n", - y: "a\nb\nc\n", + label: label + "/CyclicString", + x: "a\nb\nc\n", + y: "a\nb\nc\n", opts: []cmp.Option{ cmp.Transformer("SplitLines", func(s string) []string { return strings.Split(s, "\n") }), }, wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from string -> []string -> string", }, { - x: complex64(0), - y: complex64(0), + label: label + "/CyclicComplex", + x: complex64(0), + y: complex64(0), opts: []cmp.Option{ cmp.Transformer("T1", func(x complex64) complex128 { return complex128(x) }), cmp.Transformer("T2", func(x complex128) [2]float64 { return [2]float64{real(x), imag(x)} }), cmp.Transformer("T3", func(x float64) complex64 { return complex64(complex(x, 0)) }), }, wantPanic: "recursive set of Transformers detected", + reason: "cyclic transformation from complex64 -> complex128 -> [2]float64 -> complex64", }} } @@ -915,18 +968,18 @@ func reporterTests() []test { wantEqual: false, reason: "reporter should call String as there is no ambiguity between the two map keys", }, { - label: "/InvalidUTF8", + label: label + "/InvalidUTF8", x: MyString("\xed\xa0\x80"), wantEqual: false, reason: "invalid UTF-8 should format as quoted string", }, { - label: label, + label: label + "/UnbatchedSlice", x: MyComposite{IntsA: []int8{11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, y: MyComposite{IntsA: []int8{10, 11, 21, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, wantEqual: false, reason: "unbatched diffing desired since few elements differ", }, { - label: label, + label: label + "/BatchedSlice", x: MyComposite{IntsA: []int8{10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}}, y: MyComposite{IntsA: []int8{12, 29, 13, 27, 22, 23, 17, 18, 19, 20, 21, 10, 26, 16, 25, 28, 11, 15, 24, 14}}, wantEqual: false, @@ -946,7 +999,7 @@ func reporterTests() []test { wantEqual: false, reason: "batched output desired for a single slice of primitives unique to one of the inputs", }, { - label: label, + label: label + "/BatchedNamedAndUnnamed", x: MyComposite{ BytesA: []byte{1, 2, 3}, BytesB: []MyByte{4, 5, 6}, @@ -978,19 +1031,19 @@ func reporterTests() []test { wantEqual: false, reason: "batched diffing available for both named and unnamed slices", }, { - label: label, + label: label + "/BinaryHexdump", x: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeX\x95A\xfd$fX\x8byT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1U~{\xf6\xb3~\x1dWi \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, y: MyComposite{BytesA: []byte("\xf3\x0f\x8a\xa4\xd3\x12R\t$\xbeT\xac\r\xd8qwp\x20j\\s\u007f\x8c\x17U\xc04\xcen\xf7\xaaG\xee2\x9d\xc5\xca\x1eX\xaf\x8f'\xf3\x02J\x90\xedi.p2\xb4\xab0 \xb6\xbd\\b4\x17\xb0\x00\xbbO~'G\x06\xf4.f\xfdc\xd7\x04ݷ0\xb7\xd1u-[]]\xf6\xb3haha~\x1dWI \x9e\xbc\xdf\xe1M\xa9\xef\xa2\xd2\xed\xb4Gx\xc9\xc9'\xa4\xc6\xce\xecDp]")}, wantEqual: false, reason: "binary diff in hexdump form since data is binary data", }, { - label: label, + label: label + "/StringHexdump", x: MyComposite{StringB: MyString("readme.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000046\x0000000000000\x00011173\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, y: MyComposite{StringB: MyString("gopher.txt\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000600\x000000000\x000000000\x0000000000043\x0000000000000\x00011217\x00 0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ustar\x0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000000000\x000000000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")}, wantEqual: false, reason: "binary diff desired since string looks like binary data", }, { - label: label, + label: label + "/BinaryString", x: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"314 54th Avenue","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, y: MyComposite{BytesA: []byte(`{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"address":{"streetAddress":"21 2nd Street","city":"New York","state":"NY","postalCode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"},{"type":"mobile","number":"123 456-7890"}],"children":[],"spouse":null}`)}, wantEqual: false, @@ -1096,7 +1149,7 @@ func reporterTests() []test { wantEqual: false, reason: "total slice difference output is truncated due to excessive number of differences", }, { - label: label, + label: label + "/MultilineString", x: MyComposite{ StringA: strings.TrimPrefix(` Package cmp determines equality of values. @@ -1147,7 +1200,7 @@ using the AllowUnexported option.`, "\n"), wantEqual: false, reason: "batched per-line diff desired since string looks like multi-line textual data", }, { - label: label, + label: label + "/Slices", x: MyComposite{ BytesA: []byte{1, 2, 3}, BytesB: []MyByte{4, 5, 6}, @@ -1166,7 +1219,7 @@ using the AllowUnexported option.`, "\n"), wantEqual: false, reason: "batched diffing for non-nil slices and nil slices", }, { - label: label, + label: label + "/EmptySlices", x: MyComposite{ BytesA: []byte{}, BytesB: []MyByte{}, @@ -1188,7 +1241,7 @@ using the AllowUnexported option.`, "\n"), } func embeddedTests() []test { - const label = "EmbeddedStruct/" + const label = "EmbeddedStruct" privateStruct := *new(ts.ParentStructA).PrivateStruct() @@ -1289,52 +1342,58 @@ func embeddedTests() []test { } return []test{{ - label: label + "ParentStructA", + label: label + "/ParentStructA/PanicUnexported1", x: ts.ParentStructA{}, y: ts.ParentStructA{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructA has an unexported field", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Ignored", x: ts.ParentStructA{}, y: ts.ParentStructA{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructA{}), }, wantEqual: true, + reason: "the only field (which is unexported) of ParentStructA is ignored", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/PanicUnexported2", x: createStructA(0), y: createStructA(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Equal", x: createStructA(0), y: createStructA(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructA and privateStruct are allowed", }, { - label: label + "ParentStructA", + label: label + "/ParentStructA/Inequal", x: createStructA(0), y: createStructA(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/PanicUnexported1", x: ts.ParentStructB{}, y: ts.ParentStructB{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructB{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct has an unexported field", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Ignored", x: ts.ParentStructB{}, y: ts.ParentStructB{}, opts: []cmp.Option{ @@ -1342,77 +1401,87 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are ignored", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/PanicUnexported2", x: createStructB(0), y: createStructB(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Equal", x: createStructB(0), y: createStructB(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of both ParentStructB and PublicStruct are allowed", }, { - label: label + "ParentStructB", + label: label + "/ParentStructB/Inequal", x: createStructB(0), y: createStructB(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/PanicUnexported1", x: ts.ParentStructC{}, y: ts.ParentStructC{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructC has unexported fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Ignored", x: ts.ParentStructC{}, y: ts.ParentStructC{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructC{}), }, wantEqual: true, + reason: "unexported fields of ParentStructC are ignored", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/PanicUnexported2", x: createStructC(0), y: createStructC(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Equal", x: createStructC(0), y: createStructC(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructC and privateStruct are allowed", }, { - label: label + "ParentStructC", + label: label + "/ParentStructC/Inequal", x: createStructC(0), y: createStructC(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/PanicUnexported1", x: ts.ParentStructD{}, y: ts.ParentStructD{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructD{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructD has unexported fields", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Ignored", x: ts.ParentStructD{}, y: ts.ParentStructD{}, opts: []cmp.Option{ @@ -1420,40 +1489,45 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of ParentStructD and PublicStruct are ignored", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/PanicUnexported2", x: createStructD(0), y: createStructD(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Equal", x: createStructD(0), y: createStructD(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of both ParentStructD and PublicStruct are allowed", }, { - label: label + "ParentStructD", + label: label + "/ParentStructD/Inequal", x: createStructD(0), y: createStructD(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/PanicUnexported1", x: ts.ParentStructE{}, y: ts.ParentStructE{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructE{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructE has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Ignored", x: ts.ParentStructE{}, y: ts.ParentStructE{}, opts: []cmp.Option{ @@ -1461,48 +1535,54 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of ParentStructE and PublicStruct are ignored", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/PanicUnexported2", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/PanicUnexported3", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Equal", x: createStructE(0), y: createStructE(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructE, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructE", + label: label + "/ParentStructE/Inequal", x: createStructE(0), y: createStructE(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/PanicUnexported1", x: ts.ParentStructF{}, y: ts.ParentStructF{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructF{}), }, wantPanic: "cannot handle unexported field", + reason: "ParentStructF has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Ignored", x: ts.ParentStructF{}, y: ts.ParentStructF{}, opts: []cmp.Option{ @@ -1510,227 +1590,256 @@ func embeddedTests() []test { cmpopts.IgnoreUnexported(ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of ParentStructF and PublicStruct are ignored", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/PanicUnexported2", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/PanicUnexported3", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Equal", x: createStructF(0), y: createStructF(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructF, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructF", + label: label + "/ParentStructF/Inequal", x: createStructF(0), y: createStructF(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/PanicUnexported1", x: ts.ParentStructG{}, y: ts.ParentStructG{}, wantPanic: wantPanicNotGo110("cannot handle unexported field"), wantEqual: !flags.AtLeastGo110, + reason: "ParentStructG has unexported fields", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Ignored", x: ts.ParentStructG{}, y: ts.ParentStructG{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructG{}), }, wantEqual: true, + reason: "unexported fields of ParentStructG are ignored", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/PanicUnexported2", x: createStructG(0), y: createStructG(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Equal", x: createStructG(0), y: createStructG(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructG and privateStruct are allowed", }, { - label: label + "ParentStructG", + label: label + "/ParentStructG/Inequal", x: createStructG(0), y: createStructG(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/EqualNil", x: ts.ParentStructH{}, y: ts.ParentStructH{}, wantEqual: true, + reason: "PublicStruct is not compared because the pointer is nil", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/PanicUnexported1", x: createStructH(0), y: createStructH(0), wantPanic: "cannot handle unexported field", + reason: "PublicStruct has unexported fields", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Ignored", x: ts.ParentStructH{}, y: ts.ParentStructH{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructH{}), }, wantEqual: true, + reason: "unexported fields of ParentStructH are ignored (it has none)", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/PanicUnexported2", x: createStructH(0), y: createStructH(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct also has unexported fields", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Equal", x: createStructH(0), y: createStructH(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of both ParentStructH and PublicStruct are allowed", }, { - label: label + "ParentStructH", + label: label + "/ParentStructH/Inequal", x: createStructH(0), y: createStructH(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported1", x: ts.ParentStructI{}, y: ts.ParentStructI{}, wantPanic: wantPanicNotGo110("cannot handle unexported field"), wantEqual: !flags.AtLeastGo110, + reason: "ParentStructI has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Ignored1", x: ts.ParentStructI{}, y: ts.ParentStructI{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}), }, wantEqual: true, + reason: "unexported fields of ParentStructI are ignored", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported2", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Ignored2", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of ParentStructI and PublicStruct are ignored", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/PanicUnexported3", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Equal", x: createStructI(0), y: createStructI(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructI, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructI", + label: label + "/ParentStructI/Inequal", x: createStructI(0), y: createStructI(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/PanicUnexported1", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, wantPanic: "cannot handle unexported field", + reason: "ParentStructJ has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/PanicUnexported2", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructJ{}), }, wantPanic: "cannot handle unexported field", + reason: "PublicStruct and privateStruct also has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Ignored", x: ts.ParentStructJ{}, y: ts.ParentStructJ{}, opts: []cmp.Option{ cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), }, wantEqual: true, + reason: "unexported fields of ParentStructJ and PublicStruct are ignored", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/PanicUnexported3", x: createStructJ(0), y: createStructJ(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), }, wantPanic: "cannot handle unexported field", + reason: "privateStruct also has unexported fields", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Equal", x: createStructJ(0), y: createStructJ(0), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, wantEqual: true, + reason: "unexported fields of both ParentStructJ, PublicStruct, and privateStruct are allowed", }, { - label: label + "ParentStructJ", + label: label + "/ParentStructJ/Inequal", x: createStructJ(0), y: createStructJ(1), opts: []cmp.Option{ cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), }, wantEqual: false, + reason: "the two values differ on some fields", }} } func methodTests() []test { - const label = "EqualMethod/" + const label = "EqualMethod" // A common mistake that the Equal method is on a pointer receiver, // but only a non-pointer value is present in the struct. // A transform can be used to forcibly reference the value. - derefTransform := cmp.FilterPath(func(p cmp.Path) bool { + addrTransform := cmp.FilterPath(func(p cmp.Path) bool { if len(p) == 0 { return false } @@ -1744,7 +1853,7 @@ func methodTests() []test { tf.In(0).AssignableTo(tf.In(1)) && tf.Out(0) == reflect.TypeOf(true) } return false - }, cmp.Transformer("Ref", func(x interface{}) interface{} { + }, cmp.Transformer("Addr", func(x interface{}) interface{} { v := reflect.ValueOf(x) vp := reflect.New(v.Type()) vp.Elem().Set(v) @@ -1755,284 +1864,338 @@ func methodTests() []test { // returns true, while the underlying data are fundamentally different. // Since the method should be called, these are expected to be equal. return []test{{ - label: label + "StructA", + label: label + "/StructA/ValueEqual", x: ts.StructA{X: "NotEqual"}, y: ts.StructA{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructA value called", }, { - label: label + "StructA", + label: label + "/StructA/PointerEqual", x: &ts.StructA{X: "NotEqual"}, y: &ts.StructA{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructA pointer called", }, { - label: label + "StructB", + label: label + "/StructB/ValueInequal", x: ts.StructB{X: "NotEqual"}, y: ts.StructB{X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructB value not called", }, { - label: label + "StructB", + label: label + "/StructB/ValueAddrEqual", x: ts.StructB{X: "NotEqual"}, y: ts.StructB{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform", }, { - label: label + "StructB", + label: label + "/StructB/PointerEqual", x: &ts.StructB{X: "NotEqual"}, y: &ts.StructB{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructB pointer called", }, { - label: label + "StructC", + label: label + "/StructC/ValueEqual", x: ts.StructC{X: "NotEqual"}, y: ts.StructC{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructC value called", }, { - label: label + "StructC", + label: label + "/StructC/PointerEqual", x: &ts.StructC{X: "NotEqual"}, y: &ts.StructC{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructC pointer called", }, { - label: label + "StructD", + label: label + "/StructD/ValueInequal", x: ts.StructD{X: "NotEqual"}, y: ts.StructD{X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructD value not called", }, { - label: label + "StructD", + label: label + "/StructD/ValueAddrEqual", x: ts.StructD{X: "NotEqual"}, y: ts.StructD{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructD pointer called due to shallow copy transform", }, { - label: label + "StructD", + label: label + "/StructD/PointerEqual", x: &ts.StructD{X: "NotEqual"}, y: &ts.StructD{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructD pointer called", }, { - label: label + "StructE", + label: label + "/StructE/ValueInequal", x: ts.StructE{X: "NotEqual"}, y: ts.StructE{X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructE value not called", }, { - label: label + "StructE", + label: label + "/StructE/ValueAddrEqual", x: ts.StructE{X: "NotEqual"}, y: ts.StructE{X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructE pointer called due to shallow copy transform", }, { - label: label + "StructE", + label: label + "/StructE/PointerEqual", x: &ts.StructE{X: "NotEqual"}, y: &ts.StructE{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructE pointer called", }, { - label: label + "StructF", + label: label + "/StructF/ValueInequal", x: ts.StructF{X: "NotEqual"}, y: ts.StructF{X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructF value not called", }, { - label: label + "StructF", + label: label + "/StructF/PointerEqual", x: &ts.StructF{X: "NotEqual"}, y: &ts.StructF{X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructF pointer called", }, { - label: label + "StructA1", + label: label + "/StructA1/ValueEqual", x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructA value called with equal X field", }, { - label: label + "StructA1", + label: label + "/StructA1/ValueInequal", x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", }, { - label: label + "StructA1", + label: label + "/StructA1/PointerEqual", x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"}, y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructA value called with equal X field", }, { - label: label + "StructA1", + label: label + "/StructA1/PointerInequal", x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructA value called, but inequal X field", }, { - label: label + "StructB1", + label: label + "/StructB1/ValueEqual", x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", }, { - label: label + "StructB1", + label: label + "/StructB1/ValueInequal", x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", }, { - label: label + "StructB1", + label: label + "/StructB1/PointerEqual", x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"}, y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructB pointer called due to shallow copy transform with equal X field", }, { - label: label + "StructB1", + label: label + "/StructB1/PointerInequal", x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: false, + reason: "Equal method on StructB pointer called due to shallow copy transform, but inequal X field", }, { - label: label + "StructC1", + label: label + "/StructC1/ValueEqual", x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructC1 value called", }, { - label: label + "StructC1", + label: label + "/StructC1/PointerEqual", x: &ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructC1{StructC: ts.StructC{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructC1 pointer called", }, { - label: label + "StructD1", + label: label + "/StructD1/ValueInequal", x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructD1 value not called", }, { - label: label + "StructD1", + label: label + "/StructD1/PointerAddrEqual", x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructD1 pointer called due to shallow copy transform", }, { - label: label + "StructD1", + label: label + "/StructD1/PointerEqual", x: &ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructD1 pointer called", }, { - label: label + "StructE1", + label: label + "/StructE1/ValueInequal", x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructE1 value not called", }, { - label: label + "StructE1", + label: label + "/StructE1/ValueAddrEqual", x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, - opts: []cmp.Option{derefTransform}, + opts: []cmp.Option{addrTransform}, wantEqual: true, + reason: "Equal method on StructE1 pointer called due to shallow copy transform", }, { - label: label + "StructE1", + label: label + "/StructE1/PointerEqual", x: &ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructE1 pointer called", }, { - label: label + "StructF1", + label: label + "/StructF1/ValueInequal", x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructF1 value not called", }, { - label: label + "StructF1", + label: label + "/StructF1/PointerEqual", x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method on StructF1 pointer called", }, { - label: label + "StructA2", + label: label + "/StructA2/ValueEqual", x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", }, { - label: label + "StructA2", + label: label + "/StructA2/ValueInequal", x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", }, { - label: label + "StructA2", + label: label + "/StructA2/PointerEqual", x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"}, y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructA pointer called with equal X field", }, { - label: label + "StructA2", + label: label + "/StructA2/PointerInequal", x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructA pointer called, but inequal X field", }, { - label: label + "StructB2", + label: label + "/StructB2/ValueEqual", x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", }, { - label: label + "StructB2", + label: label + "/StructB2/ValueInequal", x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", }, { - label: label + "StructB2", + label: label + "/StructB2/PointerEqual", x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"}, y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"}, wantEqual: true, + reason: "Equal method on StructB pointer called with equal X field", }, { - label: label + "StructB2", + label: label + "/StructB2/PointerInequal", x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"}, wantEqual: false, + reason: "Equal method on StructB pointer called, but inequal X field", }, { - label: label + "StructC2", + label: label + "/StructC2/ValueEqual", x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructC2 value due to forwarded StructC pointer", }, { - label: label + "StructC2", + label: label + "/StructC2/PointerEqual", x: &ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructC2{StructC: &ts.StructC{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructC2 pointer due to forwarded StructC pointer", }, { - label: label + "StructD2", + label: label + "/StructD2/ValueEqual", x: ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructD2 value due to forwarded StructD pointer", }, { - label: label + "StructD2", + label: label + "/StructD2/PointerEqual", x: &ts.StructD2{StructD: &ts.StructD{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructD2{StructD: &ts.StructD{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructD2 pointer due to forwarded StructD pointer", }, { - label: label + "StructE2", + label: label + "/StructE2/ValueEqual", x: ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructE2 value due to forwarded StructE pointer", }, { - label: label + "StructE2", + label: label + "/StructE2/PointerEqual", x: &ts.StructE2{StructE: &ts.StructE{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructE2{StructE: &ts.StructE{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructE2 pointer due to forwarded StructE pointer", }, { - label: label + "StructF2", + label: label + "/StructF2/ValueEqual", x: ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructF2 value due to forwarded StructF pointer", }, { - label: label + "StructF2", + label: label + "/StructF2/PointerEqual", x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"}, y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"}, wantEqual: true, + reason: "Equal method called on StructF2 pointer due to forwarded StructF pointer", }, { - label: label + "StructNo", + label: label + "/StructNo/Inequal", x: ts.StructNo{X: "NotEqual"}, y: ts.StructNo{X: "not_equal"}, wantEqual: false, + reason: "Equal method not called since StructNo is not assignable to InterfaceA", }, { - label: label + "AssignA", + label: label + "/AssignA/Equal", x: ts.AssignA(func() int { return 0 }), y: ts.AssignA(func() int { return 1 }), wantEqual: true, + reason: "Equal method called since named func is assignable to unnamed func", }, { - label: label + "AssignB", + label: label + "/AssignB/Equal", x: ts.AssignB(struct{ A int }{0}), y: ts.AssignB(struct{ A int }{1}), wantEqual: true, + reason: "Equal method called since named struct is assignable to unnamed struct", }, { - label: label + "AssignC", + label: label + "/AssignC/Equal", x: ts.AssignC(make(chan bool)), y: ts.AssignC(make(chan bool)), wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", }, { - label: label + "AssignD", + label: label + "/AssignD/Equal", x: ts.AssignD(make(chan bool)), y: ts.AssignD(make(chan bool)), wantEqual: true, + reason: "Equal method called since named channel is assignable to unnamed channel", }} } @@ -2117,10 +2280,12 @@ func cycleTests() []test { var tests []test type XY struct{ x, y interface{} } for _, tt := range []struct { + label string in XY wantEqual bool reason string }{{ + label: "PointersEqual", in: func() XY { x := new(P) *x = x @@ -2129,7 +2294,9 @@ func cycleTests() []test { return XY{x, y} }(), wantEqual: true, + reason: "equal pair of single-node pointers", }, { + label: "PointersInequal", in: func() XY { x := new(P) *x = x @@ -2139,7 +2306,9 @@ func cycleTests() []test { return XY{x, y1} }(), wantEqual: false, + reason: "inequal pair of single-node and double-node pointers", }, { + label: "SlicesEqual", in: func() XY { x := S{nil} x[0] = x @@ -2148,7 +2317,9 @@ func cycleTests() []test { return XY{x, y} }(), wantEqual: true, + reason: "equal pair of single-node slices", }, { + label: "SlicesInequal", in: func() XY { x := S{nil} x[0] = x @@ -2158,7 +2329,9 @@ func cycleTests() []test { return XY{x, y1} }(), wantEqual: false, + reason: "inequal pair of single-node and double node slices", }, { + label: "MapsEqual", in: func() XY { x := M{0: nil} x[0] = x @@ -2167,7 +2340,9 @@ func cycleTests() []test { return XY{x, y} }(), wantEqual: true, + reason: "equal pair of single-node maps", }, { + label: "MapsInequal", in: func() XY { x := M{0: nil} x[0] = x @@ -2177,10 +2352,14 @@ func cycleTests() []test { return XY{x, y1} }(), wantEqual: false, + reason: "inequal pair of single-node and double-node maps", }, { + label: "GraphEqual", in: XY{makeGraph(), makeGraph()}, wantEqual: true, + reason: "graphs are equal since they have identical forms", }, { + label: "GraphInequalZeroed", in: func() XY { x := makeGraph() y := makeGraph() @@ -2190,7 +2369,9 @@ func cycleTests() []test { return XY{x, y} }(), wantEqual: false, + reason: "graphs are inequal because the ID fields are different", }, { + label: "GraphInequalStruct", in: func() XY { x := makeGraph() y := makeGraph() @@ -2201,9 +2382,10 @@ func cycleTests() []test { return XY{x, y} }(), wantEqual: false, + reason: "graphs are inequal because they differ on a map element", }} { tests = append(tests, test{ - label: label, + label: label + "/" + tt.label, x: tt.in.x, y: tt.in.y, wantEqual: tt.wantEqual, @@ -2273,7 +2455,7 @@ func project1Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported", x: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, @@ -2281,8 +2463,9 @@ func project1Tests() []test { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/ProtoEqual", x: ts.Eagle{Slaps: []ts.Slap{{ Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, @@ -2291,8 +2474,9 @@ func project1Tests() []test { }}}, opts: []cmp.Option{cmp.Comparer(pb.Equal)}, wantEqual: true, + reason: "simulated protobuf messages contain the same values", }, { - label: label, + label: label + "/ProtoInequal", x: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata"}}, }}}, @@ -2301,14 +2485,16 @@ func project1Tests() []test { }}}, opts: []cmp.Option{cmp.Comparer(pb.Equal)}, wantEqual: false, + reason: "simulated protobuf messages contain different values", }, { - label: label, + label: label + "/Equal", x: createEagle(), y: createEagle(), opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, wantEqual: true, + reason: "equal because values are the same", }, { - label: label, + label: label + "/Inequal", x: func() ts.Eagle { eg := createEagle() eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.ID = "southbay2" @@ -2324,6 +2510,7 @@ func project1Tests() []test { }(), opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, wantEqual: false, + reason: "inequal because some values are different", }} } @@ -2383,18 +2570,20 @@ func project2Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported", x: createBatch(), y: createBatch(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/Equal", x: createBatch(), y: createBatch(), opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, wantEqual: true, + reason: "equal because identical values are compared", }, { - label: label, + label: label + "/InequalOrder", x: createBatch(), y: func() ts.GermBatch { gb := createBatch() @@ -2404,8 +2593,9 @@ func project2Tests() []test { }(), opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, wantEqual: false, + reason: "inequal because slice contains elements in differing order", }, { - label: label, + label: label + "/EqualOrder", x: createBatch(), y: func() ts.GermBatch { gb := createBatch() @@ -2415,8 +2605,9 @@ func project2Tests() []test { }(), opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, wantEqual: true, + reason: "equal because unordered slice is sorted using transformer", }, { - label: label, + label: label + "/Inequal", x: func() ts.GermBatch { gb := createBatch() delete(gb.DirtyGerms, 17) @@ -2431,6 +2622,7 @@ func project2Tests() []test { }(), opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, wantEqual: false, + reason: "inequal because some values are different", }} } @@ -2468,24 +2660,27 @@ func project3Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported1", x: createDirt(), y: createDirt(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/PanicUnexported2", x: createDirt(), y: createDirt(), opts: []cmp.Option{allowVisibility, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", }, { - label: label, + label: label + "/Equal", x: createDirt(), y: createDirt(), opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", }, { - label: label, + label: label + "/Inequal", x: func() ts.Dirt { d := createDirt() d.SetTable(ts.CreateMockTable([]string{"a", "c"})) @@ -2502,6 +2697,7 @@ func project3Tests() []test { }(), opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, wantEqual: false, + reason: "inequal because some values are different", }} } @@ -2544,24 +2740,27 @@ func project4Tests() []test { } return []test{{ - label: label, + label: label + "/PanicUnexported1", x: createCartel(), y: createCartel(), wantPanic: "cannot handle unexported field", + reason: "struct contains unexported fields", }, { - label: label, + label: label + "/PanicUnexported2", x: createCartel(), y: createCartel(), opts: []cmp.Option{allowVisibility, cmp.Comparer(pb.Equal)}, wantPanic: "cannot handle unexported field", + reason: "struct contains references to simulated protobuf types with unexported fields", }, { - label: label, + label: label + "/Equal", x: createCartel(), y: createCartel(), opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, wantEqual: true, + reason: "transformer used to create reference to protobuf message so it works with pb.Equal", }, { - label: label, + label: label + "/Inequal", x: func() ts.Cartel { d := createCartel() var p1, p2 ts.Poison @@ -2581,6 +2780,7 @@ func project4Tests() []test { }(), opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, wantEqual: false, + reason: "inequal because some values are different", }} } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index c161591..baec97d 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1,54 +1,54 @@ -<<< TestDiff/Comparer#09 +<<< TestDiff/Comparer/StructInequal struct{ A int; B int; C int }{ A: 1, B: 2, - C: 3, + C: 4, } ->>> TestDiff/Comparer#09 -<<< TestDiff/Comparer#12 +>>> TestDiff/Comparer/StructInequal +<<< TestDiff/Comparer/PointerStructInequal &struct{ A *int }{ - A: &4, + A: &5, } ->>> TestDiff/Comparer#12 -<<< TestDiff/Comparer#16 +>>> TestDiff/Comparer/PointerStructInequal +<<< TestDiff/Comparer/StructNestedPointerInequal &struct{ R *bytes.Buffer }{ - R: s"", + R: nil, } ->>> TestDiff/Comparer#16 -<<< TestDiff/Comparer#23 +>>> TestDiff/Comparer/StructNestedPointerInequal +<<< TestDiff/Comparer/RegexpInequal []*regexp.Regexp{ nil, - s"a*b*c*", + s"a*b*d*", } ->>> TestDiff/Comparer#23 -<<< TestDiff/Comparer#25 +>>> TestDiff/Comparer/RegexpInequal +<<< TestDiff/Comparer/TriplePointerInequal &&&int( - 0, + 1, ) ->>> TestDiff/Comparer#25 -<<< TestDiff/Comparer#28 +>>> TestDiff/Comparer/TriplePointerInequal +<<< TestDiff/Comparer/StringerInequal struct{ fmt.Stringer }( - s"hello", + s"hello2", ) ->>> TestDiff/Comparer#28 -<<< TestDiff/Comparer#29 +>>> TestDiff/Comparer/StringerInequal +<<< TestDiff/Comparer/DifferingHash [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 +>>> TestDiff/Comparer/DifferingHash +<<< TestDiff/Comparer/NilStringer interface{}( - &fmt.Stringer(nil), ) ->>> TestDiff/Comparer#30 -<<< TestDiff/Comparer#31 +>>> TestDiff/Comparer/NilStringer +<<< TestDiff/Comparer/TarHeaders []cmp_test.tarHeader{ { ... // 4 identical fields @@ -101,8 +101,8 @@ ... // 6 identical fields }, } ->>> TestDiff/Comparer#31 -<<< TestDiff/Comparer#36 +>>> TestDiff/Comparer/TarHeaders +<<< TestDiff/Comparer/IrreflexiveComparison []int{ - Inverse(λ, float64(NaN)), + Inverse(λ, float64(NaN)), @@ -125,19 +125,19 @@ - Inverse(λ, float64(NaN)), + Inverse(λ, float64(NaN)), } ->>> TestDiff/Comparer#36 -<<< TestDiff/Comparer#37 +>>> TestDiff/Comparer/IrreflexiveComparison +<<< TestDiff/Comparer/StringerMapKey map[*testprotos.Stringer]*testprotos.Stringer( - {s"hello": s"world"}, + nil, ) ->>> TestDiff/Comparer#37 -<<< TestDiff/Comparer#38 +>>> TestDiff/Comparer/StringerMapKey +<<< TestDiff/Comparer/StringerBacktick interface{}( - []*testprotos.Stringer{s`multi\nline\nline\nline`}, ) ->>> TestDiff/Comparer#38 -<<< TestDiff/Comparer#42 +>>> TestDiff/Comparer/StringerBacktick +<<< TestDiff/Comparer/DynamicMap []interface{}{ map[string]interface{}{ "avg": float64(0.278), @@ -152,14 +152,14 @@ "name": string("Sammy Sosa"), }, } ->>> TestDiff/Comparer#42 -<<< TestDiff/Comparer#43 +>>> TestDiff/Comparer/DynamicMap +<<< TestDiff/Comparer/MapKeyPointer map[*int]string{ - &⟪0xdeadf00f⟫0: "hello", + &⟪0xdeadf00f⟫0: "world", } ->>> TestDiff/Comparer#43 -<<< TestDiff/Comparer#44 +>>> TestDiff/Comparer/MapKeyPointer +<<< TestDiff/Comparer/IgnoreSliceElements [2][]int{ {..., 1, 2, 3, ...}, { @@ -169,8 +169,8 @@ ... // 3 ignored elements }, } ->>> TestDiff/Comparer#44 -<<< TestDiff/Comparer#45 +>>> TestDiff/Comparer/IgnoreSliceElements +<<< TestDiff/Comparer/IgnoreMapEntries [2]map[string]int{ {"KEEP3": 3, "keep1": 1, "keep2": 2, ...}, { @@ -179,14 +179,14 @@ + "keep2": 2, }, } ->>> TestDiff/Comparer#45 -<<< TestDiff/Transformer +>>> TestDiff/Comparer/IgnoreMapEntries +<<< TestDiff/Transformer/Uints uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64( - 0, + 1, ))))))) ->>> TestDiff/Transformer -<<< TestDiff/Transformer#02 +>>> TestDiff/Transformer/Uints +<<< TestDiff/Transformer/Filtered []int{ Inverse(λ, int64(0)), - Inverse(λ, int64(-5)), @@ -195,14 +195,14 @@ - Inverse(λ, int64(-1)), + Inverse(λ, int64(-5)), } ->>> TestDiff/Transformer#02 -<<< TestDiff/Transformer#03 +>>> TestDiff/Transformer/Filtered +<<< TestDiff/Transformer/DisjointOutput int(Inverse(λ, interface{}( - string("zero"), + float64(1), ))) ->>> TestDiff/Transformer#03 -<<< TestDiff/Transformer#04 +>>> TestDiff/Transformer/DisjointOutput +<<< TestDiff/Transformer/JSON string(Inverse(ParseJSON, map[string]interface{}{ "address": map[string]interface{}{ - "city": string("Los Angeles"), @@ -228,8 +228,8 @@ }, + "spouse": nil, })) ->>> TestDiff/Transformer#04 -<<< TestDiff/Transformer#05 +>>> TestDiff/Transformer/JSON +<<< TestDiff/Transformer/AcyclicString cmp_test.StringBytes{ String: Inverse(SplitString, []string{ "some", @@ -251,7 +251,7 @@ }, })), } ->>> TestDiff/Transformer#05 +>>> TestDiff/Transformer/AcyclicString <<< TestDiff/Reporter/AmbiguousType interface{}( - "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, @@ -327,12 +327,12 @@ - s"hello": "goodbye", } >>> TestDiff/Reporter/NonAmbiguousStringerMapKey -<<< TestDiff//InvalidUTF8 +<<< TestDiff/Reporter/InvalidUTF8 interface{}( - cmp_test.MyString("\xed\xa0\x80"), ) ->>> TestDiff//InvalidUTF8 -<<< TestDiff/Reporter +>>> TestDiff/Reporter/InvalidUTF8 +<<< TestDiff/Reporter/UnbatchedSlice cmp_test.MyComposite{ ... // 3 identical fields BytesB: nil, @@ -350,8 +350,8 @@ IntsC: nil, ... // 6 identical fields } ->>> TestDiff/Reporter -<<< TestDiff/Reporter#01 +>>> TestDiff/Reporter/UnbatchedSlice +<<< TestDiff/Reporter/BatchedSlice cmp_test.MyComposite{ ... // 3 identical fields BytesB: nil, @@ -367,7 +367,7 @@ IntsC: nil, ... // 6 identical fields } ->>> TestDiff/Reporter#01 +>>> TestDiff/Reporter/BatchedSlice <<< TestDiff/Reporter/BatchedWithComparer cmp_test.MyComposite{ StringA: "", @@ -389,7 +389,7 @@ - cmp_test.MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, ...}}, ) >>> TestDiff/Reporter/BatchedLong -<<< TestDiff/Reporter#02 +<<< TestDiff/Reporter/BatchedNamedAndUnnamed cmp_test.MyComposite{ StringA: "", StringB: "", @@ -442,8 +442,8 @@ + 9.5, 8.5, 7.5, }, } ->>> TestDiff/Reporter#02 -<<< TestDiff/Reporter#03 +>>> TestDiff/Reporter/BatchedNamedAndUnnamed +<<< TestDiff/Reporter/BinaryHexdump cmp_test.MyComposite{ StringA: "", StringB: "", @@ -464,8 +464,8 @@ BytesC: nil, ... // 9 identical fields } ->>> TestDiff/Reporter#03 -<<< TestDiff/Reporter#04 +>>> TestDiff/Reporter/BinaryHexdump +<<< TestDiff/Reporter/StringHexdump cmp_test.MyComposite{ StringA: "", StringB: cmp_test.MyString{ @@ -489,8 +489,8 @@ BytesB: nil, ... // 10 identical fields } ->>> TestDiff/Reporter#04 -<<< TestDiff/Reporter#05 +>>> TestDiff/Reporter/StringHexdump +<<< TestDiff/Reporter/BinaryString cmp_test.MyComposite{ StringA: "", StringB: "", @@ -507,7 +507,7 @@ BytesC: nil, ... // 9 identical fields } ->>> TestDiff/Reporter#05 +>>> TestDiff/Reporter/BinaryString <<< TestDiff/Reporter/TripleQuote cmp_test.MyComposite{ StringA: ( @@ -875,7 +875,7 @@ ... // 12 identical and 10 modified elements } >>> TestDiff/Reporter/LimitMaximumSliceDiffs -<<< TestDiff/Reporter#06 +<<< TestDiff/Reporter/MultilineString cmp_test.MyComposite{ StringA: ( """ @@ -902,8 +902,8 @@ BytesA: nil, ... // 11 identical fields } ->>> TestDiff/Reporter#06 -<<< TestDiff/Reporter#07 +>>> TestDiff/Reporter/MultilineString +<<< TestDiff/Reporter/Slices cmp_test.MyComposite{ StringA: "", StringB: "", @@ -932,8 +932,8 @@ - FloatsC: cmp_test.MyFloats{7.5, 8.5, 9.5}, + FloatsC: nil, } ->>> TestDiff/Reporter#07 -<<< TestDiff/Reporter#08 +>>> TestDiff/Reporter/Slices +<<< TestDiff/Reporter/EmptySlices cmp_test.MyComposite{ StringA: "", StringB: "", @@ -962,8 +962,8 @@ - FloatsC: cmp_test.MyFloats{}, + FloatsC: nil, } ->>> TestDiff/Reporter#08 -<<< TestDiff/EmbeddedStruct/ParentStructA#04 +>>> TestDiff/Reporter/EmptySlices +<<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ - Public: 1, @@ -972,8 +972,8 @@ + private: 3, }, } ->>> TestDiff/EmbeddedStruct/ParentStructA#04 -<<< TestDiff/EmbeddedStruct/ParentStructB#04 +>>> TestDiff/EmbeddedStruct/ParentStructA/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructB/Inequal teststructs.ParentStructB{ PublicStruct: teststructs.PublicStruct{ - Public: 1, @@ -982,8 +982,8 @@ + private: 3, }, } ->>> TestDiff/EmbeddedStruct/ParentStructB#04 -<<< TestDiff/EmbeddedStruct/ParentStructC#04 +>>> TestDiff/EmbeddedStruct/ParentStructB/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructC/Inequal teststructs.ParentStructC{ privateStruct: teststructs.privateStruct{ - Public: 1, @@ -996,8 +996,8 @@ - private: 4, + private: 5, } ->>> TestDiff/EmbeddedStruct/ParentStructC#04 -<<< TestDiff/EmbeddedStruct/ParentStructD#04 +>>> TestDiff/EmbeddedStruct/ParentStructC/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructD/Inequal teststructs.ParentStructD{ PublicStruct: teststructs.PublicStruct{ - Public: 1, @@ -1010,8 +1010,8 @@ - private: 4, + private: 5, } ->>> TestDiff/EmbeddedStruct/ParentStructD#04 -<<< TestDiff/EmbeddedStruct/ParentStructE#05 +>>> TestDiff/EmbeddedStruct/ParentStructD/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructE/Inequal teststructs.ParentStructE{ privateStruct: teststructs.privateStruct{ - Public: 1, @@ -1026,8 +1026,8 @@ + private: 5, }, } ->>> TestDiff/EmbeddedStruct/ParentStructE#05 -<<< TestDiff/EmbeddedStruct/ParentStructF#05 +>>> TestDiff/EmbeddedStruct/ParentStructE/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructF/Inequal teststructs.ParentStructF{ privateStruct: teststructs.privateStruct{ - Public: 1, @@ -1046,8 +1046,8 @@ - private: 6, + private: 7, } ->>> TestDiff/EmbeddedStruct/ParentStructF#05 -<<< TestDiff/EmbeddedStruct/ParentStructG#04 +>>> TestDiff/EmbeddedStruct/ParentStructF/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructG/Inequal &teststructs.ParentStructG{ privateStruct: &teststructs.privateStruct{ - Public: 1, @@ -1056,8 +1056,8 @@ + private: 3, }, } ->>> TestDiff/EmbeddedStruct/ParentStructG#04 -<<< TestDiff/EmbeddedStruct/ParentStructH#05 +>>> TestDiff/EmbeddedStruct/ParentStructG/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructH/Inequal &teststructs.ParentStructH{ PublicStruct: &teststructs.PublicStruct{ - Public: 1, @@ -1066,8 +1066,8 @@ + private: 3, }, } ->>> TestDiff/EmbeddedStruct/ParentStructH#05 -<<< TestDiff/EmbeddedStruct/ParentStructI#06 +>>> TestDiff/EmbeddedStruct/ParentStructH/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructI/Inequal &teststructs.ParentStructI{ privateStruct: &teststructs.privateStruct{ - Public: 1, @@ -1082,8 +1082,8 @@ + private: 5, }, } ->>> TestDiff/EmbeddedStruct/ParentStructI#06 -<<< TestDiff/EmbeddedStruct/ParentStructJ#05 +>>> TestDiff/EmbeddedStruct/ParentStructI/Inequal +<<< TestDiff/EmbeddedStruct/ParentStructJ/Inequal &teststructs.ParentStructJ{ privateStruct: &teststructs.privateStruct{ - Public: 1, @@ -1110,136 +1110,136 @@ + private: 7, }, } ->>> TestDiff/EmbeddedStruct/ParentStructJ#05 -<<< TestDiff/EqualMethod/StructB +>>> TestDiff/EmbeddedStruct/ParentStructJ/Inequal +<<< TestDiff/EqualMethod/StructB/ValueInequal teststructs.StructB{ - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructB -<<< TestDiff/EqualMethod/StructD +>>> TestDiff/EqualMethod/StructB/ValueInequal +<<< TestDiff/EqualMethod/StructD/ValueInequal teststructs.StructD{ - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructD -<<< TestDiff/EqualMethod/StructE +>>> TestDiff/EqualMethod/StructD/ValueInequal +<<< TestDiff/EqualMethod/StructE/ValueInequal teststructs.StructE{ - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructE -<<< TestDiff/EqualMethod/StructF +>>> TestDiff/EqualMethod/StructE/ValueInequal +<<< TestDiff/EqualMethod/StructF/ValueInequal teststructs.StructF{ - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructF -<<< TestDiff/EqualMethod/StructA1#01 +>>> TestDiff/EqualMethod/StructF/ValueInequal +<<< TestDiff/EqualMethod/StructA1/ValueInequal teststructs.StructA1{ StructA: {X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructA1#01 -<<< TestDiff/EqualMethod/StructA1#03 +>>> TestDiff/EqualMethod/StructA1/ValueInequal +<<< TestDiff/EqualMethod/StructA1/PointerInequal &teststructs.StructA1{ StructA: {X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructA1#03 -<<< TestDiff/EqualMethod/StructB1#01 +>>> TestDiff/EqualMethod/StructA1/PointerInequal +<<< TestDiff/EqualMethod/StructB1/ValueInequal teststructs.StructB1{ - StructB: Inverse(Ref, &teststructs.StructB{X: "NotEqual"}), + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructB1#01 -<<< TestDiff/EqualMethod/StructB1#03 +>>> TestDiff/EqualMethod/StructB1/ValueInequal +<<< TestDiff/EqualMethod/StructB1/PointerInequal &teststructs.StructB1{ - StructB: Inverse(Ref, &teststructs.StructB{X: "NotEqual"}), + StructB: Inverse(Addr, &teststructs.StructB{X: "NotEqual"}), - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructB1#03 -<<< TestDiff/EqualMethod/StructD1 +>>> TestDiff/EqualMethod/StructB1/PointerInequal +<<< TestDiff/EqualMethod/StructD1/ValueInequal teststructs.StructD1{ - StructD: teststructs.StructD{X: "NotEqual"}, + StructD: teststructs.StructD{X: "not_equal"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructD1 -<<< TestDiff/EqualMethod/StructE1 +>>> TestDiff/EqualMethod/StructD1/ValueInequal +<<< TestDiff/EqualMethod/StructE1/ValueInequal teststructs.StructE1{ - StructE: teststructs.StructE{X: "NotEqual"}, + StructE: teststructs.StructE{X: "not_equal"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructE1 -<<< TestDiff/EqualMethod/StructF1 +>>> TestDiff/EqualMethod/StructE1/ValueInequal +<<< TestDiff/EqualMethod/StructF1/ValueInequal teststructs.StructF1{ - StructF: teststructs.StructF{X: "NotEqual"}, + StructF: teststructs.StructF{X: "not_equal"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructF1 -<<< TestDiff/EqualMethod/StructA2#01 +>>> TestDiff/EqualMethod/StructF1/ValueInequal +<<< TestDiff/EqualMethod/StructA2/ValueInequal teststructs.StructA2{ StructA: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructA2#01 -<<< TestDiff/EqualMethod/StructA2#03 +>>> TestDiff/EqualMethod/StructA2/ValueInequal +<<< TestDiff/EqualMethod/StructA2/PointerInequal &teststructs.StructA2{ StructA: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructA2#03 -<<< TestDiff/EqualMethod/StructB2#01 +>>> TestDiff/EqualMethod/StructA2/PointerInequal +<<< TestDiff/EqualMethod/StructB2/ValueInequal teststructs.StructB2{ StructB: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructB2#01 -<<< TestDiff/EqualMethod/StructB2#03 +>>> TestDiff/EqualMethod/StructB2/ValueInequal +<<< TestDiff/EqualMethod/StructB2/PointerInequal &teststructs.StructB2{ StructB: &{X: "NotEqual"}, - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructB2#03 -<<< TestDiff/EqualMethod/StructNo +>>> TestDiff/EqualMethod/StructB2/PointerInequal +<<< TestDiff/EqualMethod/StructNo/Inequal teststructs.StructNo{ - X: "NotEqual", + X: "not_equal", } ->>> TestDiff/EqualMethod/StructNo -<<< TestDiff/Cycle#01 +>>> TestDiff/EqualMethod/StructNo/Inequal +<<< TestDiff/Cycle/PointersInequal &&cmp_test.P( - &⟪0xdeadf00f⟫, + &&⟪0xdeadf00f⟫, ) ->>> TestDiff/Cycle#01 -<<< TestDiff/Cycle#03 +>>> TestDiff/Cycle/PointersInequal +<<< TestDiff/Cycle/SlicesInequal cmp_test.S{ - {{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}, + {{{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}}, } ->>> TestDiff/Cycle#03 -<<< TestDiff/Cycle#05 +>>> TestDiff/Cycle/SlicesInequal +<<< TestDiff/Cycle/MapsInequal cmp_test.M{ - 0: {0: ⟪0xdeadf00f⟫}, + 0: {0: {0: ⟪0xdeadf00f⟫}}, } ->>> TestDiff/Cycle#05 -<<< TestDiff/Cycle#07 +>>> TestDiff/Cycle/MapsInequal +<<< TestDiff/Cycle/GraphInequalZeroed map[string]*cmp_test.CycleAlpha{ "Bar": &{ Name: "Bar", @@ -1352,8 +1352,8 @@ }, }, } ->>> TestDiff/Cycle#07 -<<< TestDiff/Cycle#08 +>>> TestDiff/Cycle/GraphInequalZeroed +<<< TestDiff/Cycle/GraphInequalStruct map[string]*cmp_test.CycleAlpha{ "Bar": &{ Name: "Bar", @@ -1437,8 +1437,8 @@ }, "Foo": &{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &{Name: "Foo", Bravos: {...}}}}}}, } ->>> TestDiff/Cycle#08 -<<< TestDiff/Project1#02 +>>> TestDiff/Cycle/GraphInequalStruct +<<< TestDiff/Project1/ProtoInequal teststructs.Eagle{ ... // 4 identical fields Dreamers: nil, @@ -1462,8 +1462,8 @@ PrankRating: "", ... // 2 identical fields } ->>> TestDiff/Project1#02 -<<< TestDiff/Project1#04 +>>> TestDiff/Project1/ProtoInequal +<<< TestDiff/Project1/Inequal teststructs.Eagle{ ... // 2 identical fields Desc: "some description", @@ -1535,8 +1535,8 @@ PrankRating: "", ... // 2 identical fields } ->>> TestDiff/Project1#04 -<<< TestDiff/Project2#02 +>>> TestDiff/Project1/Inequal +<<< TestDiff/Project2/InequalOrder teststructs.GermBatch{ DirtyGerms: map[int32][]*testprotos.Germ{ 17: {s"germ1"}, @@ -1551,8 +1551,8 @@ GermMap: {13: s"germ13", 21: s"germ21"}, ... // 7 identical fields } ->>> TestDiff/Project2#02 -<<< TestDiff/Project2#04 +>>> TestDiff/Project2/InequalOrder +<<< TestDiff/Project2/Inequal teststructs.GermBatch{ DirtyGerms: map[int32][]*testprotos.Germ{ + 17: {s"germ1"}, @@ -1578,8 +1578,8 @@ TotalDirtyGerms: 0, InfectedAt: s"2009-11-10 23:00:00 +0000 UTC", } ->>> TestDiff/Project2#04 -<<< TestDiff/Project3#03 +>>> TestDiff/Project2/Inequal +<<< TestDiff/Project3/Inequal teststructs.Dirt{ - table: &teststructs.MockTable{state: []string{"a", "c"}}, + table: &teststructs.MockTable{state: []string{"a", "b", "c"}}, @@ -1597,8 +1597,8 @@ lastTime: 54321, ... // 1 ignored field } ->>> TestDiff/Project3#03 -<<< TestDiff/Project4#03 +>>> TestDiff/Project3/Inequal +<<< TestDiff/Project4/Inequal teststructs.Cartel{ Headquarter: teststructs.Headquarter{ id: 5, @@ -1639,4 +1639,4 @@ - &{poisonType: 2, manufacturer: "acme2"}, }, } ->>> TestDiff/Project4#03 +>>> TestDiff/Project4/Inequal From 77ae86f624cb174e21763cffcbbf070eb06cb016 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 17 Jun 2020 17:25:47 -0700 Subject: [PATCH 54/99] Improve reporting of values with cycles (#217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the reporter could handle formatting values with cycles in that it did not crash with a stack overflow. However, the output was not particularly understandable as it did not surface to the user why a particular value was truncated, and if it was truncated due to a cyclic reference, what was the referent. This change annotates the reporter tree with pointer information so that a later pass can inject reference information if it is needed to produce more understandable output. Consider the following example: map[string]*cmp_test.CycleAlpha{ "Foo": &⟪ref#0⟫{ Name: "Foo", Bravos: map[string]*cmp_test.CycleBravo{ "FooBravo": &{ - ID: 101, + ID: 0, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &⟪ref#0⟫(...)}, }, }, }, } This graph contains a cycle. To ensure that a graph can be formatted, the cycle is truncated as indicated with: &⟪ref#0⟫(...). The referent was identified earlier with: &⟪ref#0⟫{...}. --- cmp/internal/value/pointer_purego.go | 10 + cmp/internal/value/pointer_unsafe.go | 10 + cmp/report.go | 5 +- cmp/report_compare.go | 85 +++++---- cmp/report_references.go | 264 +++++++++++++++++++++++++++ cmp/report_reflect.go | 156 ++++++++-------- cmp/report_slices.go | 10 +- cmp/report_text.go | 21 ++- cmp/testdata/diffs | 129 ++++++------- 9 files changed, 490 insertions(+), 200 deletions(-) create mode 100644 cmp/report_references.go diff --git a/cmp/internal/value/pointer_purego.go b/cmp/internal/value/pointer_purego.go index 0a01c47..e9e384a 100644 --- a/cmp/internal/value/pointer_purego.go +++ b/cmp/internal/value/pointer_purego.go @@ -21,3 +21,13 @@ func PointerOf(v reflect.Value) Pointer { // assumes that the GC implementation does not use a moving collector. return Pointer{v.Pointer(), v.Type()} } + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == 0 +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return p.p +} diff --git a/cmp/internal/value/pointer_unsafe.go b/cmp/internal/value/pointer_unsafe.go index da134ae..b50c17e 100644 --- a/cmp/internal/value/pointer_unsafe.go +++ b/cmp/internal/value/pointer_unsafe.go @@ -24,3 +24,13 @@ func PointerOf(v reflect.Value) Pointer { // which is necessary if the GC ever uses a moving collector. return Pointer{unsafe.Pointer(v.Pointer()), v.Type()} } + +// IsNil reports whether the pointer is nil. +func (p Pointer) IsNil() bool { + return p.p == nil +} + +// Uintptr returns the pointer as a uintptr. +func (p Pointer) Uintptr() uintptr { + return uintptr(p.p) +} diff --git a/cmp/report.go b/cmp/report.go index 6ddf299..aafcb36 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -41,7 +41,10 @@ func (r *defaultReporter) String() string { if r.root.NumDiff == 0 { return "" } - return formatOptions{}.FormatDiff(r.root).String() + ptrs := new(pointerReferences) + text := formatOptions{}.FormatDiff(r.root, ptrs) + resolveReferences(text) + return text.String() } func assert(ok bool) { diff --git a/cmp/report_compare.go b/cmp/report_compare.go index be03a25..9e21809 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -97,7 +97,7 @@ func verbosityPreset(opts formatOptions, i int) formatOptions { // 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 { +func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { if opts.DiffMode == diffIdentical { opts = opts.WithVerbosity(1) } else { @@ -110,9 +110,9 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { 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 + var parentKind reflect.Kind + if v.parent != nil && v.parent.TransformerName == "" { + parentKind = v.parent.Type.Kind() } // For leaf nodes, format the value based on the reflect.Values alone. @@ -121,8 +121,8 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { case diffUnknown, diffIdentical: // Format Equal. if v.NumDiff == 0 { - outx := opts.FormatValue(v.ValueX, withinSlice, visitedPointers{}) - outy := opts.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + outx := opts.FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.FormatValue(v.ValueY, parentKind, ptrs) if v.NumIgnored > 0 && v.NumSame == 0 { return textEllipsis } else if outx.Len() < outy.Len() { @@ -135,12 +135,12 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { // 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{}) + outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs) + outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs) for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { opts2 := verbosityPreset(opts, i).WithTypeMode(elideType) - outx = opts2.FormatValue(v.ValueX, withinSlice, visitedPointers{}) - outy = opts2.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + outx = opts2.FormatValue(v.ValueX, parentKind, ptrs) + outy = opts2.FormatValue(v.ValueY, parentKind, ptrs) } if outx != nil { list = append(list, textRecord{Diff: '-', Value: outx}) @@ -150,36 +150,57 @@ func (opts formatOptions) FormatDiff(v *valueNode) textNode { } return opts.WithTypeMode(emitType).FormatType(v.Type, list) case diffRemoved: - return opts.FormatValue(v.ValueX, withinSlice, visitedPointers{}) + return opts.FormatValue(v.ValueX, parentKind, ptrs) case diffInserted: - return opts.FormatValue(v.ValueY, withinSlice, visitedPointers{}) + return opts.FormatValue(v.ValueY, parentKind, ptrs) default: panic("invalid diff mode") } } - // TODO: Print cycle reference for pointers, maps, and elements of a slice. + // Register slice element to support cycle detection. + if parentKind == reflect.Slice { + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true) + defer ptrs.Pop() + defer func() { out = wrapTrunkReferences(ptrRefs, out) }() + } // Descend into the child value node. if v.TransformerName != "" { - out := opts.WithTypeMode(emitType).FormatDiff(v.Value) - out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"} + out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) + out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} return opts.FormatType(v.Type, out) } else { switch k := v.Type.Kind(); k { - case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map: - return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k)) + case reflect.Struct, reflect.Array, reflect.Slice: + out = opts.formatDiffList(v.Records, k, ptrs) + out = opts.FormatType(v.Type, out) + case reflect.Map: + // Register map to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.formatDiffList(v.Records, k, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = opts.FormatType(v.Type, out) case reflect.Ptr: - return textWrap{"&", opts.FormatDiff(v.Value), ""} + // Register pointer to support cycle detection. + ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false) + defer ptrs.Pop() + + out = opts.FormatDiff(v.Value, ptrs) + out = wrapTrunkReferences(ptrRefs, out) + out = &textWrap{Prefix: "&", Value: out} case reflect.Interface: - return opts.WithTypeMode(emitType).FormatDiff(v.Value) + out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) default: panic(fmt.Sprintf("%v cannot have children", k)) } + return out } } -func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode { +func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { // Derive record name based on the data structure kind. var name string var formatKey func(reflect.Value) string @@ -195,7 +216,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te case reflect.Map: name = "entry" opts = opts.WithTypeMode(elideType) - formatKey = func(v reflect.Value) string { return formatMapKey(v, false) } + formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) } } maxLen := -1 @@ -242,14 +263,14 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te } continue } - if out := opts.FormatDiff(r.Value); out != nil { + if out := opts.FormatDiff(r.Value, ptrs); out != nil { list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) } } if deferredEllipsis { list.AppendEllipsis(diffStats{}) } - return textWrap{"{", list, "}"} + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} case diffUnknown: default: panic("invalid diff mode") @@ -290,7 +311,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te // Format the equal values. for _, r := range recs[:numLo] { - out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value) + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) keys = append(keys, r.Key) } @@ -302,7 +323,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te } } for _, r := range recs[numEqual-numHi : numEqual] { - out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value) + out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) keys = append(keys, r.Key) } @@ -318,12 +339,12 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) keys = append(keys, r.Key) case r.Value.NumChildren == r.Value.MaxDepth: - outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value) - outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value) + outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ { opts2 := verbosityPreset(opts, i) - outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value) - outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value) + outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs) + outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs) } if outx != nil { list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) @@ -334,7 +355,7 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te keys = append(keys, r.Key) } default: - out := opts.FormatDiff(r.Value) + out := opts.FormatDiff(r.Value, ptrs) list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) keys = append(keys, r.Key) } @@ -373,13 +394,13 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) te if ambiguous { for i, k := range keys { if k.IsValid() { - list[i].Key = formatMapKey(k, true) + list[i].Key = formatMapKey(k, true, ptrs) } } } } - return textWrap{"{", list, "}"} + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} } // coalesceAdjacentRecords coalesces the list of records into groups of diff --git a/cmp/report_references.go b/cmp/report_references.go new file mode 100644 index 0000000..d620c2c --- /dev/null +++ b/cmp/report_references.go @@ -0,0 +1,264 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/flags" + "github.com/google/go-cmp/cmp/internal/value" +) + +const ( + pointerDelimPrefix = "⟪" + pointerDelimSuffix = "⟫" +) + +// formatPointer prints the address of the pointer. +func formatPointer(p value.Pointer, withDelims bool) string { + v := p.Uintptr() + if flags.Deterministic { + v = 0xdeadf00f // Only used for stable testing purposes + } + if withDelims { + return pointerDelimPrefix + formatHex(uint64(v)) + pointerDelimSuffix + } + return formatHex(uint64(v)) +} + +// pointerReferences is a stack of pointers visited so far. +type pointerReferences [][2]value.Pointer + +func (ps *pointerReferences) PushPair(vx, vy reflect.Value, d diffMode, deref bool) (pp [2]value.Pointer) { + if deref && vx.IsValid() { + vx = vx.Addr() + } + if deref && vy.IsValid() { + vy = vy.Addr() + } + switch d { + case diffUnknown, diffIdentical: + pp = [2]value.Pointer{value.PointerOf(vx), value.PointerOf(vy)} + case diffRemoved: + pp = [2]value.Pointer{value.PointerOf(vx), value.Pointer{}} + case diffInserted: + pp = [2]value.Pointer{value.Pointer{}, value.PointerOf(vy)} + } + *ps = append(*ps, pp) + return pp +} + +func (ps *pointerReferences) Push(v reflect.Value) (p value.Pointer, seen bool) { + p = value.PointerOf(v) + for _, pp := range *ps { + if p == pp[0] || p == pp[1] { + return p, true + } + } + *ps = append(*ps, [2]value.Pointer{p, p}) + return p, false +} + +func (ps *pointerReferences) Pop() { + *ps = (*ps)[:len(*ps)-1] +} + +// trunkReferences is metadata for a textNode indicating that the sub-tree +// represents the value for either pointer in a pair of references. +type trunkReferences struct{ pp [2]value.Pointer } + +// trunkReference is metadata for a textNode indicating that the sub-tree +// represents the value for the given pointer reference. +type trunkReference struct{ p value.Pointer } + +// leafReference is metadata for a textNode indicating that the value is +// truncated as it refers to another part of the tree (i.e., a trunk). +type leafReference struct{ p value.Pointer } + +func wrapTrunkReferences(pp [2]value.Pointer, s textNode) textNode { + switch { + case pp[0].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[1]}} + case pp[1].IsNil(): + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + case pp[0] == pp[1]: + return &textWrap{Value: s, Metadata: trunkReference{pp[0]}} + default: + return &textWrap{Value: s, Metadata: trunkReferences{pp}} + } +} +func wrapTrunkReference(p value.Pointer, printAddress bool, s textNode) textNode { + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: s, Metadata: trunkReference{p}} +} +func makeLeafReference(p value.Pointer, printAddress bool) textNode { + out := &textWrap{Prefix: "(", Value: textEllipsis, Suffix: ")"} + var prefix string + if printAddress { + prefix = formatPointer(p, true) + } + return &textWrap{Prefix: prefix, Value: out, Metadata: leafReference{p}} +} + +// resolveReferences walks the textNode tree searching for any leaf reference +// metadata and resolves each against the corresponding trunk references. +// Since pointer addresses in memory are not particularly readable to the user, +// it replaces each pointer value with an arbitrary and unique reference ID. +func resolveReferences(s textNode) { + var walkNodes func(textNode, func(textNode)) + walkNodes = func(s textNode, f func(textNode)) { + f(s) + switch s := s.(type) { + case *textWrap: + walkNodes(s.Value, f) + case textList: + for _, r := range s { + walkNodes(r.Value, f) + } + } + } + + // Collect all trunks and leaves with reference metadata. + var trunks, leaves []*textWrap + walkNodes(s, func(s textNode) { + if s, ok := s.(*textWrap); ok { + switch s.Metadata.(type) { + case leafReference: + leaves = append(leaves, s) + case trunkReference, trunkReferences: + trunks = append(trunks, s) + } + } + }) + + // No leaf references to resolve. + if len(leaves) == 0 { + return + } + + // Collect the set of all leaf references to resolve. + leafPtrs := make(map[value.Pointer]bool) + for _, leaf := range leaves { + leafPtrs[leaf.Metadata.(leafReference).p] = true + } + + // Collect the set of trunk pointers that are always paired together. + // This allows us to assign a single ID to both pointers for brevity. + // If a pointer in a pair ever occurs by itself or as a different pair, + // then the pair is broken. + pairedTrunkPtrs := make(map[value.Pointer]value.Pointer) + unpair := func(p value.Pointer) { + if !pairedTrunkPtrs[p].IsNil() { + pairedTrunkPtrs[pairedTrunkPtrs[p]] = value.Pointer{} // invalidate other half + } + pairedTrunkPtrs[p] = value.Pointer{} // invalidate this half + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + unpair(p.p) // standalone pointer cannot be part of a pair + case trunkReferences: + p0, ok0 := pairedTrunkPtrs[p.pp[0]] + p1, ok1 := pairedTrunkPtrs[p.pp[1]] + switch { + case !ok0 && !ok1: + // Register the newly seen pair. + pairedTrunkPtrs[p.pp[0]] = p.pp[1] + pairedTrunkPtrs[p.pp[1]] = p.pp[0] + case ok0 && ok1 && p0 == p.pp[1] && p1 == p.pp[0]: + // Exact pair already seen; do nothing. + default: + // Pair conflicts with some other pair; break all pairs. + unpair(p.pp[0]) + unpair(p.pp[1]) + } + } + } + + // Correlate each pointer referenced by leaves to a unique identifier, + // and print the IDs for each trunk that matches those pointers. + var nextID uint + ptrIDs := make(map[value.Pointer]uint) + newID := func() uint { + id := nextID + nextID++ + return id + } + for _, trunk := range trunks { + switch p := trunk.Metadata.(type) { + case trunkReference: + if print := leafPtrs[p.p]; print { + id, ok := ptrIDs[p.p] + if !ok { + id = newID() + ptrIDs[p.p] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } + case trunkReferences: + print0 := leafPtrs[p.pp[0]] + print1 := leafPtrs[p.pp[1]] + if print0 || print1 { + id0, ok0 := ptrIDs[p.pp[0]] + id1, ok1 := ptrIDs[p.pp[1]] + isPair := pairedTrunkPtrs[p.pp[0]] == p.pp[1] && pairedTrunkPtrs[p.pp[1]] == p.pp[0] + if isPair { + var id uint + assert(ok0 == ok1) // must be seen together or not at all + if ok0 { + assert(id0 == id1) // must have the same ID + id = id0 + } else { + id = newID() + ptrIDs[p.pp[0]] = id + ptrIDs[p.pp[1]] = id + } + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id)) + } else { + if print0 && !ok0 { + id0 = newID() + ptrIDs[p.pp[0]] = id0 + } + if print1 && !ok1 { + id1 = newID() + ptrIDs[p.pp[1]] = id1 + } + switch { + case print0 && print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)+","+formatReference(id1)) + case print0: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id0)) + case print1: + trunk.Prefix = updateReferencePrefix(trunk.Prefix, formatReference(id1)) + } + } + } + } + } + + // Update all leaf references with the unique identifier. + for _, leaf := range leaves { + if id, ok := ptrIDs[leaf.Metadata.(leafReference).p]; ok { + leaf.Prefix = updateReferencePrefix(leaf.Prefix, formatReference(id)) + } + } +} + +func formatReference(id uint) string { + return fmt.Sprintf("ref#%d", id) +} + +func updateReferencePrefix(prefix, ref string) string { + if prefix == "" { + return pointerDelimPrefix + ref + pointerDelimSuffix + } + suffix := strings.TrimPrefix(prefix, pointerDelimPrefix) + return pointerDelimPrefix + ref + ": " + suffix +} diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 8b4325d..2d722ea 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -12,7 +12,6 @@ import ( "unicode" "unicode/utf8" - "github.com/google/go-cmp/cmp/internal/flags" "github.com/google/go-cmp/cmp/internal/value" ) @@ -21,11 +20,6 @@ type formatValueOptions struct { // methods like error.Error or fmt.Stringer.String. AvoidStringer bool - // PrintShallowPointer controls whether to print the next pointer. - // Useful when printing map keys, where pointer comparison is performed - // on the pointer address rather than the pointed-at value. - PrintShallowPointer bool - // PrintAddresses controls whether to print the address of all pointers, // slice elements, and maps. PrintAddresses bool @@ -75,26 +69,56 @@ func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode { typeName = "(" + typeName + ")" } } + return &textWrap{Prefix: typeName, Value: wrapParens(s)} +} + +// wrapParens wraps s with a set of parenthesis, but avoids it if the +// wrapped node itself is already surrounded by a pair of parenthesis or braces. +// It handles unwrapping one level of pointer-reference nodes. +func wrapParens(s textNode) textNode { + var refNode *textWrap + if s2, ok := s.(*textWrap); ok { + // Unwrap a single pointer reference node. + switch s2.Metadata.(type) { + case leafReference, trunkReference, trunkReferences: + refNode = s2 + if s3, ok := refNode.Value.(*textWrap); ok { + s2 = s3 + } + } - // 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, "}") + // Already has delimiters that make parenthesis unnecessary. + hasParens := strings.HasPrefix(s2.Prefix, "(") && strings.HasSuffix(s2.Suffix, ")") + hasBraces := strings.HasPrefix(s2.Prefix, "{") && strings.HasSuffix(s2.Suffix, "}") if hasParens || hasBraces { - return textWrap{typeName, s, ""} + return s } } - return textWrap{typeName + "(", s, ")"} + if refNode != nil { + refNode.Value = &textWrap{Prefix: "(", Value: refNode.Value, Suffix: ")"} + return s + } + return &textWrap{Prefix: "(", Value: s, Suffix: ")"} } // FormatValue prints the reflect.Value, taking extra care to avoid descending -// into pointers already in m. As pointers are visited, m is also updated. -func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visitedPointers) (out textNode) { +// into pointers already in ptrs. As pointers are visited, ptrs is also updated. +func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, ptrs *pointerReferences) (out textNode) { if !v.IsValid() { return nil } t := v.Type() + // Check slice element for cycles. + if parentKind == reflect.Slice { + ptrRef, visited := ptrs.Push(v.Addr()) + if visited { + return makeLeafReference(ptrRef, false) + } + defer ptrs.Pop() + defer func() { out = wrapTrunkReference(ptrRef, false, out) }() + } + // Check whether there is an Error or String method to call. if !opts.AvoidStringer && v.CanInterface() { // Avoid calling Error or String methods on nil receivers since many @@ -128,7 +152,6 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit } }() - var ptr string switch t.Kind() { case reflect.Bool: return textLine(fmt.Sprint(v.Bool())) @@ -137,7 +160,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: return textLine(fmt.Sprint(v.Uint())) case reflect.Uint8: - if withinSlice { + if parentKind == reflect.Slice || parentKind == reflect.Array { return textLine(formatHex(v.Uint())) } return textLine(fmt.Sprint(v.Uint())) @@ -157,7 +180,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit } return textLine(formatString(v.String())) case reflect.UnsafePointer, reflect.Chan, reflect.Func: - return textLine(formatPointer(v)) + return textLine(formatPointer(value.PointerOf(v), true)) case reflect.Struct: var list textList v := makeAddressable(v) // needed for retrieveUnexportedField @@ -179,17 +202,14 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit if supportExporters && !isExported(sf.Name) { vv = retrieveUnexportedField(v, sf, true) } - s := opts.WithTypeMode(autoType).FormatValue(vv, false, m) + s := opts.WithTypeMode(autoType).FormatValue(vv, t.Kind(), ptrs) list = append(list, textRecord{Key: sf.Name, Value: s}) } - return textWrap{"{", list, "}"} + return &textWrap{Prefix: "{", Value: list, Suffix: "}"} case reflect.Slice: if v.IsNil() { return textNil } - if opts.PrintAddresses { - ptr = fmt.Sprintf("⟪ptr:0x%x, len:%d, cap:%d⟫", pointerValue(v), v.Len(), v.Cap()) - } fallthrough case reflect.Array: maxLen := v.Len() @@ -203,29 +223,27 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit list.AppendEllipsis(diffStats{}) break } - 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) + s := opts.WithTypeMode(elideType).FormatValue(v.Index(i), t.Kind(), ptrs) list = append(list, textRecord{Value: s}) } - return textWrap{ptr + "{", list, "}"} + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + if t.Kind() == reflect.Slice && opts.PrintAddresses { + header := fmt.Sprintf("ptr:%v, len:%d, cap:%d", formatPointer(value.PointerOf(v), false), v.Len(), v.Cap()) + out = &textWrap{Prefix: pointerDelimPrefix + header + pointerDelimSuffix, Value: out} + } + return out case reflect.Map: if v.IsNil() { return textNil } - if m.Visit(v) { - return textLine(formatPointer(v)) + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + return makeLeafReference(ptrRef, opts.PrintAddresses) } + defer ptrs.Pop() maxLen := v.Len() if opts.LimitVerbosity { @@ -238,27 +256,32 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit list.AppendEllipsis(diffStats{}) break } - sk := formatMapKey(k, false) - sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), false, m) + sk := formatMapKey(k, false, ptrs) + sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), t.Kind(), ptrs) list = append(list, textRecord{Key: sk, Value: sv}) } - if opts.PrintAddresses { - ptr = formatPointer(v) - } - return textWrap{ptr + "{", list, "}"} + + out = &textWrap{Prefix: "{", Value: list, Suffix: "}"} + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + return out case reflect.Ptr: if v.IsNil() { return textNil } - if m.Visit(v) { - return textLine(formatPointer(v)) - } - if opts.PrintAddresses || opts.PrintShallowPointer { - ptr = formatPointer(v) - opts.PrintShallowPointer = false + + // Check pointer for cycles. + ptrRef, visited := ptrs.Push(v) + if visited { + out = makeLeafReference(ptrRef, opts.PrintAddresses) + return &textWrap{Prefix: "&", Value: out} } + defer ptrs.Pop() + skipType = true // Let the underlying value print the type instead - return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), false, m), ""} + out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) + out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) + out = &textWrap{Prefix: "&", Value: out} + return out case reflect.Interface: if v.IsNil() { return textNil @@ -266,7 +289,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit // 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) + return opts.WithTypeMode(emitType).FormatValue(v.Elem(), t.Kind(), ptrs) default: panic(fmt.Sprintf("%v kind not handled", v.Kind())) } @@ -274,14 +297,14 @@ func (opts formatOptions) FormatValue(v reflect.Value, withinSlice bool, m visit // formatMapKey formats v as if it were a map key. // The result is guaranteed to be a single line. -func formatMapKey(v reflect.Value, disambiguate bool) string { +func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { var opts formatOptions opts.DiffMode = diffIdentical opts.TypeMode = elideType - opts.PrintShallowPointer = true + opts.PrintAddresses = disambiguate opts.AvoidStringer = disambiguate opts.QualifiedNames = disambiguate - s := opts.FormatValue(v, false, visitedPointers{}).String() + s := opts.FormatValue(v, reflect.Map, ptrs).String() return strings.TrimSpace(s) } @@ -328,26 +351,3 @@ func formatHex(u uint64) string { } return fmt.Sprintf(f, u) } - -// formatPointer prints the address of the pointer. -func formatPointer(v reflect.Value) string { - return fmt.Sprintf("⟪0x%x⟫", pointerValue(v)) -} -func pointerValue(v reflect.Value) uintptr { - p := v.Pointer() - if flags.Deterministic { - p = 0xdeadf00f // Only used for stable testing purposes - } - return 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 index 49fc5ec..35315da 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -112,7 +112,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } } isText = !isBinary - isLinedText = isText && numLines >= 4 && maxLineLen <= 256 + isLinedText = isText && numLines >= 4 && maxLineLen <= 1024 } // Format the string into printable records. @@ -194,7 +194,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } list2 = append(list2, textRecord{Value: textLine(`"""`), ElideComma: true}) if isTripleQuoted { - var out textNode = textWrap{"(", list2, ")"} + var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} switch t.Kind() { case reflect.String: if t != reflect.TypeOf(string("")) { @@ -281,7 +281,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } // Wrap the output with appropriate type information. - var out textNode = textWrap{"{", list, "}"} + var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} if !isText { // The "{...}" byte-sequence literal is not valid Go syntax for strings. // Emit the type for extra clarity (e.g. "string{...}"). @@ -292,12 +292,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } switch t.Kind() { case reflect.String: - out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)} + out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: 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)} + out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} if t != reflect.TypeOf([]byte(nil)) { out = opts.FormatType(t, out) } diff --git a/cmp/report_text.go b/cmp/report_text.go index b8ec9d2..8b12c05 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -94,21 +94,22 @@ type textNode interface { // 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., "}" + Prefix string // e.g., "bytes.Buffer{" + Value textNode // textWrap | textList | textLine + Suffix string // e.g., "}" + Metadata interface{} // arbitrary metadata; has no effect on formatting } -func (s textWrap) Len() int { +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 { +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 { +func (s *textWrap) String() string { var d diffMode var n indentMode _, s2 := s.formatCompactTo(nil, d) @@ -117,7 +118,7 @@ func (s textWrap) String() string { b = append(b, '\n') // Trailing newline return string(b) } -func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { +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) @@ -127,7 +128,7 @@ func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { } return b, s } -func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte { +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...) @@ -195,7 +196,7 @@ func (s1 textList) Equal(s2 textNode) bool { } func (s textList) String() string { - return textWrap{"{", s, "}"}.String() + return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String() } func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index baec97d..949e8d1 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1222,124 +1222,124 @@ } >>> TestDiff/EqualMethod/StructNo/Inequal <<< TestDiff/Cycle/PointersInequal - &&cmp_test.P( -- &⟪0xdeadf00f⟫, -+ &&⟪0xdeadf00f⟫, + &&⟪ref#0⟫cmp_test.P( +- &⟪ref#0⟫(...), ++ &&⟪ref#0⟫(...), ) >>> TestDiff/Cycle/PointersInequal <<< TestDiff/Cycle/SlicesInequal cmp_test.S{ -- {{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}, -+ {{{{*(*cmp_test.S)(⟪0xdeadf00f⟫)}}}}, +- ⟪ref#0⟫{⟪ref#0⟫(...)}, ++ ⟪ref#1⟫{{⟪ref#1⟫(...)}}, } >>> TestDiff/Cycle/SlicesInequal <<< TestDiff/Cycle/MapsInequal - cmp_test.M{ -- 0: {0: ⟪0xdeadf00f⟫}, -+ 0: {0: {0: ⟪0xdeadf00f⟫}}, + cmp_test.M⟪ref#0⟫{ +- 0: ⟪ref#0⟫(...), ++ 0: {0: ⟪ref#0⟫(...)}, } >>> TestDiff/Cycle/MapsInequal <<< TestDiff/Cycle/GraphInequalZeroed map[string]*cmp_test.CycleAlpha{ - "Bar": &{ + "Bar": &⟪ref#0⟫{ Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ - ID: 102, + ID: 0, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{Name: "Bar", Bravos: {...}}, - "Buzz": &{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, - "BuzzBarBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ - ID: 103, + ID: 0, Name: "BuzzBarBravo", Mods: 0, - Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, }, }, }, }, }, - "BuzzBarBravo": &{ + "BuzzBarBravo": &⟪ref#3⟫{ - ID: 103, + ID: 0, Name: "BuzzBarBravo", Mods: 0, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{Name: "Bar", Bravos: {...}}, - "Buzz": &{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ - ID: 102, + ID: 0, Name: "BarBuzzBravo", Mods: 2, - Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, }, - "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: {...}}, + "BuzzBarBravo": &⟪ref#3⟫(...), }, }, }, }, }, }, - "Buzz": &{ + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ - ID: 102, + ID: 0, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{ + "Bar": &⟪ref#0⟫{ Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, - "BuzzBarBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ - ID: 103, + ID: 0, Name: "BuzzBarBravo", Mods: 0, - Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, }, }, }, - "Buzz": &{Name: "Buzz", Bravos: {...}}, + "Buzz": &⟪ref#2⟫(...), }, }, - "BuzzBarBravo": &{ + "BuzzBarBravo": &⟪ref#3⟫{ - ID: 103, + ID: 0, Name: "BuzzBarBravo", Mods: 0, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{ + "Bar": &⟪ref#0⟫{ Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ - ID: 102, + ID: 0, Name: "BarBuzzBravo", Mods: 2, - Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}, + Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, }, - "BuzzBarBravo": &{Name: "BuzzBarBravo", Alphas: {...}}, + "BuzzBarBravo": &⟪ref#3⟫(...), }, }, - "Buzz": &{Name: "Buzz", Bravos: {...}}, + "Buzz": &⟪ref#2⟫(...), }, }, }, }, - "Foo": &{ + "Foo": &⟪ref#4⟫{ Name: "Foo", Bravos: map[string]*cmp_test.CycleBravo{ "FooBravo": &{ @@ -1347,7 +1347,7 @@ + ID: 0, Name: "FooBravo", Mods: 100, - Alphas: {"Foo": &{Name: "Foo", Bravos: {...}}}, + Alphas: {"Foo": &⟪ref#4⟫(...)}, }, }, }, @@ -1355,87 +1355,68 @@ >>> TestDiff/Cycle/GraphInequalZeroed <<< TestDiff/Cycle/GraphInequalStruct map[string]*cmp_test.CycleAlpha{ - "Bar": &{ + "Bar": &⟪ref#0⟫{ Name: "Bar", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{Name: "Bar", Bravos: {...}}, - "Buzz": &{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, - "BuzzBarBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫(...), + "BuzzBarBravo": &⟪ref#3⟫{ ID: 103, Name: "BuzzBarBravo", Mods: 0, - Alphas: nil, -+ Alphas: map[string]*cmp_test.CycleAlpha{ -+ "Bar": &{ -+ Name: "Bar", -+ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{...}, "BuzzBarBravo": &{...}}, -+ }, -+ "Buzz": &{ -+ Name: "Buzz", -+ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}, -+ }, -+ }, ++ Alphas: map[string]*cmp_test.CycleAlpha{"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}, }, }, }, }, }, - "BuzzBarBravo": &{ + "BuzzBarBravo": &⟪ref#3⟫{ ID: 103, Name: "BuzzBarBravo", Mods: 0, Alphas: map[string]*cmp_test.CycleAlpha{ - "Bar": &{Name: "Bar", Bravos: {...}}, - "Buzz": &{ + "Bar": &⟪ref#0⟫(...), + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}, + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}, - "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{...}}, -+ "Buzz": &{Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{...}}, -+ }, -+ }, ++ "BuzzBarBravo": &⟪ref#3⟫(...), }, }, }, }, }, }, - "Buzz": &{ + "Buzz": &⟪ref#2⟫{ Name: "Buzz", Bravos: map[string]*cmp_test.CycleBravo{ - "BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &{Name: "Bar", Bravos: {"BarBuzzBravo": &{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {...}}, "BuzzBarBravo": &{ID: 103, Name: "BuzzBarBravo", Alphas: {"Bar": &{Name: "Bar", Bravos: {...}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}}}, "Buzz": &{Name: "Buzz", Bravos: {...}}}}, - "BuzzBarBravo": &{ + "BarBuzzBravo": &⟪ref#1⟫{ID: 102, Name: "BarBuzzBravo", Mods: 2, Alphas: {"Bar": &⟪ref#0⟫{Name: "Bar", Bravos: {"BarBuzzBravo": &⟪ref#1⟫(...), "BuzzBarBravo": &⟪ref#3⟫{ID: 103, Name: "BuzzBarBravo", Alphas: {"Bar": &⟪ref#0⟫(...), "Buzz": &⟪ref#2⟫(...)}}}}, "Buzz": &⟪ref#2⟫(...)}}, + "BuzzBarBravo": &⟪ref#3⟫{ ID: 103, Name: "BuzzBarBravo", Mods: 0, - Alphas: nil, + Alphas: map[string]*cmp_test.CycleAlpha{ -+ "Bar": &{ ++ "Bar": &⟪ref#0⟫{ + Name: "Bar", -+ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &{...}, "BuzzBarBravo": &{...}}, -+ }, -+ "Buzz": &{ -+ Name: "Buzz", -+ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": ⟪0xdeadf00f⟫, "BuzzBarBravo": ⟪0xdeadf00f⟫}, ++ Bravos: map[string]*cmp_test.CycleBravo{"BarBuzzBravo": &⟪ref#1⟫{...}, "BuzzBarBravo": &⟪ref#3⟫(...)}, + }, ++ "Buzz": &⟪ref#2⟫(...), + }, }, }, }, - "Foo": &{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &{Name: "Foo", Bravos: {...}}}}}}, + "Foo": &⟪ref#4⟫{Name: "Foo", Bravos: {"FooBravo": &{ID: 101, Name: "FooBravo", Mods: 100, Alphas: {"Foo": &⟪ref#4⟫(...)}}}}, } >>> TestDiff/Cycle/GraphInequalStruct <<< TestDiff/Project1/ProtoInequal From d669b046d12237b504e86b93d6b25ec551e8c349 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 22 Jun 2020 09:53:17 -0700 Subject: [PATCH 55/99] Swallow panic when calling String or Error (#221) If a panic occurs while calling String or Error, the reporter recovers from it and ignores it, proceeding with its usual functionality for formatting a value. --- cmp/compare_test.go | 13 +++++++++++++ cmp/report_reflect.go | 18 ++++++++++++------ cmp/testdata/diffs | 12 ++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index c94f6c0..b34530b 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -8,6 +8,7 @@ import ( "bytes" "crypto/md5" "encoding/json" + "errors" "flag" "fmt" "io" @@ -874,6 +875,18 @@ func reporterTests() []test { ) return []test{{ + label: label + "/PanicStringer", + x: struct{ X fmt.Stringer }{struct{ fmt.Stringer }{nil}}, + y: struct{ X fmt.Stringer }{bytes.NewBuffer(nil)}, + wantEqual: false, + reason: "panic from fmt.Stringer should not crash the reporter", + }, { + label: label + "/PanicError", + x: struct{ X error }{struct{ error }{nil}}, + y: struct{ X error }{errors.New("")}, + wantEqual: false, + reason: "panic from error should not crash the reporter", + }, { label: label + "/AmbiguousType", x: foo1.Bar{}, y: foo2.Bar{}, diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 2d722ea..28e0e92 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -125,12 +125,18 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, // implementations crash when doing so. if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() { var prefix, strVal string - switch v := v.Interface().(type) { - case error: - prefix, strVal = "e", v.Error() - case fmt.Stringer: - prefix, strVal = "s", v.String() - } + func() { + // Swallow and ignore any panics from String or Error. + defer func() { recover() }() + switch v := v.Interface().(type) { + case error: + strVal = v.Error() + prefix = "e" + case fmt.Stringer: + strVal = v.String() + prefix = "s" + } + }() if prefix != "" { maxLen := len(strVal) if opts.LimitVerbosity { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 949e8d1..05fa3fd 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -252,6 +252,18 @@ })), } >>> TestDiff/Transformer/AcyclicString +<<< TestDiff/Reporter/PanicStringer + struct{ X fmt.Stringer }{ +- X: struct{ fmt.Stringer }{}, ++ X: s"", + } +>>> TestDiff/Reporter/PanicStringer +<<< TestDiff/Reporter/PanicError + struct{ X error }{ +- X: struct{ error }{}, ++ X: e"", + } +>>> TestDiff/Reporter/PanicError <<< TestDiff/Reporter/AmbiguousType interface{}( - "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, From 1536a0c407e000815ccef10665d81cef0c855cd1 Mon Sep 17 00:00:00 2001 From: "k.nakada" <36500782+ko30005@users.noreply.github.com> Date: Wed, 15 Jul 2020 02:35:29 +0900 Subject: [PATCH 56/99] Adjust panic for IgnoreUnexported and IgnoreFields (#228) Adjust the panic message to be more specific about what the user should do, and reduces the need for the user to look at the source code. --- cmp/cmpopts/ignore.go | 2 +- cmp/cmpopts/struct_filter.go | 2 +- cmp/cmpopts/util_test.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index afd36be..48787dd 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -128,7 +128,7 @@ func newUnexportedFilter(typs ...interface{}) unexportedFilter { for _, typ := range typs { t := reflect.TypeOf(typ) if t == nil || t.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid struct type: %T", typ)) + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) } ux.m[t] = true } diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index dae7ced..fe8d1b9 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -42,7 +42,7 @@ func newStructFilter(typ interface{}, names ...string) structFilter { t := reflect.TypeOf(typ) if t == nil || t.Kind() != reflect.Struct { - panic(fmt.Sprintf("%T must be a struct", typ)) + panic(fmt.Sprintf("%T must be a non-pointer struct", typ)) } var ft fieldTree for _, name := range names { diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 37704c8..9e96632 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -1223,7 +1223,7 @@ func TestPanic(t *testing.T) { label: "IgnoreFields", fnc: IgnoreFields, args: args(&Foo1{}, "Alpha"), - wantPanic: "must be a struct", + wantPanic: "must be a non-pointer struct", reason: "the type must be a struct (not pointer to a struct)", }, { label: "IgnoreFields", @@ -1304,13 +1304,13 @@ func TestPanic(t *testing.T) { label: "IgnoreUnexported", fnc: IgnoreUnexported, args: args(nil), - wantPanic: "invalid struct type", + wantPanic: "must be a non-pointer struct", reason: "input must not be nil value", }, { label: "IgnoreUnexported", fnc: IgnoreUnexported, args: args(&Foo1{}), - wantPanic: "invalid struct type", + wantPanic: "must be a non-pointer struct", reason: "input must be a struct type (not a pointer to a struct)", }, { label: "IgnoreUnexported", From 9680bfaf28748393e28e00238d94070fb9972fd8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 21 Jul 2020 12:17:14 -0700 Subject: [PATCH 57/99] Use triple-quote formatting for multiline strings (#229) For strings, []bytes containing text data, Error method output, and String method output, use the triple-quoted syntax. This improves readability by presenting the data more naturally compared to a single-line quoted string with many escaped characters. --- cmp/compare_test.go | 13 ++++++++ cmp/report_reflect.go | 73 +++++++++++++++++++++++++++++++++---------- cmp/testdata/diffs | 33 +++++++++++++++++++ 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index b34530b..ba39bde 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1133,6 +1133,19 @@ func reporterTests() []test { y: "aaa\nbbb\nccc \nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu\nvvv\nwww\nxxx\nyyy\nzzz\n", wantEqual: false, reason: "avoid triple-quote syntax due to visual equivalence of differences", + }, { + label: label + "/TripleQuoteStringer", + x: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")), + }, + y: []fmt.Stringer{ + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")), + bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")), + }, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantEqual: false, + reason: "multi-line String output should be formatted with triple quote", }, { label: label + "/LimitMaximumBytesDiffs", x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="), diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 28e0e92..786f671 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -5,6 +5,7 @@ package cmp import ( + "bytes" "fmt" "reflect" "strconv" @@ -138,14 +139,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } }() if prefix != "" { - maxLen := len(strVal) - if opts.LimitVerbosity { - maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... - } - if len(strVal) > maxLen+len(textEllipsis) { - return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis)) - } - return textLine(prefix + formatString(strVal)) + return opts.formatString(prefix, strVal) } } } @@ -177,14 +171,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, case reflect.Complex64, reflect.Complex128: return textLine(fmt.Sprint(v.Complex())) case reflect.String: - maxLen := v.Len() - if opts.LimitVerbosity { - maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... - } - if v.Len() > maxLen+len(textEllipsis) { - return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis)) - } - return textLine(formatString(v.String())) + return opts.formatString("", v.String()) case reflect.UnsafePointer, reflect.Chan, reflect.Func: return textLine(formatPointer(value.PointerOf(v), true)) case reflect.Struct: @@ -216,6 +203,17 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, if v.IsNil() { return textNil } + + // Check whether this is a []byte of text data. + if t.Elem() == reflect.TypeOf(byte(0)) { + b := v.Bytes() + isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) } + if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { + out = opts.formatString("", string(b)) + return opts.WithTypeMode(emitType).FormatType(t, out) + } + } + fallthrough case reflect.Array: maxLen := v.Len() @@ -301,6 +299,49 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } } +func (opts formatOptions) formatString(prefix, s string) textNode { + maxLen := len(s) + maxLines := strings.Count(s, "\n") + 1 + if opts.LimitVerbosity { + maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc... + maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc... + } + + // For multiline strings, use the triple-quote syntax, + // but only use it when printing removed or inserted nodes since + // we only want the extra verbosity for those cases. + lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n") + isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+') + for i := 0; i < len(lines) && isTripleQuoted; i++ { + lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support + isPrintable := func(r rune) bool { + return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable + } + line := lines[i] + isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen + } + if isTripleQuoted { + var list textList + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + for i, line := range lines { + if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 { + comment := commentString(fmt.Sprintf("%d elided lines", numElided)) + list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment}) + break + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true}) + } + list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true}) + return &textWrap{Prefix: "(", Value: list, Suffix: ")"} + } + + // Format the string as a single-line quoted string. + if len(s) > maxLen+len(textEllipsis) { + return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis)) + } + return textLine(prefix + formatString(s)) +} + // formatMapKey formats v as if it were a map key. // The result is guaranteed to be a single line. func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 05fa3fd..dee035d 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -730,6 +730,39 @@ ... // 7 identical lines }, "\n") >>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace +<<< TestDiff/Reporter/TripleQuoteStringer + []fmt.Stringer{ + s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"..., +- ( +- s""" +- package main +- +- import ( +- "fmt" +- "math/rand" +- ) +- +- func main() { +- fmt.Println("My favorite number is", rand.Intn(10)) +- } +- s""" +- ), ++ ( ++ s""" ++ package main ++ ++ import ( ++ "fmt" ++ "math" ++ ) ++ ++ func main() { ++ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7)) ++ } ++ s""" ++ ), + } +>>> TestDiff/Reporter/TripleQuoteStringer <<< TestDiff/Reporter/LimitMaximumBytesDiffs []uint8{ - 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S| From 036ffc7f24c0d0eda6ce580168fdaec194a1561f Mon Sep 17 00:00:00 2001 From: Ernest Galbrun Date: Wed, 29 Jul 2020 17:27:27 +0200 Subject: [PATCH 58/99] Fix Diff documentation (#231) Fix the documentation on Diff. It was mentioning the plus or minus sign being printed if the field was added to y or removed from y, but both are the same. Fix it so that it properly mentions the use of a minus sign for elements removed from x. --- cmp/compare.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 580ae20..6656186 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -95,12 +95,12 @@ func Equal(x, y interface{}, opts ...Option) bool { 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. +// Diff returns a human-readable report of the differences between two values: +// y - x. It returns an empty string if and only if Equal returns true for the +// same input values and options. // // 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, +// At the start of each line, a "-" prefix indicates an element removed from y, // 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 From db9de4321f4e5db7c6f996d170ebce7953f22f8e Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 12 Aug 2020 12:30:49 -0700 Subject: [PATCH 59/99] Add testing for Go1.15 (#232) --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index efb4782..13d3717 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,9 @@ matrix: script: - go test -v -race ./... - go: 1.14.x + script: + - go test -v -race ./... + - go: 1.15.x script: - diff -u <(echo -n) <(gofmt -d .) - go test -v -race ./... From d2fcc899bdc2d134b7c00e36137260db963e193c Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 18 Aug 2020 12:37:11 -0700 Subject: [PATCH 60/99] Suggest use of cmpopts.EquateErrors (#234) If cmp panics because it is trying to access an unexported field, specially suggest the use of cmpopts.EquateErrors if the parent type implements the error interface. Fixes #233 --- cmp/compare_test.go | 13 +++++++++++++ cmp/options.go | 5 ++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index ba39bde..bdcc06b 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -696,6 +696,19 @@ func comparerTests() []test { }, wantEqual: true, reason: "verify that exporter does not leak implementation details", + }, { + label: label + "/ErrorPanic", + x: io.EOF, + y: io.EOF, + wantPanic: "consider using cmpopts.EquateErrors", + reason: "suggest cmpopts.EquateErrors when accessing unexported fields of error types", + }, { + label: label + "/ErrorEqual", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{cmpopts.EquateErrors()}, + wantEqual: true, + reason: "cmpopts.EquateErrors should equate these two errors as sentinel values", }} } diff --git a/cmp/options.go b/cmp/options.go index abbd2a6..4b0407a 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -225,11 +225,14 @@ func (validator) apply(s *state, vx, vy reflect.Value) { // 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" + help := "consider using a custom Comparer; if you control the implementation of type, you can also consider using an Exporter, AllowUnexported, or cmpopts.IgnoreUnexported" var name string if t := s.curPath.Index(-2).Type(); t.Name() != "" { // Named type with unexported fields. name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType + if _, ok := reflect.New(t).Interface().(error); ok { + help = "consider using cmpopts.EquateErrors to compare error values" + } } else { // Unnamed type with unexported fields. Derive PkgPath from field. var pkgPath string From d713870ac17fdb9ee5e2ee48ff6562dfb1c0157b Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 23 Sep 2020 13:30:54 -0700 Subject: [PATCH 61/99] Fix license headers (#236) There is no LICENSE.md file, but there is a LICENSE file. --- cmp/cmpopts/equate.go | 2 +- cmp/cmpopts/ignore.go | 2 +- cmp/cmpopts/sort.go | 2 +- cmp/cmpopts/struct_filter.go | 2 +- cmp/cmpopts/util_test.go | 2 +- cmp/cmpopts/xform.go | 2 +- cmp/compare.go | 2 +- cmp/compare_test.go | 2 +- cmp/example_reporter_test.go | 2 +- cmp/example_test.go | 2 +- cmp/export_panic.go | 2 +- cmp/export_unsafe.go | 2 +- cmp/internal/diff/debug_disable.go | 2 +- cmp/internal/diff/debug_enable.go | 2 +- cmp/internal/diff/diff.go | 2 +- cmp/internal/diff/diff_test.go | 2 +- cmp/internal/flags/flags.go | 2 +- cmp/internal/flags/toolchain_legacy.go | 2 +- cmp/internal/flags/toolchain_recent.go | 2 +- cmp/internal/function/func.go | 2 +- cmp/internal/function/func_test.go | 2 +- cmp/internal/testprotos/protos.go | 2 +- cmp/internal/teststructs/foo1/foo.go | 2 +- cmp/internal/teststructs/foo2/foo.go | 2 +- cmp/internal/teststructs/project1.go | 2 +- cmp/internal/teststructs/project2.go | 2 +- cmp/internal/teststructs/project3.go | 2 +- cmp/internal/teststructs/project4.go | 2 +- cmp/internal/teststructs/structs.go | 2 +- cmp/internal/value/name.go | 2 +- cmp/internal/value/name_test.go | 2 +- cmp/internal/value/pointer_purego.go | 2 +- cmp/internal/value/pointer_unsafe.go | 2 +- cmp/internal/value/sort.go | 2 +- cmp/internal/value/sort_test.go | 2 +- cmp/internal/value/zero.go | 2 +- cmp/internal/value/zero_test.go | 2 +- cmp/options.go | 2 +- cmp/options_test.go | 2 +- cmp/path.go | 2 +- cmp/report.go | 2 +- cmp/report_compare.go | 2 +- cmp/report_references.go | 2 +- cmp/report_reflect.go | 2 +- cmp/report_slices.go | 2 +- cmp/report_text.go | 2 +- cmp/report_value.go | 2 +- 47 files changed, 47 insertions(+), 47 deletions(-) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 8667908..51ce36f 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package cmpopts provides common options for the cmp package. package cmpopts diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index 48787dd..80c6061 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmpopts diff --git a/cmp/cmpopts/sort.go b/cmp/cmpopts/sort.go index 3a48046..a646d74 100644 --- a/cmp/cmpopts/sort.go +++ b/cmp/cmpopts/sort.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmpopts diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index fe8d1b9..a09829c 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmpopts diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index 9e96632..b19bcab 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmpopts diff --git a/cmp/cmpopts/xform.go b/cmp/cmpopts/xform.go index 9d65155..4eb49d6 100644 --- a/cmp/cmpopts/xform.go +++ b/cmp/cmpopts/xform.go @@ -1,6 +1,6 @@ // Copyright 2018, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmpopts diff --git a/cmp/compare.go b/cmp/compare.go index 6656186..83fc5a4 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package cmp determines equality of values. // diff --git a/cmp/compare_test.go b/cmp/compare_test.go index bdcc06b..13a3a4e 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp_test diff --git a/cmp/example_reporter_test.go b/cmp/example_reporter_test.go index bc1932e..bacba28 100644 --- a/cmp/example_reporter_test.go +++ b/cmp/example_reporter_test.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp_test diff --git a/cmp/example_test.go b/cmp/example_test.go index d4f7391..d165383 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp_test diff --git a/cmp/export_panic.go b/cmp/export_panic.go index dfa5d21..5ff0b42 100644 --- a/cmp/export_panic.go +++ b/cmp/export_panic.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build purego diff --git a/cmp/export_unsafe.go b/cmp/export_unsafe.go index 351f1a3..21eb548 100644 --- a/cmp/export_unsafe.go +++ b/cmp/export_unsafe.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build !purego diff --git a/cmp/internal/diff/debug_disable.go b/cmp/internal/diff/debug_disable.go index fe98dcc..1daaaac 100644 --- a/cmp/internal/diff/debug_disable.go +++ b/cmp/internal/diff/debug_disable.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build !cmp_debug diff --git a/cmp/internal/diff/debug_enable.go b/cmp/internal/diff/debug_enable.go index 597b6ae..4b91dbc 100644 --- a/cmp/internal/diff/debug_enable.go +++ b/cmp/internal/diff/debug_enable.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build cmp_debug diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 730e223..441953e 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package diff implements an algorithm for producing edit-scripts. // The edit-script is a sequence of operations needed to transform one list diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go index 35e8642..d97fef8 100644 --- a/cmp/internal/diff/diff_test.go +++ b/cmp/internal/diff/diff_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package diff diff --git a/cmp/internal/flags/flags.go b/cmp/internal/flags/flags.go index a9e7fc0..d8e459c 100644 --- a/cmp/internal/flags/flags.go +++ b/cmp/internal/flags/flags.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package flags diff --git a/cmp/internal/flags/toolchain_legacy.go b/cmp/internal/flags/toolchain_legacy.go index 01aed0a..82d1d7f 100644 --- a/cmp/internal/flags/toolchain_legacy.go +++ b/cmp/internal/flags/toolchain_legacy.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build !go1.10 diff --git a/cmp/internal/flags/toolchain_recent.go b/cmp/internal/flags/toolchain_recent.go index c0b667f..8646f05 100644 --- a/cmp/internal/flags/toolchain_recent.go +++ b/cmp/internal/flags/toolchain_recent.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build go1.10 diff --git a/cmp/internal/function/func.go b/cmp/internal/function/func.go index ace1dbe..d127d43 100644 --- a/cmp/internal/function/func.go +++ b/cmp/internal/function/func.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package function provides functionality for identifying function types. package function diff --git a/cmp/internal/function/func_test.go b/cmp/internal/function/func_test.go index 61eeccd..f03ef45 100644 --- a/cmp/internal/function/func_test.go +++ b/cmp/internal/function/func_test.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package function diff --git a/cmp/internal/testprotos/protos.go b/cmp/internal/testprotos/protos.go index 120c8b0..81622d3 100644 --- a/cmp/internal/testprotos/protos.go +++ b/cmp/internal/testprotos/protos.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package testprotos diff --git a/cmp/internal/teststructs/foo1/foo.go b/cmp/internal/teststructs/foo1/foo.go index c769dfb..c0882fb 100644 --- a/cmp/internal/teststructs/foo1/foo.go +++ b/cmp/internal/teststructs/foo1/foo.go @@ -1,6 +1,6 @@ // Copyright 2020, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package foo is deliberately named differently than the parent directory. // It contain declarations that have ambiguity in their short names, diff --git a/cmp/internal/teststructs/foo2/foo.go b/cmp/internal/teststructs/foo2/foo.go index c769dfb..c0882fb 100644 --- a/cmp/internal/teststructs/foo2/foo.go +++ b/cmp/internal/teststructs/foo2/foo.go @@ -1,6 +1,6 @@ // Copyright 2020, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // Package foo is deliberately named differently than the parent directory. // It contain declarations that have ambiguity in their short names, diff --git a/cmp/internal/teststructs/project1.go b/cmp/internal/teststructs/project1.go index 1999e38..223d6ab 100644 --- a/cmp/internal/teststructs/project1.go +++ b/cmp/internal/teststructs/project1.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project2.go b/cmp/internal/teststructs/project2.go index 536592b..1616dd8 100644 --- a/cmp/internal/teststructs/project2.go +++ b/cmp/internal/teststructs/project2.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project3.go b/cmp/internal/teststructs/project3.go index 957d093..9e56dfa 100644 --- a/cmp/internal/teststructs/project3.go +++ b/cmp/internal/teststructs/project3.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/project4.go b/cmp/internal/teststructs/project4.go index 49920f2..a09aba2 100644 --- a/cmp/internal/teststructs/project4.go +++ b/cmp/internal/teststructs/project4.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/teststructs/structs.go b/cmp/internal/teststructs/structs.go index 6b4d2a7..bfd2de8 100644 --- a/cmp/internal/teststructs/structs.go +++ b/cmp/internal/teststructs/structs.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package teststructs diff --git a/cmp/internal/value/name.go b/cmp/internal/value/name.go index 8228e7d..b6c12ce 100644 --- a/cmp/internal/value/name.go +++ b/cmp/internal/value/name.go @@ -1,6 +1,6 @@ // Copyright 2020, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value diff --git a/cmp/internal/value/name_test.go b/cmp/internal/value/name_test.go index ddb31d4..3eec91c 100644 --- a/cmp/internal/value/name_test.go +++ b/cmp/internal/value/name_test.go @@ -1,6 +1,6 @@ // Copyright 2020, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value diff --git a/cmp/internal/value/pointer_purego.go b/cmp/internal/value/pointer_purego.go index e9e384a..44f4a5a 100644 --- a/cmp/internal/value/pointer_purego.go +++ b/cmp/internal/value/pointer_purego.go @@ -1,6 +1,6 @@ // Copyright 2018, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build purego diff --git a/cmp/internal/value/pointer_unsafe.go b/cmp/internal/value/pointer_unsafe.go index b50c17e..a605953 100644 --- a/cmp/internal/value/pointer_unsafe.go +++ b/cmp/internal/value/pointer_unsafe.go @@ -1,6 +1,6 @@ // Copyright 2018, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. // +build !purego diff --git a/cmp/internal/value/sort.go b/cmp/internal/value/sort.go index 24fbae6..98533b0 100644 --- a/cmp/internal/value/sort.go +++ b/cmp/internal/value/sort.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value diff --git a/cmp/internal/value/sort_test.go b/cmp/internal/value/sort_test.go index fb86fce..26222d6 100644 --- a/cmp/internal/value/sort_test.go +++ b/cmp/internal/value/sort_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value_test diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go index 06a8ffd..9147a29 100644 --- a/cmp/internal/value/zero.go +++ b/cmp/internal/value/zero.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value diff --git a/cmp/internal/value/zero_test.go b/cmp/internal/value/zero_test.go index 1d6c434..ddaa337 100644 --- a/cmp/internal/value/zero_test.go +++ b/cmp/internal/value/zero_test.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package value diff --git a/cmp/options.go b/cmp/options.go index 4b0407a..e57b9eb 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/options_test.go b/cmp/options_test.go index f8066c7..c7d45f3 100644 --- a/cmp/options_test.go +++ b/cmp/options_test.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/path.go b/cmp/path.go index 603dbb0..3d45c1a 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report.go b/cmp/report.go index aafcb36..f43cd12 100644 --- a/cmp/report.go +++ b/cmp/report.go @@ -1,6 +1,6 @@ // Copyright 2017, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 9e21809..a6c070c 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_references.go b/cmp/report_references.go index d620c2c..be31b33 100644 --- a/cmp/report_references.go +++ b/cmp/report_references.go @@ -1,6 +1,6 @@ // Copyright 2020, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 786f671..5b47420 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 35315da..da04caf 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_text.go b/cmp/report_text.go index 8b12c05..0fd46d7 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp diff --git a/cmp/report_value.go b/cmp/report_value.go index 83031a7..668d470 100644 --- a/cmp/report_value.go +++ b/cmp/report_value.go @@ -1,6 +1,6 @@ // Copyright 2019, The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. +// license that can be found in the LICENSE file. package cmp From 566225a2554cf156c7af1006dc3a0940e1e02b09 Mon Sep 17 00:00:00 2001 From: colinnewell Date: Sun, 4 Oct 2020 09:16:17 +0100 Subject: [PATCH 62/99] Add an example for IgnoreFields (#205) Add an example for IgnoreFields. This resuses the test data from the example in the cmp package to provide consistency between examples. --- cmp/cmpopts/example_test.go | 130 ++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 cmp/cmpopts/example_test.go diff --git a/cmp/cmpopts/example_test.go b/cmp/cmpopts/example_test.go new file mode 100644 index 0000000..0cf2513 --- /dev/null +++ b/cmp/cmpopts/example_test.go @@ -0,0 +1,130 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cmpopts_test + +import ( + "fmt" + "net" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-cmp/cmp/internal/flags" +) + +func init() { + flags.Deterministic = true +} + +// Use IgnoreFields to ignore fields on a struct type when comparing +// by providing a value of the type and the field names to ignore. +// Typically, a zero value of the type is used (e.g., foo.MyStruct{}). +func ExampleIgnoreFields_testing() { + // Let got be the hypothetical value obtained from some logic under test + // and want be the expected golden data. + got, want := MakeGatewayInfo() + + // While the specified fields will be semantically ignored for the comparison, + // the fields may be printed in the diff when displaying entire values + // that are already determined to be different. + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(Client{}, "IPAddress")); diff != "" { + t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff) + } + + // Output: + // MakeGatewayInfo() mismatch (-want +got): + // cmpopts_test.Gateway{ + // SSID: "CoffeeShopWiFi", + // - IPAddress: s"192.168.0.2", + // + IPAddress: s"192.168.0.1", + // NetMask: {0xff, 0xff, 0x00, 0x00}, + // Clients: []cmpopts_test.Client{ + // ... // 3 identical elements + // {Hostname: "espresso", ...}, + // {Hostname: "latte", LastSeen: s"2009-11-10 23:00:23 +0000 UTC", ...}, + // + { + // + Hostname: "americano", + // + IPAddress: s"192.168.0.188", + // + LastSeen: s"2009-11-10 23:03:05 +0000 UTC", + // + }, + // }, + // } +} + +type ( + Gateway struct { + SSID string + IPAddress net.IP + NetMask net.IPMask + Clients []Client + } + Client struct { + Hostname string + IPAddress net.IP + LastSeen time.Time + } +) + +func MakeGatewayInfo() (x, y Gateway) { + x = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 1), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 219), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }, { + Hostname: "americano", + IPAddress: net.IPv4(192, 168, 0, 188), + LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC), + }}, + } + y = Gateway{ + SSID: "CoffeeShopWiFi", + IPAddress: net.IPv4(192, 168, 0, 2), + NetMask: net.IPv4Mask(255, 255, 0, 0), + Clients: []Client{{ + Hostname: "ristretto", + IPAddress: net.IPv4(192, 168, 0, 116), + }, { + Hostname: "aribica", + IPAddress: net.IPv4(192, 168, 0, 104), + LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC), + }, { + Hostname: "macchiato", + IPAddress: net.IPv4(192, 168, 0, 153), + LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC), + }, { + Hostname: "espresso", + IPAddress: net.IPv4(192, 168, 0, 121), + }, { + Hostname: "latte", + IPAddress: net.IPv4(192, 168, 0, 221), + LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC), + }}, + } + return x, y +} + +var t fakeT + +type fakeT struct{} + +func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } From ab46b8bd0abd4c4557cc4709ad7ae12d47570603 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 20 Oct 2020 14:23:13 -0700 Subject: [PATCH 63/99] Adjust for reflect.Type.NumMethod change in Go1.16 (#240) In Go1.16, the reflect.Type.NumMethod method will no longer report unexported fields, matching the documented behavior on the method. This means that t.NumMethod() == 0 is no longer a reliable means to detect whether an interface type is the empty interface or not. Fix the code to check whether the empty interface itself implements the target type. --- cmp/cmpopts/ignore.go | 3 ++- cmp/internal/value/iface.go | 14 +++++++++++++ cmp/internal/value/iface_test.go | 35 ++++++++++++++++++++++++++++++++ cmp/options.go | 7 ++++--- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 cmp/internal/value/iface.go create mode 100644 cmp/internal/value/iface_test.go diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index 80c6061..3cdc211 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" ) // IgnoreFields returns an Option that ignores fields of the @@ -82,7 +83,7 @@ func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { 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: + case value.IsEmptyInterface(fi.Type): // This matches everything; why would you ever want this? panic("cannot ignore empty interface") default: diff --git a/cmp/internal/value/iface.go b/cmp/internal/value/iface.go new file mode 100644 index 0000000..d8d07a9 --- /dev/null +++ b/cmp/internal/value/iface.go @@ -0,0 +1,14 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import "reflect" + +var emptyIfaceType = reflect.TypeOf((*interface{})(nil)).Elem() + +// IsEmptyInterface reports whether t is an interface type with no methods. +func IsEmptyInterface(t reflect.Type) bool { + return t.Kind() == reflect.Interface && emptyIfaceType.Implements(t) +} diff --git a/cmp/internal/value/iface_test.go b/cmp/internal/value/iface_test.go new file mode 100644 index 0000000..817e1d6 --- /dev/null +++ b/cmp/internal/value/iface_test.go @@ -0,0 +1,35 @@ +// Copyright 2020, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "reflect" + "testing" +) + +func TestIsEmptyInterface(t *testing.T) { + type ( + Empty interface{} + Exported interface{ X() } + Unexported interface{ x() } + ) + tests := []struct { + in reflect.Type + want bool + }{ + {reflect.TypeOf((*interface{})(nil)).Elem(), true}, + {reflect.TypeOf((*Empty)(nil)).Elem(), true}, + {reflect.TypeOf((*Exported)(nil)).Elem(), false}, + {reflect.TypeOf((*Unexported)(nil)).Elem(), false}, + {reflect.TypeOf(5), false}, + {reflect.TypeOf(struct{}{}), false}, + } + for _, tt := range tests { + got := IsEmptyInterface(tt.in) + if got != tt.want { + t.Errorf("IsEmptyInterface(%v) = %v, want %v", tt.in, got, tt.want) + } + } +} diff --git a/cmp/options.go b/cmp/options.go index e57b9eb..3b71f54 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" ) // Option configures for specific behavior of Equal and Diff. In particular, @@ -161,7 +162,7 @@ func FilterValues(f interface{}, opt Option) Option { } 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 { + if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { vf.typ = ti } return vf @@ -286,7 +287,7 @@ func Transformer(name string, f interface{}) Option { 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 { + if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { tr.typ = ti } return tr @@ -345,7 +346,7 @@ func Comparer(f interface{}) Option { 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 { + if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { cm.typ = ti } return cm From d3c8501c1f408298cf94082d6774e0a5b77c3ce0 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 12 Nov 2020 12:17:33 -0800 Subject: [PATCH 64/99] Revert "Adjust for reflect.Type.NumMethod change in Go1.16 (#240)" (#242) This reverts commit ab46b8bd0abd4c4557cc4709ad7ae12d47570603. The upstream change in Go1.16 has been rolled back. See golang/go#42123 --- cmp/cmpopts/ignore.go | 3 +-- cmp/internal/value/iface.go | 14 ------------- cmp/internal/value/iface_test.go | 35 -------------------------------- cmp/options.go | 7 +++---- 4 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 cmp/internal/value/iface.go delete mode 100644 cmp/internal/value/iface_test.go diff --git a/cmp/cmpopts/ignore.go b/cmp/cmpopts/ignore.go index 3cdc211..80c6061 100644 --- a/cmp/cmpopts/ignore.go +++ b/cmp/cmpopts/ignore.go @@ -12,7 +12,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/internal/function" - "github.com/google/go-cmp/cmp/internal/value" ) // IgnoreFields returns an Option that ignores fields of the @@ -83,7 +82,7 @@ func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { panic("struct cannot have named fields") case fi.Type.Kind() != reflect.Interface: panic("embedded field must be an interface type") - case value.IsEmptyInterface(fi.Type): + case fi.Type.NumMethod() == 0: // This matches everything; why would you ever want this? panic("cannot ignore empty interface") default: diff --git a/cmp/internal/value/iface.go b/cmp/internal/value/iface.go deleted file mode 100644 index d8d07a9..0000000 --- a/cmp/internal/value/iface.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package value - -import "reflect" - -var emptyIfaceType = reflect.TypeOf((*interface{})(nil)).Elem() - -// IsEmptyInterface reports whether t is an interface type with no methods. -func IsEmptyInterface(t reflect.Type) bool { - return t.Kind() == reflect.Interface && emptyIfaceType.Implements(t) -} diff --git a/cmp/internal/value/iface_test.go b/cmp/internal/value/iface_test.go deleted file mode 100644 index 817e1d6..0000000 --- a/cmp/internal/value/iface_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE.md file. - -package value - -import ( - "reflect" - "testing" -) - -func TestIsEmptyInterface(t *testing.T) { - type ( - Empty interface{} - Exported interface{ X() } - Unexported interface{ x() } - ) - tests := []struct { - in reflect.Type - want bool - }{ - {reflect.TypeOf((*interface{})(nil)).Elem(), true}, - {reflect.TypeOf((*Empty)(nil)).Elem(), true}, - {reflect.TypeOf((*Exported)(nil)).Elem(), false}, - {reflect.TypeOf((*Unexported)(nil)).Elem(), false}, - {reflect.TypeOf(5), false}, - {reflect.TypeOf(struct{}{}), false}, - } - for _, tt := range tests { - got := IsEmptyInterface(tt.in) - if got != tt.want { - t.Errorf("IsEmptyInterface(%v) = %v, want %v", tt.in, got, tt.want) - } - } -} diff --git a/cmp/options.go b/cmp/options.go index 3b71f54..e57b9eb 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/google/go-cmp/cmp/internal/function" - "github.com/google/go-cmp/cmp/internal/value" ) // Option configures for specific behavior of Equal and Diff. In particular, @@ -162,7 +161,7 @@ func FilterValues(f interface{}, opt Option) Option { } if opt := normalizeOption(opt); opt != nil { vf := &valuesFilter{fnc: v, opt: opt} - if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { vf.typ = ti } return vf @@ -287,7 +286,7 @@ func Transformer(name string, f interface{}) Option { panic(fmt.Sprintf("invalid name: %q", name)) } tr := &transformer{name: name, fnc: reflect.ValueOf(f)} - if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { tr.typ = ti } return tr @@ -346,7 +345,7 @@ func Comparer(f interface{}) Option { panic(fmt.Sprintf("invalid comparer function: %T", f)) } cm := &comparer{fnc: v} - if ti := v.Type().In(0); !value.IsEmptyInterface(ti) { + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { cm.typ = ti } return cm From 0a3ecd384c2ae80de757e4b62138949ab721f02e Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 12 Nov 2020 12:24:02 -0800 Subject: [PATCH 65/99] Fix Diff documentation (#237) The description inaccurately describes the operation of Diff, which is y - x, where a '+' prefix denotes elements added from y and a '-' prefix denotes elements removed from x. For example: // Consider this call to Diff and its result. x y cmp.Diff({b:2, c:3}, {a:1, b:2}) => {+a:1, b:2, -c:3} // Consider the same in mathematical notation. y - x {a:1, b:2} - {b:2, c:3} = {+a:1, b:2, -c:3} --- cmp/compare.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 83fc5a4..86d0903 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -100,8 +100,8 @@ func Equal(x, y interface{}, opts ...Option) bool { // 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 y, -// a "+" prefix to indicates an element added to y, and the lack of a prefix +// At the start of each line, a "-" prefix indicates an element removed from x, +// a "+" prefix to indicates an element added from y, and the lack of a prefix // indicates an element common to both x and y. If possible, the output // uses fmt.Stringer.String or error.Error methods to produce more humanly // readable outputs. In such cases, the string is prefixed with either an From ade6b74536ea3af0d70b4ebd51c08c5d31313078 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 23 Nov 2020 10:51:54 -0800 Subject: [PATCH 66/99] Use GitHub actions for testing (#246) Use a GitHub action to run test on each push and pull request. We test across a matrix covering Linux and MacOSX, and Go 1.8 to 1.15. --- .github/workflows/test.yml | 30 ++++++++++++++++++++++++++++++ .travis.yml | 35 ----------------------------------- 2 files changed, 30 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0120fdc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +on: [push, pull_request] +name: Test +jobs: + test: + env: + GOPATH: ${{ github.workspace }} + defaults: + run: + working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} + strategy: + matrix: + go-version: [1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v2 + with: + path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} + - name: Checkout dependencies + run: go get golang.org/x/xerrors + - name: Test + run: go test -v -race ./... + - name: Format + if: matrix.go-version == '1.15.x' + run: diff -u <(echo -n) <(gofmt -d .) diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 13d3717..0000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -sudo: false -language: go -matrix: - include: - - go: 1.8.x - script: - - go test -v -race ./... - - go: 1.9.x - script: - - go test -v -race ./... - - go: 1.10.x - script: - - go test -v -race ./... - - go: 1.11.x - script: - - 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: - - go test -v -race ./... - - go: 1.15.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 From 449e17c6c9daf9b0c84a35fef7d79321b9535763 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 24 Nov 2020 09:53:22 -0800 Subject: [PATCH 67/99] Fix non-determinism in diffing algorithm (#247) A previous attempt to add non-determinism to the diffing algorithm unfortunately broke the algorithm for half the cases. This change modifies the algorithm to truly switch between starting with a forward search versus a reverse search. The main for-loop of Difference would switch repeatedly between performing a forward search, then a reverse search, and vice-versa. Since we can't jump into the middle of a for-loop to start with the reverse search first, we use a series of labels and goto statements to accomplish the same effect. Fixes #238 --- cmp/internal/diff/diff.go | 48 +++++++++++++++++++--------------- cmp/internal/diff/diff_test.go | 27 +++++++++---------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index 441953e..bc196b1 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -119,7 +119,7 @@ func (r Result) Similar() bool { return r.NumSame+1 >= r.NumDiff } -var randInt = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) +var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 // Difference reports whether two lists of lengths nx and ny are equal // given the definition of equality provided as f. @@ -168,17 +168,6 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // 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. - // To ensure flexibility in changing the algorithm in the future, - // introduce some degree of deliberate instability. - // This is achieved by fiddling the zigzag iterator to start searching - // the graph starting from the bottom-right versus than the top-left. - // The result may differ depending on the starting search location, - // but still produces a valid edit script. - zigzagInit := randInt // either 0 or 1 - if flags.Deterministic { - zigzagInit = 0 - } - // Invariants: // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny @@ -197,6 +186,11 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // approximately the square-root of the search budget. searchBudget := 4 * (nx + ny) // O(n) + // Running the tests with the "cmp_debug" build tag prints a visualization + // of the algorithm running in real-time. This is educational for + // understanding how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + // The algorithm below is a greedy, meet-in-the-middle algorithm for // computing sub-optimal edit-scripts between two lists. // @@ -214,22 +208,28 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // frontier towards the opposite corner. // • This algorithm terminates when either the X coordinates or the // Y coordinates of the forward and reverse frontier points ever intersect. - // + // This algorithm is correct even if searching only in the forward direction // or in the reverse direction. We do both because it is commonly observed // that two lists commonly differ because elements were added to the front // or end of the other list. // - // Running the tests with the "cmp_debug" build tag prints a visualization - // of the algorithm running in real-time. This is educational for - // understanding how the algorithm works. See debug_enable.go. - f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) - for { + // Non-deterministically start with either the forward or reverse direction + // to introduce some deliberate instability so that we have the flexibility + // to change this algorithm in the future. + if flags.Deterministic || randBool { + goto forwardSearch + } else { + goto reverseSearch + } + +forwardSearch: + { // Forward search from the beginning. if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break + goto finishSearch } - for stop1, stop2, i := false, false, zigzagInit; !(stop1 && stop2) && searchBudget > 0; i++ { + 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} @@ -262,10 +262,14 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { } else { fwdFrontier.Y++ } + goto reverseSearch + } +reverseSearch: + { // Reverse search from the end. if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { - break + goto finishSearch } for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { // Search in a diagonal pattern for a match. @@ -300,8 +304,10 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { } else { revFrontier.Y-- } + goto forwardSearch } +finishSearch: // Join the forward and reverse paths and then append the reverse path. fwdPath.connect(revPath.point, f) for i := len(revPath.es) - 1; i >= 0; i-- { diff --git a/cmp/internal/diff/diff_test.go b/cmp/internal/diff/diff_test.go index d97fef8..eacf072 100644 --- a/cmp/internal/diff/diff_test.go +++ b/cmp/internal/diff/diff_test.go @@ -10,21 +10,15 @@ import ( "strings" "testing" "unicode" - - "github.com/google/go-cmp/cmp/internal/flags" ) -func init() { - flags.Deterministic = true -} - 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 + want string // '|' separated list of possible outputs }{{ x: "", y: "", @@ -36,7 +30,7 @@ func TestDifference(t *testing.T) { }, { x: "##", y: "# ", - want: ".X", + want: ".X|X.", }, { x: "a#", y: "A ", @@ -48,7 +42,7 @@ func TestDifference(t *testing.T) { }, { x: "# ", y: "##", - want: ".Y", + want: ".Y|Y.", }, { x: " #", y: "@#", @@ -148,7 +142,7 @@ func TestDifference(t *testing.T) { }, { x: "ABCAB BA ", y: " C BABAC", - want: "XX.X.Y..Y", + want: "XX.X.Y..Y|XX.Y.X..Y", }, { x: "# #### ###", y: "#y####yy###", @@ -164,7 +158,7 @@ func TestDifference(t *testing.T) { }, { x: "0 12z3x 456789 x x 0", y: "0y12Z3 y456789y y y0", - want: ".Y..M.XY......YXYXY.", + want: ".Y..M.XY......YXYXY.|.Y..M.XY......XYXYY.", }, { x: "0 2 4 6 8 ..................abXXcdEXF.ghXi", y: " 1 3 5 7 9..................AB CDE F.GH I", @@ -216,7 +210,7 @@ func TestDifference(t *testing.T) { }, { x: "0123456789 ", y: " 5678901234", - want: "XXXXX.....YYYYY", + want: "XXXXX.....YYYYY|YYYYY.....XXXXX", }, { x: "0123456789 ", y: " 4567890123", @@ -252,9 +246,14 @@ func TestDifference(t *testing.T) { x := strings.Replace(tt.x, " ", "", -1) y := strings.Replace(tt.y, " ", "", -1) es := testStrings(t, x, y) - if got := es.String(); got != tt.want { - t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, tt.want) + var want string + got := es.String() + for _, want = range strings.Split(tt.want, "|") { + if got == want { + return + } } + t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, want) }) } } From ec71d6d790538ad88c95a192fd059e11afb45b6f Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 24 Nov 2020 10:26:02 -0800 Subject: [PATCH 68/99] Impose verbosity limit when formatting map keys (#248) Map keys should have a sensible verbosity limit imposed, otherwise the reporter can end up printing a massive data structure that cannot reasonably fit in memory. --- cmp/compare_test.go | 11 +++++++++++ cmp/report_reflect.go | 2 ++ cmp/testdata/diffs | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 13a3a4e..84f7353 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1276,6 +1276,17 @@ using the AllowUnexported option.`, "\n"), y: MyComposite{}, wantEqual: false, reason: "batched diffing for empty slices and nil slices", + }, { + label: label + "/LargeMapKey", + x: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20, 1<<20) + return &b + }(): 0}, + y: map[*[]byte]int{func() *[]byte { + b := make([]byte, 1<<20, 1<<20) + return &b + }(): 0}, + reason: "printing map keys should have some verbosity limit imposed", }} } diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 5b47420..33f0357 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -351,6 +351,8 @@ func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) s opts.PrintAddresses = disambiguate opts.AvoidStringer = disambiguate opts.QualifiedNames = disambiguate + opts.VerbosityLevel = maxVerbosityPreset + opts.LimitVerbosity = true s := opts.FormatValue(v, reflect.Map, ptrs).String() return strings.TrimSpace(s) } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index dee035d..81d5769 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1008,6 +1008,12 @@ + FloatsC: nil, } >>> TestDiff/Reporter/EmptySlices +<<< TestDiff/Reporter/LargeMapKey + map[*[]uint8]int{ +- &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, ++ &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, + } +>>> TestDiff/Reporter/LargeMapKey <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 3a98a11b2c6d5ad66696f2954b3811f0244fb71d Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Fri, 5 Feb 2021 01:08:49 +0100 Subject: [PATCH 69/99] =?UTF-8?q?cmp/cmpopts:=20use=20errors.Is=20with=20?= =?UTF-8?q?=E2=89=A5go1.13=20in=20compareErrors=20(#251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the standard definition of errors.Is to implement compareErrors with ≥go1.13. Retain the implementation using golang.org/x/xerrors for versions Date: Sat, 20 Feb 2021 23:00:13 +0100 Subject: [PATCH 70/99] Run tests on Go 1.16 (#252) Add Go 1.16 to the GitHub actions test coverage matrix. Also switch the gofmt check to only run on Go 1.16, i.e. the latest supported version. --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0120fdc..6b1b1c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} strategy: matrix: - go-version: [1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x] + go-version: [1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: @@ -26,5 +26,5 @@ jobs: - name: Test run: go test -v -race ./... - name: Format - if: matrix.go-version == '1.15.x' + if: matrix.go-version == '1.16.x' run: diff -u <(echo -n) <(gofmt -d .) From dc6435e426906757e7c944f74758028d3d2edc3c Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 3 Mar 2021 09:41:45 -0800 Subject: [PATCH 71/99] De-virtualize interfaces for specialized diffing (#254) Specialized diffing strings and slices should occur for interface types where both values have the same concrete type. This is especially relevant for protocmp.Transform, which transforms every proto.Message as a map[string]interface{}. --- cmp/compare_test.go | 10 ++++++++++ cmp/report_slices.go | 25 +++++++++++++++++++++---- cmp/testdata/diffs | 22 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 84f7353..172e3d4 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1287,6 +1287,16 @@ using the AllowUnexported option.`, "\n"), return &b }(): 0}, reason: "printing map keys should have some verbosity limit imposed", + }, { + label: label + "/LargeStringInInterface", + x: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}, + y: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}, + reason: "strings within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeBytesInInterface", + x: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.")}, + y: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,")}, + reason: "bytes slice within an interface should benefit from specialized diffing", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index da04caf..168f92f 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -26,8 +26,6 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { return false // No differences detected case !v.ValueX.IsValid() || !v.ValueY.IsValid(): return false // Both values must be valid - case v.Type.Kind() == reflect.Slice && (v.ValueX.Len() == 0 || v.ValueY.Len() == 0): - return false // Both slice values have to be non-empty case v.NumIgnored > 0: return false // Some ignore option was used case v.NumTransformed > 0: @@ -45,7 +43,16 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { return false } - switch t := v.Type; t.Kind() { + // Check whether this is an interface with the same concrete types. + t := v.Type + vx, vy := v.ValueX, v.ValueY + if t.Kind() == reflect.Interface && !vx.IsNil() && !vy.IsNil() && vx.Elem().Type() == vy.Elem().Type() { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + } + + // Check whether we provide specialized diffing for this type. + switch t.Kind() { case reflect.String: case reflect.Array, reflect.Slice: // Only slices of primitive types have specialized handling. @@ -57,6 +64,11 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { return false } + // Both slice values have to be non-empty. + if t.Kind() == reflect.Slice && (vx.Len() == 0 || vy.Len() == 0) { + return false + } + // If a sufficient number of elements already differ, // use specialized formatting even if length requirement is not met. if v.NumDiff > v.NumSame { @@ -68,7 +80,7 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { // Use specialized string diffing for longer slices or strings. const minLength = 64 - return v.ValueX.Len() >= minLength && v.ValueY.Len() >= minLength + return vx.Len() >= minLength && vy.Len() >= minLength } // FormatDiffSlice prints a diff for the slices (or strings) represented by v. @@ -77,6 +89,11 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { assert(opts.DiffMode == diffUnknown) t, vx, vy := v.Type, v.ValueX, v.ValueY + if t.Kind() == reflect.Interface { + vx, vy = vx.Elem(), vy.Elem() + t = vx.Type() + opts = opts.WithTypeMode(emitType) + } // Auto-detect the type of the data. var isLinedText, isText, isBinary bool diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 81d5769..e2ffdd2 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1014,6 +1014,28 @@ + &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, } >>> TestDiff/Reporter/LargeMapKey +<<< TestDiff/Reporter/LargeStringInInterface + struct{ X interface{} }{ + X: strings.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeStringInInterface +<<< TestDiff/Reporter/LargeBytesInInterface + struct{ X interface{} }{ + X: bytes.Join({ + ... // 485 identical bytes + "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", + "citudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis", +- ".", ++ ",", + }, ""), + } +>>> TestDiff/Reporter/LargeBytesInInterface <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 8fa37b4dd109f12e42b131e485268768f18bcbf8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 3 Mar 2021 12:48:37 -0800 Subject: [PATCH 72/99] Fix reporter verbosity bug (#253) FormatDiff should only set the verbosity to 3 if the current verbosity is lower than 3. Otherwise, it may remove an intended higher verbosity setting causing the reporter output to not differentiate between two large values that are different at the end. While we are at it, increase the maxVerbosityPreset to 6. --- cmp/compare_test.go | 5 +++++ cmp/report_compare.go | 4 ++-- cmp/testdata/diffs | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 172e3d4..f7b1f13 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1297,6 +1297,11 @@ using the AllowUnexported option.`, "\n"), x: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.")}, y: struct{ X interface{} }{[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,")}, reason: "bytes slice within an interface should benefit from specialized diffing", + }, { + label: label + "/LargeStandaloneString", + x: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}}, + y: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}}, + reason: "printing a large standalone string that is different should print enough context to see the difference", }} } diff --git a/cmp/report_compare.go b/cmp/report_compare.go index a6c070c..104bb30 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -79,7 +79,7 @@ func (opts formatOptions) verbosity() uint { } } -const maxVerbosityPreset = 3 +const maxVerbosityPreset = 6 // verbosityPreset modifies the verbosity settings given an index // between 0 and maxVerbosityPreset, inclusive. @@ -100,7 +100,7 @@ func verbosityPreset(opts formatOptions, i int) formatOptions { func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) { if opts.DiffMode == diffIdentical { opts = opts.WithVerbosity(1) - } else { + } else if opts.verbosity() < 3 { opts = opts.WithVerbosity(3) } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index e2ffdd2..05118be 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1010,8 +1010,8 @@ >>> TestDiff/Reporter/EmptySlices <<< TestDiff/Reporter/LargeMapKey map[*[]uint8]int{ -- &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, -+ &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, +- &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, ++ &⟪0xdeadf00f⟫⟪ptr:0xdeadf00f, len:1048576, cap:1048576⟫{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...}: 0, } >>> TestDiff/Reporter/LargeMapKey <<< TestDiff/Reporter/LargeStringInInterface @@ -1036,6 +1036,16 @@ }, ""), } >>> TestDiff/Reporter/LargeBytesInInterface +<<< TestDiff/Reporter/LargeStandaloneString + struct{ X interface{} }{ +- X: [1]string{ +- "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.", +- }, ++ X: [1]string{ ++ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,", ++ }, + } +>>> TestDiff/Reporter/LargeStandaloneString <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 1ee4af8b89b19a908a39ae7281c93dcc59eeb4d3 Mon Sep 17 00:00:00 2001 From: Ikko Ashimine Date: Tue, 13 Apr 2021 01:47:06 +0900 Subject: [PATCH 73/99] Fix typo in path.go (#256) s/seperate/separate/ --- cmp/path.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/path.go b/cmp/path.go index 3d45c1a..f01eff3 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -315,7 +315,7 @@ func (tf Transform) Option() Option { return tf.trans } // 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. +// uses a separate stack for the x and y values. // // If a cycle is detected we need to determine whether the two pointers // should be considered equal. The definition of equality chosen by Equal From c5c3378d8544789f76de8a338a95307b0854e816 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 24 May 2021 17:33:36 -0700 Subject: [PATCH 74/99] Cleanup edit groups after coalescing (#259) Even with an optimal diffing algoritm, coalescing adjacent edit groups may cause the corresponding pair of strings for an edit group to have leading or trailing spans of equal elements. While this is technically a correct representation of a diff, it is a suboptimal outcome. As such, scan through all unequal groups and move leading/trailing equal spans to the preceding/succeeding equal group. Before this change: strings.Join({ "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", - ",#=_value _value=2 ", + " _value=2 ", `11 org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb`, - ",#=_value _value=2 2", + " _value=2 2", `1 org-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc`, - ",#=_value", ` _value=1 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, "=a,tag2", - "=dd,#=_value", + "=dd", ` _value=3 31 org-4747474747474747,bucket-4242424242424242:m,tag1`, - "=c,#=_value", + "=c", ` _value=4 41 `, }, "") After this change: strings.Join({ "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", - ",#=_value", ` _value=2 11 org-4747474747474747,bucket-4242424242424242:m,tag1`, "=a,tag2=bb", - ",#=_value", ` _value=2 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, "=b,tag2=cc", - ",#=_value", ` _value=1 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, "=a,tag2=dd", - ",#=_value", ` _value=3 31 org-4747474747474747,bucket-4242424242424242:m,tag1`, "=c", - ",#=_value", ` _value=4 41 `, }, "") --- cmp/compare_test.go | 5 ++ cmp/report_slices.go | 152 ++++++++++++++++++++++++++++++++++++++++--- cmp/testdata/diffs | 19 ++++++ 3 files changed, 166 insertions(+), 10 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index f7b1f13..7eac029 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1302,6 +1302,11 @@ using the AllowUnexported option.`, "\n"), x: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}}, y: struct{ X interface{} }{[1]string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}}, reason: "printing a large standalone string that is different should print enough context to see the difference", + }, { + label: label + "/SurroundingEqualElements", + x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,#=_value _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,#=_value _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,#=_value _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,#=_value _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c,#=_value _value=4 41\t", + y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\t", + reason: "leading/trailing equal spans should not appear in diff lines", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 168f92f..f985cc9 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -338,8 +338,11 @@ 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()) + eq := func(ix, iy int) bool { + return vx.Index(ix).Interface() == vy.Index(iy).Interface() + } + es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + return diff.BoolResult(eq(ix, iy)) }) appendChunks := func(v reflect.Value, d diffMode) int { @@ -364,6 +367,7 @@ func (opts formatOptions) formatDiffSlice( groups := coalesceAdjacentEdits(name, es) groups = coalesceInterveningIdentical(groups, chunkSize/4) + groups = cleanupSurroundingIdentical(groups, eq) maxGroup := diffStats{Name: name} for i, ds := range groups { if maxLen >= 0 && numDiffs >= maxLen { @@ -416,25 +420,36 @@ func (opts formatOptions) formatDiffSlice( // coalesceAdjacentEdits coalesces the list of edits into groups of adjacent // equal or unequal counts. +// +// Example: +// +// Input: "..XXY...Y" +// Output: [ +// {NumIdentical: 2}, +// {NumRemoved: 2, NumInserted 1}, +// {NumIdentical: 3}, +// {NumInserted: 1}, +// ] +// func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { - var prevCase int // Arbitrary index into which case last occurred - lastStats := func(i int) *diffStats { - if prevCase != i { + var prevMode byte + lastStats := func(mode byte) *diffStats { + if prevMode != mode { groups = append(groups, diffStats{Name: name}) - prevCase = i + prevMode = mode } return &groups[len(groups)-1] } for _, e := range es { switch e { case diff.Identity: - lastStats(1).NumIdentical++ + lastStats('=').NumIdentical++ case diff.UniqueX: - lastStats(2).NumRemoved++ + lastStats('!').NumRemoved++ case diff.UniqueY: - lastStats(2).NumInserted++ + lastStats('!').NumInserted++ case diff.Modified: - lastStats(2).NumModified++ + lastStats('!').NumModified++ } } return groups @@ -444,6 +459,35 @@ func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) // equal groups into adjacent unequal groups that currently result in a // dual inserted/removed printout. This acts as a high-pass filter to smooth // out high-frequency changes within the windowSize. +// +// Example: +// +// WindowSize: 16, +// Input: [ +// {NumIdentical: 61}, // group 0 +// {NumRemoved: 3, NumInserted: 1}, // group 1 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 9}, // └── coalesce +// {NumIdentical: 64}, // group 2 +// {NumRemoved: 3, NumInserted: 1}, // group 3 +// {NumIdentical: 6}, // ├── coalesce +// {NumInserted: 2}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 7}, // ├── coalesce +// {NumIdentical: 1}, // ├── coalesce +// {NumRemoved: 2}, // └── coalesce +// {NumIdentical: 63}, // group 4 +// ] +// Output: [ +// {NumIdentical: 61}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 64}, +// {NumIdentical: 8, NumRemoved: 12, NumInserted: 3}, +// {NumIdentical: 63}, +// ] +// func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { groups, groupsOrig := groups[:0], groups for i, ds := range groupsOrig { @@ -463,3 +507,91 @@ func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStat } return groups } + +// cleanupSurroundingIdentical scans through all unequal groups, and +// moves any leading sequence of equal elements to the preceding equal group and +// moves and trailing sequence of equal elements to the succeeding equal group. +// +// This is necessary since coalesceInterveningIdentical may coalesce edit groups +// together such that leading/trailing spans of equal elements becomes possible. +// Note that this can occur even with an optimal diffing algorithm. +// +// Example: +// +// Input: [ +// {NumIdentical: 61}, +// {NumIdentical: 1 , NumRemoved: 11, NumInserted: 2}, // assume 3 leading identical elements +// {NumIdentical: 67}, +// {NumIdentical: 7, NumRemoved: 12, NumInserted: 3}, // assume 10 trailing identical elements +// {NumIdentical: 54}, +// ] +// Output: [ +// {NumIdentical: 64}, // incremented by 3 +// {NumRemoved: 9}, +// {NumIdentical: 67}, +// {NumRemoved: 9}, +// {NumIdentical: 64}, // incremented by 10 +// ] +// +func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats { + var ix, iy int // indexes into sequence x and y + for i, ds := range groups { + // Handle equal group. + if ds.NumDiff() == 0 { + ix += ds.NumIdentical + iy += ds.NumIdentical + continue + } + + // Handle unequal group. + nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified + ny := ds.NumIdentical + ds.NumInserted + ds.NumModified + var numLeadingIdentical, numTrailingIdentical int + for i := 0; i < nx && i < ny && eq(ix+i, iy+i); i++ { + numLeadingIdentical++ + } + for i := 0; i < nx && i < ny && eq(ix+nx-1-i, iy+ny-1-i); i++ { + numTrailingIdentical++ + } + if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 { + if numLeadingIdentical > 0 { + // Remove leading identical span from this group and + // insert it into the preceding group. + if i-1 >= 0 { + groups[i-1].NumIdentical += numLeadingIdentical + } else { + // No preceding group exists, so prepend a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append([]diffStats{{Name: groups[0].Name, NumIdentical: numLeadingIdentical}}, groups...) + }() + } + // Increment indexes since the preceding group would have handled this. + ix += numLeadingIdentical + iy += numLeadingIdentical + } + if numTrailingIdentical > 0 { + // Remove trailing identical span from this group and + // insert it into the succeeding group. + if i+1 < len(groups) { + groups[i+1].NumIdentical += numTrailingIdentical + } else { + // No succeeding group exists, so append a new group, + // but do so after we finish iterating over all groups. + defer func() { + groups = append(groups, diffStats{Name: groups[len(groups)-1].Name, NumIdentical: numTrailingIdentical}) + }() + } + // Do not increment indexes since the succeeding group will handle this. + } + + // Update this group since some identical elements were removed. + nx -= numIdentical + ny -= numIdentical + groups[i] = diffStats{Name: ds.Name, NumRemoved: nx, NumInserted: ny} + } + ix += nx + iy += ny + } + return groups +} diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 05118be..2d9f9ab 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1046,6 +1046,25 @@ + }, } >>> TestDiff/Reporter/LargeStandaloneString +<<< TestDiff/Reporter/SurroundingEqualElements + strings.Join({ + "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", +- ",#=_value", + ` _value=2 11 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=a,tag2=bb", +- ",#=_value", + ` _value=2 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=b,tag2=cc", +- ",#=_value", + ` _value=1 21 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=a,tag2=dd", +- ",#=_value", + ` _value=3 31 org-4747474747474747,bucket-4242424242424242:m,tag1`, + "=c", +- ",#=_value", + ` _value=4 41 `, + }, "") +>>> TestDiff/Reporter/SurroundingEqualElements <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 9181d1e7c97613e12da2beec65ca4bd834a7a7da Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 24 May 2021 18:23:32 -0700 Subject: [PATCH 75/99] Avoid diffing by lines if inefficient (#260) Avoid diffing by lines if it turns out to be significantly less efficient than diffing by bytes. Before this change: ( """ - d5c14bdf6bac81c27afc5429500ed750 - 25483503b557c606dad4f144d27ae10b - 90bdbcdbb6ea7156068e3dcfb7459244 - 978f480a6e3cced51e297fbff9a506b7 + Xd5c14bdf6bac81c27afc5429500ed750 + X25483503b557c606dad4f144d27ae10b + X90bdbcdbb6ea7156068e3dcfb7459244 + X978f480a6e3cced51e297fbff9a506b7 """ ) After this change: strings.Join({ + "X", "d5c14bdf6bac81c27afc5429500ed750\n", + "X", "25483503b557c606dad4f144d27ae10b\n", + "X", "90bdbcdbb6ea7156068e3dcfb7459244\n", + "X", "978f480a6e3cced51e297fbff9a506b7\n", }, "") --- cmp/compare_test.go | 5 +++++ cmp/report_slices.go | 19 +++++++++++++++++-- cmp/testdata/diffs | 12 ++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 7eac029..baae591 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1307,6 +1307,11 @@ using the AllowUnexported option.`, "\n"), x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,#=_value _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,#=_value _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,#=_value _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,#=_value _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c,#=_value _value=4 41\t", y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\t", reason: "leading/trailing equal spans should not appear in diff lines", + }, { + label: label + "/AllLinesDiffer", + x: "d5c14bdf6bac81c27afc5429500ed750\n25483503b557c606dad4f144d27ae10b\n90bdbcdbb6ea7156068e3dcfb7459244\n978f480a6e3cced51e297fbff9a506b7\n", + y: "Xd5c14bdf6bac81c27afc5429500ed750\nX25483503b557c606dad4f144d27ae10b\nX90bdbcdbb6ea7156068e3dcfb7459244\nX978f480a6e3cced51e297fbff9a506b7\n", + reason: "all lines are different, so diffing based on lines is pointless", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index f985cc9..bd8ca15 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -98,6 +98,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // Auto-detect the type of the data. var isLinedText, isText, isBinary bool var sx, sy string + var ssx, ssy []string switch { case t.Kind() == reflect.String: sx, sy = vx.String(), vy.String() @@ -130,6 +131,22 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } isText = !isBinary isLinedText = isText && numLines >= 4 && maxLineLen <= 1024 + + // Avoid diffing by lines if it produces a significantly more complex + // edit script than diffing by bytes. + if isLinedText { + ssx = strings.Split(sx, "\n") + ssy = strings.Split(sy, "\n") + esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result { + return diff.BoolResult(ssx[ix] == ssy[iy]) + }) + esBytes := diff.Difference(len(sx), len(sy), func(ix, iy int) diff.Result { + return diff.BoolResult(sx[ix] == sy[iy]) + }) + efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) + efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) + isLinedText = efficiencyLines < 4*efficiencyBytes + } } // Format the string into printable records. @@ -139,8 +156,6 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // 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 { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 2d9f9ab..c02df29 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1065,6 +1065,18 @@ ` _value=4 41 `, }, "") >>> TestDiff/Reporter/SurroundingEqualElements +<<< TestDiff/Reporter/AllLinesDiffer + strings.Join({ ++ "X", + "d5c14bdf6bac81c27afc5429500ed750\n", ++ "X", + "25483503b557c606dad4f144d27ae10b\n", ++ "X", + "90bdbcdbb6ea7156068e3dcfb7459244\n", ++ "X", + "978f480a6e3cced51e297fbff9a506b7\n", + }, "") +>>> TestDiff/Reporter/AllLinesDiffer <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From d103655696d8ae43c4125ee61454dbf03d8e8324 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 24 May 2021 19:50:45 -0700 Subject: [PATCH 76/99] Print as text if mostly text (#258) The previous heuristic of treating strings as binary data if it contains any invalid UTF-8 was too strict. Loosen the heuristic to check if most of the characters are printable text. Fixes #257 --- cmp/compare_test.go | 5 +++++ cmp/report_slices.go | 35 ++++++++++++++++++----------------- cmp/testdata/diffs | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index baae591..649c917 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1307,6 +1307,11 @@ using the AllowUnexported option.`, "\n"), x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,#=_value _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,#=_value _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,#=_value _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,#=_value _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c,#=_value _value=4 41\t", y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\torg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\torg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\t", reason: "leading/trailing equal spans should not appear in diff lines", + }, { + label: label + "/MostlyTextString", + x: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa,\xff=_value _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb,\xff=_value _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc,\xff=_value _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd,\xff=_value _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1=c,\xff=_value _value=4 41\n", + y: "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=bb _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=b,tag2=cc _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=dd _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1=c _value=4 41\n", + reason: "the presence of a few invalid UTF-8 characters should not prevent printing this as text", }, { label: label + "/AllLinesDiffer", x: "d5c14bdf6bac81c27afc5429500ed750\n25483503b557c606dad4f144d27ae10b\n90bdbcdbb6ea7156068e3dcfb7459244\n978f480a6e3cced51e297fbff9a506b7\n", diff --git a/cmp/report_slices.go b/cmp/report_slices.go index bd8ca15..2ad3bc8 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -7,6 +7,7 @@ package cmp import ( "bytes" "fmt" + "math" "reflect" "strconv" "strings" @@ -96,16 +97,16 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { } // Auto-detect the type of the data. - var isLinedText, isText, isBinary bool var sx, sy string var ssx, ssy []string + var isString, isMostlyText, isPureLinedText, isBinary bool switch { case t.Kind() == reflect.String: sx, sy = vx.String(), vy.String() - isText = true // Initial estimate, verify later + isString = true 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 + isString = true case t.Kind() == reflect.Array: // Arrays need to be addressable for slice operations to work. vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem() @@ -113,13 +114,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { vy2.Set(vy) vx, vy = vx2, vy2 } - if isText || isBinary { - var numLines, lastLineIdx, maxLineLen int - isBinary = !utf8.ValidString(sx) || !utf8.ValidString(sy) + if isString { + var numTotalRunes, numValidRunes, numLines, lastLineIdx, maxLineLen int for i, r := range sx + sy { - if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError { - isBinary = true - break + numTotalRunes++ + if (unicode.IsPrint(r) || unicode.IsSpace(r)) && r != utf8.RuneError { + numValidRunes++ } if r == '\n' { if maxLineLen < i-lastLineIdx { @@ -129,12 +129,14 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { numLines++ } } - isText = !isBinary - isLinedText = isText && numLines >= 4 && maxLineLen <= 1024 + isPureText := numValidRunes == numTotalRunes + isMostlyText = float64(numValidRunes) > math.Floor(0.90*float64(numTotalRunes)) + isPureLinedText = isPureText && numLines >= 4 && maxLineLen <= 1024 + isBinary = !isMostlyText // Avoid diffing by lines if it produces a significantly more complex // edit script than diffing by bytes. - if isLinedText { + if isPureLinedText { ssx = strings.Split(sx, "\n") ssy = strings.Split(sy, "\n") esLines := diff.Difference(len(ssx), len(ssy), func(ix, iy int) diff.Result { @@ -145,7 +147,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { }) efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) - isLinedText = efficiencyLines < 4*efficiencyBytes + isPureLinedText = efficiencyLines < 4*efficiencyBytes } } @@ -155,7 +157,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { switch { // If the text appears to be multi-lined text, // then perform differencing across individual lines. - case isLinedText: + case isPureLinedText: list = opts.formatDiffSlice( reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line", func(v reflect.Value, d diffMode) textRecord { @@ -244,7 +246,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // 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: + case isMostlyText: list = opts.formatDiffSlice( reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte", func(v reflect.Value, d diffMode) textRecord { @@ -252,7 +254,6 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { return textRecord{Diff: d, Value: textLine(s)} }, ) - delim = "" // If the text appears to be binary data, // then perform differencing in approximately fixed-sized chunks. @@ -314,7 +315,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // Wrap the output with appropriate type information. var out textNode = &textWrap{Prefix: "{", Value: list, Suffix: "}"} - if !isText { + if !isMostlyText { // The "{...}" byte-sequence literal is not valid Go syntax for strings. // Emit the type for extra clarity (e.g. "string{...}"). if t.Kind() == reflect.String { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index c02df29..a3d5909 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1065,6 +1065,25 @@ ` _value=4 41 `, }, "") >>> TestDiff/Reporter/SurroundingEqualElements +<<< TestDiff/Reporter/MostlyTextString + strings.Join({ + "org-4747474747474747,bucket-4242424242424242:m,tag1=a,tag2=aa", +- ",\xff=_value", + " _value=2 11\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=a,tag2=bb", +- ",\xff=_value", + " _value=2 21\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=b,tag2=cc", +- ",\xff=_value", + " _value=1 21\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=a,tag2=dd", +- ",\xff=_value", + " _value=3 31\norg-4747474747474747,bucket-4242424242424242:m,tag1", + "=c", +- ",\xff=_value", + " _value=4 41\n", + }, "") +>>> TestDiff/Reporter/MostlyTextString <<< TestDiff/Reporter/AllLinesDiffer strings.Join({ + "X", From 248ccfffa4b8906ebc7f9ab720fe5b6012bb7745 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 27 May 2021 09:45:01 -0700 Subject: [PATCH 77/99] Fix staticcheck findings (#262) Address some minor issues flagged by staticcheck. None of these affect the correctness of the package. --- cmp/compare_test.go | 8 ++++---- cmp/example_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 649c917..dc0bfe0 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -657,8 +657,8 @@ func comparerTests() []test { reason: "all zero map entries are ignored (even if missing)", }, { label: label + "/PanicUnexportedNamed", - x: namedWithUnexported{}, - y: namedWithUnexported{}, + x: namedWithUnexported{unexported: "x"}, + y: namedWithUnexported{unexported: "y"}, wantPanic: strconv.Quote(reflect.TypeOf(namedWithUnexported{}).PkgPath()) + ".namedWithUnexported", reason: "panic on named struct type with unexported field", }, { @@ -1279,11 +1279,11 @@ using the AllowUnexported option.`, "\n"), }, { label: label + "/LargeMapKey", x: map[*[]byte]int{func() *[]byte { - b := make([]byte, 1<<20, 1<<20) + b := make([]byte, 1<<20) return &b }(): 0}, y: map[*[]byte]int{func() *[]byte { - b := make([]byte, 1<<20, 1<<20) + b := make([]byte, 1<<20) return &b }(): 0}, reason: "printing map keys should have some verbosity limit imposed", diff --git a/cmp/example_test.go b/cmp/example_test.go index d165383..e1f4338 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -98,9 +98,9 @@ func ExampleOption_equalNaNs() { return (math.IsNaN(x) && math.IsNaN(y)) || x == y }) - x := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} - y := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} - z := []float64{1.0, math.NaN(), math.Pi, -0.0, +0.0} // Pi constant instead of E + x := []float64{1.0, math.NaN(), math.E, 0.0} + y := []float64{1.0, math.NaN(), math.E, 0.0} + z := []float64{1.0, math.NaN(), math.Pi, 0.0} // Pi constant instead of E fmt.Println(cmp.Equal(x, y, opt)) fmt.Println(cmp.Equal(y, z, opt)) @@ -216,7 +216,7 @@ func ExampleOption_sortedSlice() { type otherString string func (x otherString) Equal(y otherString) bool { - return strings.ToLower(string(x)) == strings.ToLower(string(y)) + return strings.EqualFold(string(x), string(y)) } // If the Equal method defined on a type is not suitable, the type can be From 290a6a23966f9edffe2a0a4a1d8dd065cc0753fd Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 27 May 2021 10:48:29 -0700 Subject: [PATCH 78/99] Avoid shadowing variable (#263) Rename the shadowed variable i to j for better readability. --- cmp/report_slices.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 2ad3bc8..9809228 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -563,10 +563,10 @@ func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []d nx := ds.NumIdentical + ds.NumRemoved + ds.NumModified ny := ds.NumIdentical + ds.NumInserted + ds.NumModified var numLeadingIdentical, numTrailingIdentical int - for i := 0; i < nx && i < ny && eq(ix+i, iy+i); i++ { + for j := 0; j < nx && j < ny && eq(ix+j, iy+j); j++ { numLeadingIdentical++ } - for i := 0; i < nx && i < ny && eq(ix+nx-1-i, iy+ny-1-i); i++ { + for j := 0; j < nx && j < ny && eq(ix+nx-1-j, iy+ny-1-j); j++ { numTrailingIdentical++ } if numIdentical := numLeadingIdentical + numTrailingIdentical; numIdentical > 0 { From d5fcb386864232c4b315ea7f9838e455a2ce3cda Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Sun, 18 Jul 2021 17:32:48 -0700 Subject: [PATCH 79/99] Fix textual printing of byte slices There are two bugs being fixed: 1. The hueristic for whether a slice of byte looks like text should check whether a rune IsPrint OR IsSpace, and not both. Only a single rune (i.e., U+0020) ever satisfies both conditions. Previously, it would print as: MyBytes{0x68, 0x65, 0x6c, 0x6c, 0x6f} and now it would now print as: MyBytes(MyBytes("hello")) 2. If we're printing as string, then we should set skipType=true since we already explicitly format the value with the type. Previously, it would print as: MyBytes(MyBytes("hello")) and now it would now print as: MyBytes("hello") --- cmp/compare_test.go | 20 ++++++++++++++++++++ cmp/report_reflect.go | 3 ++- cmp/testdata/diffs | 24 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index dc0bfe0..a435209 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1317,6 +1317,26 @@ using the AllowUnexported option.`, "\n"), x: "d5c14bdf6bac81c27afc5429500ed750\n25483503b557c606dad4f144d27ae10b\n90bdbcdbb6ea7156068e3dcfb7459244\n978f480a6e3cced51e297fbff9a506b7\n", y: "Xd5c14bdf6bac81c27afc5429500ed750\nX25483503b557c606dad4f144d27ae10b\nX90bdbcdbb6ea7156068e3dcfb7459244\nX978f480a6e3cced51e297fbff9a506b7\n", reason: "all lines are different, so diffing based on lines is pointless", + }, { + label: label + "/StringifiedBytes", + x: struct{ X []byte }{[]byte("hello, world!")}, + y: struct{ X []byte }{}, + reason: "[]byte should be printed as text since it is printable text", + }, { + label: label + "/NonStringifiedBytes", + x: struct{ X []byte }{[]byte("\xde\xad\xbe\xef")}, + y: struct{ X []byte }{}, + reason: "[]byte should not be printed as text since it is binary data", + }, { + label: label + "/StringifiedNamedBytes", + x: struct{ X MyBytes }{MyBytes("hello, world!")}, + y: struct{ X MyBytes }{}, + reason: "MyBytes should be printed as text since it is printable text", + }, { + label: label + "/NonStringifiedNamedBytes", + x: struct{ X MyBytes }{MyBytes("\xde\xad\xbe\xef")}, + y: struct{ X MyBytes }{}, + reason: "MyBytes should not be printed as text since it is binary data", }} } diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 33f0357..76c04fd 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -207,9 +207,10 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, // Check whether this is a []byte of text data. if t.Elem() == reflect.TypeOf(byte(0)) { b := v.Bytes() - isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) } + isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { out = opts.formatString("", string(b)) + skipType = true return opts.WithTypeMode(emitType).FormatType(t, out) } } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index a3d5909..96ea191 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1096,6 +1096,30 @@ "978f480a6e3cced51e297fbff9a506b7\n", }, "") >>> TestDiff/Reporter/AllLinesDiffer +<<< TestDiff/Reporter/StringifiedBytes + struct{ X []uint8 }{ +- X: []uint8("hello, world!"), ++ X: nil, + } +>>> TestDiff/Reporter/StringifiedBytes +<<< TestDiff/Reporter/NonStringifiedBytes + struct{ X []uint8 }{ +- X: []uint8{0xde, 0xad, 0xbe, 0xef}, ++ X: nil, + } +>>> TestDiff/Reporter/NonStringifiedBytes +<<< TestDiff/Reporter/StringifiedNamedBytes + struct{ X cmp_test.MyBytes }{ +- X: cmp_test.MyBytes("hello, world!"), ++ X: nil, + } +>>> TestDiff/Reporter/StringifiedNamedBytes +<<< TestDiff/Reporter/NonStringifiedNamedBytes + struct{ X cmp_test.MyBytes }{ +- X: cmp_test.MyBytes{0xde, 0xad, 0xbe, 0xef}, ++ X: nil, + } +>>> TestDiff/Reporter/NonStringifiedNamedBytes <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 395a0ac549058d781199661ca2e37cc636db43e4 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 22 Jul 2021 16:14:42 -0700 Subject: [PATCH 80/99] Use sha256 in test (#268) Some aggressive dependency checks flag the use of md5. Switch to sha256 as it accomplishes the same purpose. --- cmp/compare_test.go | 6 +++--- cmp/testdata/diffs | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index a435209..5cdce37 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -6,7 +6,7 @@ package cmp_test import ( "bytes" - "crypto/md5" + "crypto/sha256" "encoding/json" "errors" "flag" @@ -476,8 +476,8 @@ func comparerTests() []test { reason: "comparer for fmt.Stringer used to compare differing types with different strings", }, { label: label + "/DifferingHash", - x: md5.Sum([]byte{'a'}), - y: md5.Sum([]byte{'b'}), + x: sha256.Sum256([]byte{'a'}), + y: sha256.Sum256([]byte{'b'}), wantEqual: false, reason: "hash differs", }, { diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 96ea191..afc7366 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -38,9 +38,11 @@ ) >>> TestDiff/Comparer/StringerInequal <<< TestDiff/Comparer/DifferingHash - [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, + [32]uint8{ +- 0xca, 0x97, 0x81, 0x12, 0xca, 0x1b, 0xbd, 0xca, 0xfa, 0xc2, 0x31, 0xb3, 0x9a, 0x23, 0xdc, 0x4d, +- 0xa7, 0x86, 0xef, 0xf8, 0x14, 0x7c, 0x4e, 0x72, 0xb9, 0x80, 0x77, 0x85, 0xaf, 0xee, 0x48, 0xbb, ++ 0x3e, 0x23, 0xe8, 0x16, 0x00, 0x39, 0x59, 0x4a, 0x33, 0x89, 0x4f, 0x65, 0x64, 0xe1, 0xb1, 0x34, ++ 0x8b, 0xbd, 0x7a, 0x00, 0x88, 0xd4, 0x2c, 0x4a, 0xcb, 0x73, 0xee, 0xae, 0xd5, 0x9c, 0x00, 0x9d, } >>> TestDiff/Comparer/DifferingHash <<< TestDiff/Comparer/NilStringer From 3ee52c8a5607a16505c9c7141153ff82d360817a Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 16 Sep 2021 15:55:50 -0700 Subject: [PATCH 81/99] Fix spelling mistakes (#271) --- cmp/cmpopts/equate.go | 2 +- cmp/compare_test.go | 7 ++++--- cmp/path.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index e4ffca8..62837c9 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -111,7 +111,7 @@ type timeApproximator struct { 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. + // difference is larger than the largest representable duration. if x.After(y) { // Ensure x is always before y x, y = y, x diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 5cdce37..5f02a82 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -515,7 +515,7 @@ func comparerTests() []test { wantPanic: "non-deterministic or non-symmetric function detected", reason: "non-deterministic filter", }, { - label: label + "/AssymetricComparer", + label: label + "/AsymmetricComparer", x: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, y: []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, opts: []cmp.Option{ @@ -1288,8 +1288,9 @@ using the AllowUnexported option.`, "\n"), }(): 0}, reason: "printing map keys should have some verbosity limit imposed", }, { - label: label + "/LargeStringInInterface", - x: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}, + label: label + "/LargeStringInInterface", + x: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis."}, + y: struct{ X interface{} }{"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis,"}, reason: "strings within an interface should benefit from specialized diffing", }, { diff --git a/cmp/path.go b/cmp/path.go index f01eff3..c710034 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -178,7 +178,7 @@ type structField struct { unexported bool mayForce bool // Forcibly allow visibility paddr bool // Was parent addressable? - pvx, pvy reflect.Value // Parent values (always addressible) + pvx, pvy reflect.Value // Parent values (always addressable) field reflect.StructField // Field information } From 9094ef9afacbd93e9f232d0ccadaf8530f353441 Mon Sep 17 00:00:00 2001 From: Jake Son Date: Fri, 17 Sep 2021 08:00:29 +0900 Subject: [PATCH 82/99] Change build status badge (#269) Co-authored-by: Damien Neil --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed0eb9b..e592e3a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 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] +[![Build Status](https://github.com/google/go-cmp/actions/workflows/test.yml/badge.svg?branch=master)][actions] This package is intended to be a more powerful and safer alternative to `reflect.DeepEqual` for comparing whether two values are semantically equal. @@ -29,7 +29,7 @@ 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 +[actions]: https://github.com/google/go-cmp/actions ## Install From f1773ad7bdb1ddccb1801c5b8ca4311cf0d301e4 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 12 Oct 2021 10:46:09 -0700 Subject: [PATCH 83/99] Use any alias instead of interface{} (#276) See golang/go#33232. --- cmp/internal/value/name.go | 7 +++++ cmp/internal/value/name_test.go | 2 +- cmp/testdata/diffs | 46 ++++++++++++++++----------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/cmp/internal/value/name.go b/cmp/internal/value/name.go index b6c12ce..7b498bb 100644 --- a/cmp/internal/value/name.go +++ b/cmp/internal/value/name.go @@ -9,6 +9,8 @@ import ( "strconv" ) +var anyType = reflect.TypeOf((*interface{})(nil)).Elem() + // TypeString is nearly identical to reflect.Type.String, // but has an additional option to specify that full type names be used. func TypeString(t reflect.Type, qualified bool) string { @@ -20,6 +22,11 @@ func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte // of the same name and within the same package, // but declared within the namespace of different functions. + // Use the "any" alias instead of "interface{}" for better readability. + if t == anyType { + return append(b, "any"...) + } + // Named type. if t.Name() != "" { if qualified && t.PkgPath() != "" { diff --git a/cmp/internal/value/name_test.go b/cmp/internal/value/name_test.go index 3eec91c..c177e72 100644 --- a/cmp/internal/value/name_test.go +++ b/cmp/internal/value/name_test.go @@ -111,7 +111,7 @@ func TestTypeString(t *testing.T) { want: "*$PackagePath.Named", }, { in: (*interface{})(nil), - want: "*interface{}", + want: "*any", }, { in: (*interface{ Read([]byte) (int, error) })(nil), want: "*interface{ Read([]uint8) (int, error) }", diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index afc7366..59afe6d 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -46,7 +46,7 @@ } >>> TestDiff/Comparer/DifferingHash <<< TestDiff/Comparer/NilStringer - interface{}( + any( - &fmt.Stringer(nil), ) >>> TestDiff/Comparer/NilStringer @@ -135,19 +135,19 @@ ) >>> TestDiff/Comparer/StringerMapKey <<< TestDiff/Comparer/StringerBacktick - interface{}( + any( - []*testprotos.Stringer{s`multi\nline\nline\nline`}, ) >>> TestDiff/Comparer/StringerBacktick <<< TestDiff/Comparer/DynamicMap - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "avg": float64(0.278), - "hr": int(65), + "hr": float64(65), "name": string("Mark McGwire"), }, - map[string]interface{}{ + map[string]any{ "avg": float64(0.288), - "hr": int(63), + "hr": float64(63), @@ -199,14 +199,14 @@ } >>> TestDiff/Transformer/Filtered <<< TestDiff/Transformer/DisjointOutput - int(Inverse(λ, interface{}( + int(Inverse(λ, any( - string("zero"), + float64(1), ))) >>> TestDiff/Transformer/DisjointOutput <<< TestDiff/Transformer/JSON - string(Inverse(ParseJSON, map[string]interface{}{ - "address": map[string]interface{}{ + string(Inverse(ParseJSON, map[string]any{ + "address": map[string]any{ - "city": string("Los Angeles"), + "city": string("New York"), "postalCode": string("10021-3100"), @@ -215,18 +215,18 @@ "streetAddress": string("21 2nd Street"), }, "age": float64(25), - "children": []interface{}{}, + "children": []any{}, "firstName": string("John"), "isAlive": bool(true), "lastName": string("Smith"), - "phoneNumbers": []interface{}{ - map[string]interface{}{ + "phoneNumbers": []any{ + map[string]any{ - "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")}, + map[string]any{"number": string("646 555-4567"), "type": string("office")}, + map[string]any{"number": string("123 456-7890"), "type": string("mobile")}, }, + "spouse": nil, })) @@ -267,7 +267,7 @@ } >>> TestDiff/Reporter/PanicError <<< TestDiff/Reporter/AmbiguousType - interface{}( + any( - "github.com/google/go-cmp/cmp/internal/teststructs/foo1".Bar{}, + "github.com/google/go-cmp/cmp/internal/teststructs/foo2".Bar{}, ) @@ -297,7 +297,7 @@ } >>> TestDiff/Reporter/AmbiguousPointerMap <<< TestDiff/Reporter/AmbiguousStringer - interface{}( + any( - cmp_test.Stringer("hello"), + &cmp_test.Stringer("hello"), ) @@ -327,7 +327,7 @@ ) >>> TestDiff/Reporter/AmbiguousSliceHeader <<< TestDiff/Reporter/AmbiguousStringerMapKey - map[interface{}]string{ + map[any]string{ - nil: "nil", + &⟪0xdeadf00f⟫"github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", - "github.com/google/go-cmp/cmp_test".Stringer("hello"): "goodbye", @@ -336,13 +336,13 @@ } >>> TestDiff/Reporter/AmbiguousStringerMapKey <<< TestDiff/Reporter/NonAmbiguousStringerMapKey - map[interface{}]string{ + map[any]string{ + s"fizz": "buzz", - s"hello": "goodbye", } >>> TestDiff/Reporter/NonAmbiguousStringerMapKey <<< TestDiff/Reporter/InvalidUTF8 - interface{}( + any( - cmp_test.MyString("\xed\xa0\x80"), ) >>> TestDiff/Reporter/InvalidUTF8 @@ -399,7 +399,7 @@ } >>> TestDiff/Reporter/BatchedWithComparer <<< TestDiff/Reporter/BatchedLong - interface{}( + any( - cmp_test.MyComposite{IntsA: []int8{0, 1, 2, 3, 4, 5, 6, 7, ...}}, ) >>> TestDiff/Reporter/BatchedLong @@ -1017,7 +1017,7 @@ } >>> TestDiff/Reporter/LargeMapKey <<< TestDiff/Reporter/LargeStringInInterface - struct{ X interface{} }{ + struct{ X any }{ X: strings.Join({ ... // 485 identical bytes "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", @@ -1028,7 +1028,7 @@ } >>> TestDiff/Reporter/LargeStringInInterface <<< TestDiff/Reporter/LargeBytesInInterface - struct{ X interface{} }{ + struct{ X any }{ X: bytes.Join({ ... // 485 identical bytes "s mus. Pellentesque mi lorem, consectetur id porttitor id, solli", @@ -1039,7 +1039,7 @@ } >>> TestDiff/Reporter/LargeBytesInInterface <<< TestDiff/Reporter/LargeStandaloneString - struct{ X interface{} }{ + struct{ X any }{ - X: [1]string{ - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet pretium ligula, at gravida quam. Integer iaculis, velit at sagittis ultricies, lacus metus scelerisque turpis, ornare feugiat nulla nisl ac erat. Maecenas elementum ultricies libero, sed efficitur lacus molestie non. Nulla ac pretium dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mi lorem, consectetur id porttitor id, sollicitudin sit amet enim. Duis eu dolor magna. Nunc ut augue turpis.", - }, @@ -1614,7 +1614,7 @@ ... // 4 identical fields ContSlaps: nil, ContSlapsInterval: 0, - Animal: []interface{}{ + Animal: []any{ teststructs.Goat{ Target: "corporation", Slaps: nil, From 6faefd0594fae82639a62c23f0aed1451509dcc0 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 12 Oct 2021 11:15:02 -0700 Subject: [PATCH 84/99] Reduce minimum length for specialize string diffing (#275) The original threshold of 64 was chosen without much thought. Lower it to 32 now that we have some concrete examples that it is aesthetically better. Co-authored-by: Damien Neil --- cmp/compare_test.go | 13 +++++++++++++ cmp/report_slices.go | 2 +- cmp/testdata/diffs | 12 ++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 5f02a82..c7a409d 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1338,6 +1338,19 @@ using the AllowUnexported option.`, "\n"), x: struct{ X MyBytes }{MyBytes("\xde\xad\xbe\xef")}, y: struct{ X MyBytes }{}, reason: "MyBytes should not be printed as text since it is binary data", + }, { + label: label + "/ShortJSON", + x: `{ + "id": 1, + "foo": true, + "bar": true, +}`, + y: `{ + "id": 1434180, + "foo": true, + "bar": true, +}`, + reason: "short multiline JSON should prefer triple-quoted string diff as it is more readable", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 9809228..68b5c1a 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -80,7 +80,7 @@ func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool { } // Use specialized string diffing for longer slices or strings. - const minLength = 64 + const minLength = 32 return vx.Len() >= minLength && vy.Len() >= minLength } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 59afe6d..d207803 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1122,6 +1122,18 @@ + X: nil, } >>> TestDiff/Reporter/NonStringifiedNamedBytes +<<< TestDiff/Reporter/ShortJSON + ( + """ + { +- "id": 1, ++ "id": 1434180, + "foo": true, + "bar": true, + } + """ + ) +>>> TestDiff/Reporter/ShortJSON <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From f59cd61478ea347f01124caa5fe7cafe512a4c8f Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 7 Dec 2021 11:10:08 -0800 Subject: [PATCH 85/99] Update minimum supported version to go1.11 (#281) --- .github/workflows/test.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b1b1c4..2e6a1c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} strategy: matrix: - go-version: [1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x] + go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: diff --git a/go.mod b/go.mod index 5391dee..4cdb762 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/google/go-cmp -go 1.8 +go 1.11 require golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 From 3242228551a7556abc624443ef03a6dbfeeacd22 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 7 Dec 2021 13:01:18 -0800 Subject: [PATCH 86/99] Drop hacks to work around Go reflection bugs in Go1.9 (#282) Now that Go 1.11 is the minimally supported version, we can drop some local hacks to work around bugs in reflect that were present in Go1.9. --- cmp/compare.go | 17 ----------------- cmp/compare_test.go | 14 ++------------ cmp/internal/flags/toolchain_legacy.go | 10 ---------- cmp/internal/flags/toolchain_recent.go | 10 ---------- 4 files changed, 2 insertions(+), 49 deletions(-) delete mode 100644 cmp/internal/flags/toolchain_legacy.go delete mode 100644 cmp/internal/flags/toolchain_recent.go diff --git a/cmp/compare.go b/cmp/compare.go index 86d0903..2a54467 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -36,7 +36,6 @@ import ( "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" ) @@ -319,7 +318,6 @@ func (s *state) tryMethod(t reflect.Type, vx, vy reflect.Value) bool { } 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] } @@ -343,8 +341,6 @@ func (s *state) callTRFunc(f, v reflect.Value, step Transform) reflect.Value { } func (s *state) callTTBFunc(f, x, y reflect.Value) bool { - x = sanitizeValue(x, f.Type().In(0)) - y = sanitizeValue(y, f.Type().In(1)) if !s.dynChecker.Next() { return f.Call([]reflect.Value{x, y})[0].Bool() } @@ -372,19 +368,6 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { ret = f.Call(vs)[0] } -// sanitizeValue converts nil interfaces of type T to those of type R, -// assuming that T is assignable to R. -// Otherwise, it returns the input value as is. -func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value { - // TODO(≥go1.10): 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 diff --git a/cmp/compare_test.go b/cmp/compare_test.go index c7a409d..9ad9456 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1447,14 +1447,6 @@ func embeddedTests() []test { return s } - // TODO(≥go1.10): 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/PanicUnexported1", x: ts.ParentStructA{}, @@ -1745,8 +1737,7 @@ func embeddedTests() []test { label: label + "/ParentStructG/PanicUnexported1", x: ts.ParentStructG{}, y: ts.ParentStructG{}, - wantPanic: wantPanicNotGo110("cannot handle unexported field"), - wantEqual: !flags.AtLeastGo110, + wantPanic: "cannot handle unexported field", reason: "ParentStructG has unexported fields", }, { label: label + "/ParentStructG/Ignored", @@ -1836,8 +1827,7 @@ func embeddedTests() []test { label: label + "/ParentStructI/PanicUnexported1", x: ts.ParentStructI{}, y: ts.ParentStructI{}, - wantPanic: wantPanicNotGo110("cannot handle unexported field"), - wantEqual: !flags.AtLeastGo110, + wantPanic: "cannot handle unexported field", reason: "ParentStructI has unexported fields", }, { label: label + "/ParentStructI/Ignored1", diff --git a/cmp/internal/flags/toolchain_legacy.go b/cmp/internal/flags/toolchain_legacy.go deleted file mode 100644 index 82d1d7f..0000000 --- a/cmp/internal/flags/toolchain_legacy.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +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 deleted file mode 100644 index 8646f05..0000000 --- a/cmp/internal/flags/toolchain_recent.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build go1.10 - -package flags - -// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10. -const AtLeastGo110 = true From 039e37cba1f3e52c48404633d6960421b369a19a Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Tue, 4 Jan 2022 18:50:40 +0100 Subject: [PATCH 87/99] Add //go:build lines (#285) Starting with Go 1.17, //go:build lines are preferred over // +build lines, see https://golang.org/doc/go1.17#build-lines and https://golang.org/design/draft-gobuild for details. This change was generated by running Go 1.17 go fmt ./... which automatically adds //go:build lines based on the existing // +build lines. Also update the corresponding GitHub action to use Go 1.17 gofmt. --- .github/workflows/test.yml | 2 +- cmp/cmpopts/errors_go113.go | 1 + cmp/cmpopts/errors_xerrors.go | 1 + cmp/export_panic.go | 1 + cmp/export_unsafe.go | 1 + cmp/internal/diff/debug_disable.go | 1 + cmp/internal/diff/debug_enable.go | 1 + cmp/internal/value/pointer_purego.go | 1 + cmp/internal/value/pointer_unsafe.go | 1 + 9 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e6a1c9..5664da6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,5 @@ jobs: - name: Test run: go test -v -race ./... - name: Format - if: matrix.go-version == '1.16.x' + if: matrix.go-version == '1.17.x' run: diff -u <(echo -n) <(gofmt -d .) diff --git a/cmp/cmpopts/errors_go113.go b/cmp/cmpopts/errors_go113.go index 26fe25d..8eb2b84 100644 --- a/cmp/cmpopts/errors_go113.go +++ b/cmp/cmpopts/errors_go113.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.13 // +build go1.13 package cmpopts diff --git a/cmp/cmpopts/errors_xerrors.go b/cmp/cmpopts/errors_xerrors.go index 6eeb8d6..60b0727 100644 --- a/cmp/cmpopts/errors_xerrors.go +++ b/cmp/cmpopts/errors_xerrors.go @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build !go1.13 // +build !go1.13 // TODO(≥go1.13): For support on Date: Tue, 22 Mar 2022 22:05:27 +0800 Subject: [PATCH 88/99] Run tests on Go 1.18 (#290) --- .github/workflows/test.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5664da6..9a50673 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,14 +2,9 @@ on: [push, pull_request] name: Test jobs: test: - env: - GOPATH: ${{ github.workspace }} - defaults: - run: - working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} strategy: matrix: - go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x] + go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: @@ -19,12 +14,8 @@ jobs: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v2 - with: - path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} - - name: Checkout dependencies - run: go get golang.org/x/xerrors - name: Test run: go test -v -race ./... - name: Format - if: matrix.go-version == '1.17.x' + if: matrix.go-version == '1.18.x' run: diff -u <(echo -n) <(gofmt -d .) From 4664e24d52beca933d6f5378d36e1d405099115d Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Apr 2022 09:26:47 -0700 Subject: [PATCH 89/99] Fix printing of types in reporter output (#293) When printing a pointer, only elide the type for unnamed pointers. Otherwise, we can run into situations where named and unnamed pointers format the same way in indistinguishable ways. When printing an interview, never skip the interface type. Whether we skip printing the type should be determined by the parent containers, and not locally determined. For examples, interface values within a struct, slice, or map will always be elided since they can be inferred. --- cmp/compare.go | 2 ++ cmp/compare_test.go | 34 ++++++++++++++++++++++++++++++++++ cmp/report_reflect.go | 8 ++++++-- cmp/testdata/diffs | 24 ++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index 2a54467..fd2b3a4 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -40,6 +40,8 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) +// TODO(≥go1.18): Use any instead of interface{}. + // Equal reports whether x and y are equal by recursively applying the // following rules in the given order to x and y and all of their sub-values: // diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 9ad9456..7278485 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -104,6 +104,7 @@ func mustFormatGolden(path string, in []struct{ Name, Data string }) { var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC) +// TODO(≥go1.18): Define a generic function that boxes a value on the heap. func newInt(n int) *int { return &n } type Stringer string @@ -885,6 +886,7 @@ func reporterTests() []test { FloatsB []MyFloat FloatsC MyFloats } + PointerString *string ) return []test{{ @@ -1351,6 +1353,38 @@ using the AllowUnexported option.`, "\n"), "bar": true, }`, reason: "short multiline JSON should prefer triple-quoted string diff as it is more readable", + }, { + label: label + "/PointerToStringOrAny", + x: func() *string { + var v string = "hello" + return &v + }(), + y: func() *interface{} { + var v interface{} = "hello" + return &v + }(), + reason: "mismatched types between any and *any should print differently", + }, { + label: label + "/NamedPointer", + x: func() *string { + v := "hello" + return &v + }(), + y: func() PointerString { + v := "hello" + return &v + }(), + reason: "mismatched pointer types should print differently", + }, { + label: label + "/MapStringAny", + x: map[string]interface{}{"key": int(0)}, + y: map[string]interface{}{"key": uint(0)}, + reason: "mismatched underlying value within interface", + }, { + label: label + "/StructFieldAny", + x: struct{ X interface{} }{int(0)}, + y: struct{ X interface{} }{uint(0)}, + reason: "mismatched underlying value within interface", }} } diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 76c04fd..d9d7323 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -282,7 +282,12 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } defer ptrs.Pop() - skipType = true // Let the underlying value print the type instead + // Skip the name only if this is an unnamed pointer type. + // Otherwise taking the address of a value does not reproduce + // the named pointer type. + if v.Type().Name() == "" { + skipType = true // Let the underlying value print the type instead + } out = opts.FormatValue(v.Elem(), t.Kind(), ptrs) out = wrapTrunkReference(ptrRef, opts.PrintAddresses, out) out = &textWrap{Prefix: "&", Value: out} @@ -293,7 +298,6 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } // 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(), t.Kind(), ptrs) default: panic(fmt.Sprintf("%v kind not handled", v.Kind())) diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index d207803..23781f7 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1134,6 +1134,30 @@ """ ) >>> TestDiff/Reporter/ShortJSON +<<< TestDiff/Reporter/PointerToStringOrAny + any( +- &string("hello"), ++ &any(string("hello")), + ) +>>> TestDiff/Reporter/PointerToStringOrAny +<<< TestDiff/Reporter/NamedPointer + any( +- &string("hello"), ++ cmp_test.PointerString(&string("hello")), + ) +>>> TestDiff/Reporter/NamedPointer +<<< TestDiff/Reporter/MapStringAny + map[string]any{ +- "key": int(0), ++ "key": uint(0), + } +>>> TestDiff/Reporter/MapStringAny +<<< TestDiff/Reporter/StructFieldAny + struct{ X any }{ +- X: int(0), ++ X: uint(0), + } +>>> TestDiff/Reporter/StructFieldAny <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 71220fc3ca5513eaf3583d10fd18da893bc332d8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 25 Apr 2022 09:32:19 -0700 Subject: [PATCH 90/99] Use string formatting for slice of bytes (#294) If a slice of bytes is mostly text, format them as text instead of as []byte literal with hexadecimal digits. Avoid always printing the type. This is technically invalid Go code, but is unnecessary in many cases since the type is inferred from the parent concrete type. Fixes #272 --- cmp/cmpopts/example_test.go | 2 +- cmp/compare_test.go | 18 ++++++++++++++++++ cmp/example_test.go | 2 +- cmp/report_compare.go | 5 ++++- cmp/report_reflect.go | 2 +- cmp/testdata/diffs | 30 +++++++++++++++++++++++++++--- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/cmp/cmpopts/example_test.go b/cmp/cmpopts/example_test.go index 0cf2513..4b9a8ab 100644 --- a/cmp/cmpopts/example_test.go +++ b/cmp/cmpopts/example_test.go @@ -39,7 +39,7 @@ func ExampleIgnoreFields_testing() { // SSID: "CoffeeShopWiFi", // - IPAddress: s"192.168.0.2", // + IPAddress: s"192.168.0.1", - // NetMask: {0xff, 0xff, 0x00, 0x00}, + // NetMask: s"ffff0000", // Clients: []cmpopts_test.Client{ // ... // 3 identical elements // {Hostname: "espresso", ...}, diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 7278485..aafe441 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1385,6 +1385,24 @@ using the AllowUnexported option.`, "\n"), x: struct{ X interface{} }{int(0)}, y: struct{ X interface{} }{uint(0)}, reason: "mismatched underlying value within interface", + }, { + label: label + "/SliceOfBytesText", + x: [][]byte{ + []byte("hello"), []byte("foo"), []byte("barbaz"), []byte("blahdieblah"), + }, + y: [][]byte{ + []byte("foo"), []byte("foo"), []byte("barbaz"), []byte("added"), []byte("here"), []byte("hrmph"), + }, + reason: "should print text byte slices as strings", + }, { + label: label + "/SliceOfBytesBinary", + x: [][]byte{ + []byte("\xde\xad\xbe\xef"), []byte("\xffoo"), []byte("barbaz"), []byte("blahdieblah"), + }, + y: [][]byte{ + []byte("\xffoo"), []byte("foo"), []byte("barbaz"), []byte("added"), []byte("here"), []byte("hrmph\xff"), + }, + reason: "should print text byte slices as strings except those with binary", }} } diff --git a/cmp/example_test.go b/cmp/example_test.go index e1f4338..9968149 100644 --- a/cmp/example_test.go +++ b/cmp/example_test.go @@ -37,7 +37,7 @@ func ExampleDiff_testing() { // SSID: "CoffeeShopWiFi", // - IPAddress: s"192.168.0.2", // + IPAddress: s"192.168.0.1", - // NetMask: {0xff, 0xff, 0x00, 0x00}, + // NetMask: s"ffff0000", // Clients: []cmp_test.Client{ // ... // 2 identical elements // {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"}, diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 104bb30..1ef65ac 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -116,7 +116,10 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out } // For leaf nodes, format the value based on the reflect.Values alone. - if v.MaxDepth == 0 { + // As a special case, treat equal []byte as a leaf nodes. + isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == reflect.TypeOf(byte(0)) + isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0 + if v.MaxDepth == 0 || isEqualBytes { switch opts.DiffMode { case diffUnknown, diffIdentical: // Format Equal. diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index d9d7323..287b893 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -211,7 +211,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { out = opts.formatString("", string(b)) skipType = true - return opts.WithTypeMode(emitType).FormatType(t, out) + return opts.FormatType(t, out) } } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 23781f7..8bff76f 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -241,9 +241,9 @@ "string", }), Bytes: []uint8(Inverse(SplitBytes, [][]uint8{ - {0x73, 0x6f, 0x6d, 0x65}, - {0x6d, 0x75, 0x6c, 0x74, ...}, - {0x6c, 0x69, 0x6e, 0x65}, + "some", + "multi", + "line", { - 0x62, + 0x42, @@ -1158,6 +1158,30 @@ + X: uint(0), } >>> TestDiff/Reporter/StructFieldAny +<<< TestDiff/Reporter/SliceOfBytesText + [][]uint8{ +- "hello", + "foo", ++ "foo", + "barbaz", ++ "added", ++ "here", +- "blahdieblah", ++ "hrmph", + } +>>> TestDiff/Reporter/SliceOfBytesText +<<< TestDiff/Reporter/SliceOfBytesBinary + [][]uint8{ +- {0xde, 0xad, 0xbe, 0xef}, + {0xff, 0x6f, 0x6f}, ++ "foo", + "barbaz", ++ "added", ++ "here", +- "blahdieblah", ++ {0x68, 0x72, 0x6d, 0x70, 0x68, 0xff}, + } +>>> TestDiff/Reporter/SliceOfBytesBinary <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{ From 63c2960be651bb95aaf14535bd5e36e86b6b5458 Mon Sep 17 00:00:00 2001 From: Tatsuya Kaneko Date: Wed, 27 Apr 2022 04:24:26 +0900 Subject: [PATCH 91/99] remove xerrors (#292) Versions older than Go 1.13 are no longer in use. Remove unnecessary dependencies. --- .github/workflows/test.yml | 2 +- cmp/cmpopts/errors_xerrors.go | 19 ------------------- cmp/cmpopts/util_test.go | 13 ++++++------- go.mod | 2 -- go.sum | 2 -- 5 files changed, 7 insertions(+), 31 deletions(-) delete mode 100644 cmp/cmpopts/errors_xerrors.go delete mode 100644 go.sum diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a50673..a93f058 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x] + go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: diff --git a/cmp/cmpopts/errors_xerrors.go b/cmp/cmpopts/errors_xerrors.go deleted file mode 100644 index 60b0727..0000000 --- a/cmp/cmpopts/errors_xerrors.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2021, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.13 -// +build !go1.13 - -// TODO(≥go1.13): For support on Date: Tue, 26 Apr 2022 13:49:16 -0700 Subject: [PATCH 92/99] Additional cleanup with Go 1.13 as minimal version (#295) --- cmp/cmpopts/equate.go | 7 +++++++ cmp/cmpopts/errors_go113.go | 16 ---------------- go.mod | 2 +- 3 files changed, 8 insertions(+), 17 deletions(-) delete mode 100644 cmp/cmpopts/errors_go113.go diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 62837c9..c49a756 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -6,6 +6,7 @@ package cmpopts import ( + "errors" "math" "reflect" "time" @@ -146,3 +147,9 @@ func areConcreteErrors(x, y interface{}) bool { _, ok2 := y.(error) return ok1 && ok2 } + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + return errors.Is(xe, ye) || errors.Is(ye, xe) +} diff --git a/cmp/cmpopts/errors_go113.go b/cmp/cmpopts/errors_go113.go deleted file mode 100644 index 8eb2b84..0000000 --- a/cmp/cmpopts/errors_go113.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.13 -// +build go1.13 - -package cmpopts - -import "errors" - -func compareErrors(x, y interface{}) bool { - xe := x.(error) - ye := y.(error) - return errors.Is(xe, ye) || errors.Is(ye, xe) -} diff --git a/go.mod b/go.mod index b570017..f55cea6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/google/go-cmp -go 1.11 +go 1.13 From a53d7e09b000ee6e0ca9f2676820299b5de8e89f Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 6 Jun 2022 10:31:27 -0700 Subject: [PATCH 93/99] Use reflect.Value.IsZero (#297) Now that Go 1.13 is the minimum version, we can use the reflect.Value.IsZero method instead of our own internal/value.IsZero function. Interestingly, our IsZero function pre-dates the IsZero method, but fortunately has the exact same semantics, since both are targetting semantics defined by the Go language specification. --- cmp/internal/value/zero.go | 48 ------------------------------ cmp/internal/value/zero_test.go | 52 --------------------------------- cmp/report_compare.go | 8 ++--- cmp/report_reflect.go | 2 +- 4 files changed, 4 insertions(+), 106 deletions(-) delete mode 100644 cmp/internal/value/zero.go delete mode 100644 cmp/internal/value/zero_test.go diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go deleted file mode 100644 index 9147a29..0000000 --- a/cmp/internal/value/zero.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2017, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE 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 deleted file mode 100644 index ddaa337..0000000 --- a/cmp/internal/value/zero_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2019, The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package 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/report_compare.go b/cmp/report_compare.go index 1ef65ac..498c57a 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -7,8 +7,6 @@ package cmp import ( "fmt" "reflect" - - "github.com/google/go-cmp/cmp/internal/value" ) // numContextRecords is the number of surrounding equal records to print. @@ -248,11 +246,11 @@ func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, pt var isZero bool switch opts.DiffMode { case diffIdentical: - isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY) + isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero() case diffRemoved: - isZero = value.IsZero(r.Value.ValueX) + isZero = r.Value.ValueX.IsZero() case diffInserted: - isZero = value.IsZero(r.Value.ValueY) + isZero = r.Value.ValueY.IsZero() } if isZero { continue diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 287b893..381fd31 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -184,7 +184,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } for i := 0; i < v.NumField(); i++ { vv := v.Field(i) - if value.IsZero(vv) { + if vv.IsZero() { continue // Elide fields with zero values } if len(list) == maxLen { From 14ad8a02f30ba66e7e19f9814e69daab44219cb8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 13 Jul 2022 11:10:07 -0700 Subject: [PATCH 94/99] Format with Go 1.19 formatter (#304) This allows the GoDoc to take advantage of new markup syntax introduced in Go 1.19. This does not require that our minimum supported version be bumped to Go 1.19 since the pkgsite renders our godoc regardless of supported Go version. --- cmp/cmpopts/equate.go | 1 + cmp/cmpopts/sort.go | 14 ++++---- cmp/cmpopts/struct_filter.go | 2 ++ cmp/cmpopts/xform.go | 1 + cmp/compare.go | 62 +++++++++++++++++++----------------- cmp/compare_test.go | 2 +- cmp/internal/diff/diff.go | 44 +++++++++++++------------ cmp/options.go | 8 +++-- cmp/path.go | 16 ++++++---- cmp/report_slices.go | 12 +++---- cmp/report_text.go | 1 + 11 files changed, 88 insertions(+), 75 deletions(-) diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index c49a756..e54a76c 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -42,6 +42,7 @@ func isEmpty(x, y interface{}) bool { // The fraction and margin must be non-negative. // // The mathematical expression used is equivalent to: +// // |x-y| ≤ max(fraction*min(|x|, |y|), margin) // // EquateApprox can be used in conjunction with EquateNaNs. diff --git a/cmp/cmpopts/sort.go b/cmp/cmpopts/sort.go index a646d74..0eb2a75 100644 --- a/cmp/cmpopts/sort.go +++ b/cmp/cmpopts/sort.go @@ -18,9 +18,9 @@ import ( // sort any slice with element type V that is assignable to T. // // The less function must be: -// • Deterministic: less(x, y) == less(x, y) -// • Irreflexive: !less(x, x) -// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) // // The less function does not have to be "total". That is, if !less(x, y) and // !less(y, x) for two elements x and y, their relative order is maintained. @@ -91,10 +91,10 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool { // use Comparers on K or the K.Equal method if it exists. // // The less function must be: -// • Deterministic: less(x, y) == less(x, y) -// • Irreflexive: !less(x, x) -// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) -// • Total: if x != y, then either less(x, y) or less(y, x) +// - Deterministic: less(x, y) == less(x, y) +// - Irreflexive: !less(x, x) +// - Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// - Total: if x != y, then either less(x, y) or less(y, x) // // SortMaps can be used in conjunction with EquateEmpty. func SortMaps(lessFunc interface{}) cmp.Option { diff --git a/cmp/cmpopts/struct_filter.go b/cmp/cmpopts/struct_filter.go index a09829c..ca11a40 100644 --- a/cmp/cmpopts/struct_filter.go +++ b/cmp/cmpopts/struct_filter.go @@ -67,12 +67,14 @@ func (sf structFilter) filter(p cmp.Path) bool { // fieldTree represents a set of dot-separated identifiers. // // For example, inserting the following selectors: +// // Foo // Foo.Bar.Baz // Foo.Buzz // Nuka.Cola.Quantum // // Results in a tree of the form: +// // {sub: { // "Foo": {ok: true, sub: { // "Bar": {sub: { diff --git a/cmp/cmpopts/xform.go b/cmp/cmpopts/xform.go index 4eb49d6..8812443 100644 --- a/cmp/cmpopts/xform.go +++ b/cmp/cmpopts/xform.go @@ -23,6 +23,7 @@ func (xf xformFilter) filter(p cmp.Path) bool { // that the transformer cannot be recursively applied upon its own output. // // An example use case is a transformer that splits a string by lines: +// // AcyclicTransformer("SplitLines", func(s string) []string{ // return strings.Split(s, "\n") // }) diff --git a/cmp/compare.go b/cmp/compare.go index fd2b3a4..caf75ad 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -13,21 +13,21 @@ // // The primary features of cmp are: // -// • When the default behavior of equality does not suit the needs of the test, -// custom equality functions can override the equality operation. -// For example, an equality function may report floats as equal so long as they -// are within some tolerance of each other. +// - When the default behavior of equality does not suit the test's needs, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as +// they are within some tolerance of each other. // -// • Types that have an Equal method may use that method to determine equality. -// This allows package authors to determine the equality operation for the types -// that they define. +// - Types with an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation +// for the types that they define. // -// • If no custom equality functions are used and no Equal method is defined, -// equality is determined by recursively comparing the primitive kinds on both -// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported -// fields are not compared by default; they result in panics unless suppressed -// by using an Ignore option (see cmpopts.IgnoreUnexported) or explicitly -// compared using the Exporter option. +// - If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on +// both values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, +// unexported fields are not compared by default; they result in panics +// unless suppressed by using an Ignore option (see cmpopts.IgnoreUnexported) +// or explicitly compared using the Exporter option. package cmp import ( @@ -45,25 +45,25 @@ import ( // 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. +// - Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is non-zero, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform +// the current values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. // -// • If the values have an Equal method of the form "(T) Equal(T) bool" or -// "(T) Equal(I) bool" where T is assignable to I, then use the result of -// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and -// evaluation proceeds to the next rule. +// - If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y) even if x or y is nil. Otherwise, no such method exists and +// evaluation proceeds to the next rule. // -// • Lastly, try to compare x and y based on their basic kinds. -// Simple kinds like booleans, integers, floats, complex numbers, strings, and -// channels are compared using the equivalent of the == operator in Go. -// Functions are only equal if they are both nil, otherwise they are unequal. +// - Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, +// and channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. // // Structs are equal if recursively calling Equal on all fields report equal. // If a struct contains unexported fields, Equal panics unless an Ignore option @@ -639,7 +639,9 @@ type dynChecker struct{ curr, next int } // Next increments the state and reports whether a check should be performed. // // Checks occur every Nth function call, where N is a triangular number: +// // 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// // See https://en.wikipedia.org/wiki/Triangular_number // // This sequence ensures that the cost of checks drops significantly as diff --git a/cmp/compare_test.go b/cmp/compare_test.go index aafe441..dc86f01 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -43,7 +43,7 @@ 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. +// mustParseGolden parses a file as a set of key-value pairs. // // The syntax is simple and looks something like: // diff --git a/cmp/internal/diff/diff.go b/cmp/internal/diff/diff.go index bc196b1..a248e54 100644 --- a/cmp/internal/diff/diff.go +++ b/cmp/internal/diff/diff.go @@ -127,9 +127,9 @@ var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 // This function returns an edit-script, which is a sequence of operations // needed to convert one list into the other. The following invariants for // the edit-script are maintained: -// • eq == (es.Dist()==0) -// • nx == es.LenX() -// • ny == es.LenY() +// - eq == (es.Dist()==0) +// - nx == es.LenX() +// - ny == es.LenY() // // This algorithm is not guaranteed to be an optimal solution (i.e., one that // produces an edit-script with a minimal Levenshtein distance). This algorithm @@ -169,12 +169,13 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // A diagonal edge is equivalent to a matching symbol between both X and Y. // Invariants: - // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx - // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // - 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // - 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny // // In general: - // • fwdFrontier.X < revFrontier.X - // • fwdFrontier.Y < revFrontier.Y + // - fwdFrontier.X < revFrontier.X + // - fwdFrontier.Y < revFrontier.Y + // // Unless, it is time for the algorithm to terminate. fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} revPath := path{-1, point{nx, ny}, make(EditScript, 0)} @@ -195,19 +196,21 @@ func Difference(nx, ny int, f EqualFunc) (es EditScript) { // computing sub-optimal edit-scripts between two lists. // // The algorithm is approximately as follows: - // • Searching for differences switches back-and-forth between - // a search that starts at the beginning (the top-left corner), and - // a search that starts at the end (the bottom-right corner). The goal of - // the search is connect with the search from the opposite corner. - // • As we search, we build a path in a greedy manner, where the first - // match seen is added to the path (this is sub-optimal, but provides a - // decent result in practice). When matches are found, we try the next pair - // of symbols in the lists and follow all matches as far as possible. - // • When searching for matches, we search along a diagonal going through - // through the "frontier" point. If no matches are found, we advance the - // frontier towards the opposite corner. - // • This algorithm terminates when either the X coordinates or the - // Y coordinates of the forward and reverse frontier points ever intersect. + // - Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). + // The goal of the search is connect with the search + // from the opposite corner. + // - As we search, we build a path in a greedy manner, + // where the first match seen is added to the path (this is sub-optimal, + // but provides a decent result in practice). When matches are found, + // we try the next pair of symbols in the lists and follow all matches + // as far as possible. + // - When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, + // we advance the frontier towards the opposite corner. + // - This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. // This algorithm is correct even if searching only in the forward direction // or in the reverse direction. We do both because it is commonly observed @@ -389,6 +392,7 @@ type point struct{ X, Y int } func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } // zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// // [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] func zigzag(x int) int { if x&1 != 0 { diff --git a/cmp/options.go b/cmp/options.go index e57b9eb..e254ffd 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -33,6 +33,7 @@ type Option interface { } // applicableOption represents the following types: +// // Fundamental: ignore | validator | *comparer | *transformer // Grouping: Options type applicableOption interface { @@ -43,6 +44,7 @@ type applicableOption interface { } // coreOption represents the following types: +// // Fundamental: ignore | validator | *comparer | *transformer // Filters: *pathFilter | *valuesFilter type coreOption interface { @@ -336,9 +338,9 @@ func (tr transformer) String() string { // both implement T. // // The equality function must be: -// • Symmetric: equal(x, y) == equal(y, x) -// • Deterministic: equal(x, y) == equal(x, y) -// • Pure: equal(x, y) does not modify x or y +// - Symmetric: equal(x, y) == equal(y, x) +// - Deterministic: equal(x, y) == equal(x, y) +// - Pure: equal(x, y) does not modify x or y func Comparer(f interface{}) Option { v := reflect.ValueOf(f) if !function.IsType(v.Type(), function.Equal) || v.IsNil() { diff --git a/cmp/path.go b/cmp/path.go index c710034..557c7f2 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -41,13 +41,13 @@ type PathStep interface { // 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. + // - 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) @@ -94,6 +94,7 @@ func (pa Path) Index(i int) PathStep { // The simplified path only contains struct field accesses. // // For example: +// // MyMap.MySlices.MyField func (pa Path) String() string { var ss []string @@ -108,6 +109,7 @@ func (pa Path) String() string { // GoString returns the path to a specific node using Go syntax. // // For example: +// // (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField func (pa Path) GoString() string { var ssPre, ssPost []string diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 68b5c1a..21aecd9 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -171,12 +171,13 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { // differences in a string literal. This format is more readable, // but has edge-cases where differences are visually indistinguishable. // This format is avoided under the following conditions: - // • A line starts with `"""` - // • A line starts with "..." - // • A line contains non-printable characters - // • Adjacent different lines differ only by whitespace + // - A line starts with `"""` + // - A line starts with "..." + // - A line contains non-printable characters + // - Adjacent different lines differ only by whitespace // // For example: + // // """ // ... // 3 identical lines // foo @@ -446,7 +447,6 @@ func (opts formatOptions) formatDiffSlice( // {NumIdentical: 3}, // {NumInserted: 1}, // ] -// func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) { var prevMode byte lastStats := func(mode byte) *diffStats { @@ -503,7 +503,6 @@ func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) // {NumIdentical: 8, NumRemoved: 12, NumInserted: 3}, // {NumIdentical: 63}, // ] -// func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats { groups, groupsOrig := groups[:0], groups for i, ds := range groupsOrig { @@ -548,7 +547,6 @@ func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStat // {NumRemoved: 9}, // {NumIdentical: 64}, // incremented by 10 // ] -// func cleanupSurroundingIdentical(groups []diffStats, eq func(i, j int) bool) []diffStats { var ix, iy int // indexes into sequence x and y for i, ds := range groups { diff --git a/cmp/report_text.go b/cmp/report_text.go index 0fd46d7..388fcf5 100644 --- a/cmp/report_text.go +++ b/cmp/report_text.go @@ -393,6 +393,7 @@ func (s diffStats) Append(ds diffStats) diffStats { // 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 From 5dac6aa44b75666a956f67df1b5bd4e2e044e1f8 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 30 Aug 2022 15:55:16 -0700 Subject: [PATCH 95/99] Fix typo in Result documentation (#300) --- cmp/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/options.go b/cmp/options.go index e254ffd..1f9ca9c 100644 --- a/cmp/options.go +++ b/cmp/options.go @@ -432,7 +432,7 @@ func AllowUnexported(types ...interface{}) Option { } // Result represents the comparison result for a single node and -// is provided by cmp when calling Result (see Reporter). +// is provided by cmp when calling Report (see Reporter). type Result struct { _ [0]func() // Make Result incomparable flags resultFlags From f36a68d19a9bca43e070954ab9170a8305662d15 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 30 Aug 2022 16:06:02 -0700 Subject: [PATCH 96/99] Pre-declare global type variables (#302) Co-authored-by: Damien Neil --- cmp/compare.go | 2 +- cmp/report_compare.go | 2 +- cmp/report_reflect.go | 9 ++++++++- cmp/report_slices.go | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cmp/compare.go b/cmp/compare.go index caf75ad..087320d 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -144,7 +144,7 @@ func rootStep(x, y interface{}) PathStep { // 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() + t = anyType if vx.IsValid() { vvx := reflect.New(t).Elem() vvx.Set(vx) diff --git a/cmp/report_compare.go b/cmp/report_compare.go index 498c57a..2050bf6 100644 --- a/cmp/report_compare.go +++ b/cmp/report_compare.go @@ -115,7 +115,7 @@ func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out // For leaf nodes, format the value based on the reflect.Values alone. // As a special case, treat equal []byte as a leaf nodes. - isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == reflect.TypeOf(byte(0)) + isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0 if v.MaxDepth == 0 || isEqualBytes { switch opts.DiffMode { diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go index 381fd31..2ab41fa 100644 --- a/cmp/report_reflect.go +++ b/cmp/report_reflect.go @@ -16,6 +16,13 @@ import ( "github.com/google/go-cmp/cmp/internal/value" ) +var ( + anyType = reflect.TypeOf((*interface{})(nil)).Elem() + stringType = reflect.TypeOf((*string)(nil)).Elem() + bytesType = reflect.TypeOf((*[]byte)(nil)).Elem() + byteType = reflect.TypeOf((*byte)(nil)).Elem() +) + type formatValueOptions struct { // AvoidStringer controls whether to avoid calling custom stringer // methods like error.Error or fmt.Stringer.String. @@ -205,7 +212,7 @@ func (opts formatOptions) FormatValue(v reflect.Value, parentKind reflect.Kind, } // Check whether this is a []byte of text data. - if t.Elem() == reflect.TypeOf(byte(0)) { + if t.Elem() == byteType { b := v.Bytes() isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) || unicode.IsSpace(r) } if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 { diff --git a/cmp/report_slices.go b/cmp/report_slices.go index 21aecd9..b38ed68 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -104,7 +104,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { case t.Kind() == reflect.String: sx, sy = vx.String(), vy.String() isString = true - case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)): + case t.Kind() == reflect.Slice && t.Elem() == byteType: sx, sy = string(vx.Bytes()), string(vy.Bytes()) isString = true case t.Kind() == reflect.Array: @@ -232,7 +232,7 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { var out textNode = &textWrap{Prefix: "(", Value: list2, Suffix: ")"} switch t.Kind() { case reflect.String: - if t != reflect.TypeOf(string("")) { + if t != stringType { out = opts.FormatType(t, out) } case reflect.Slice: @@ -327,12 +327,12 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { switch t.Kind() { case reflect.String: out = &textWrap{Prefix: "strings.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} - if t != reflect.TypeOf(string("")) { + if t != stringType { out = opts.FormatType(t, out) } case reflect.Slice: out = &textWrap{Prefix: "bytes.Join(", Value: out, Suffix: fmt.Sprintf(", %q)", delim)} - if t != reflect.TypeOf([]byte(nil)) { + if t != bytesType { out = opts.FormatType(t, out) } } From 6606d4d51e3239f038565f525940ac6043aff53e Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Tue, 30 Aug 2022 16:39:27 -0700 Subject: [PATCH 97/99] Use value.TypeString in PathStep.String (#306) The value.TypeString function is what the rest of the package uses and is slightly cleaner than using reflect.Type.String. Updates #305 Co-authored-by: Damien Neil --- cmp/path.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmp/path.go b/cmp/path.go index 557c7f2..a0a5885 100644 --- a/cmp/path.go +++ b/cmp/path.go @@ -161,7 +161,7 @@ func (ps pathStep) String() string { if ps.typ == nil { return "" } - s := ps.typ.String() + s := value.TypeString(ps.typ, false) if s == "" || strings.ContainsAny(s, "{}\n") { return "root" // Type too simple or complex to print } @@ -284,7 +284,7 @@ type typeAssertion struct { 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) } +func (ta TypeAssertion) String() string { return fmt.Sprintf(".(%v)", value.TypeString(ta.typ, false)) } // Transform is a transformation from the parent type to the current type. type Transform struct{ *transform } From 377d28384c85781079e04aab3937170479da8cd6 Mon Sep 17 00:00:00 2001 From: Aoang Date: Wed, 31 Aug 2022 09:25:27 +0800 Subject: [PATCH 98/99] Run tests on Go 1.19 (#309) * Run tests on Go 1.19 * Format comment Finish the rest of the work for https://github.com/google/go-cmp/pull/304 Co-authored-by: Damien Neil --- .github/workflows/test.yml | 4 ++-- cmp/cmpopts/util_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a93f058..b49573d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x] + go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: @@ -17,5 +17,5 @@ jobs: - name: Test run: go test -v -race ./... - name: Format - if: matrix.go-version == '1.18.x' + if: matrix.go-version == '1.19.x' run: diff -u <(echo -n) <(gofmt -d .) diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index c2eaf0b..7adeb9b 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -1072,7 +1072,7 @@ func TestOptions(t *testing.T) { }, { label: "AcyclicTransformer", x: "this is a sentence", - y: "this is a sentence", + y: "this is a sentence", opts: []cmp.Option{ AcyclicTransformer("", strings.Fields), }, From a97318bf6562f2ed2632c5f985db51b1bc5bdcd0 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Fri, 2 Sep 2022 15:41:56 -0700 Subject: [PATCH 99/99] Adjust heuristic for line-based versus byte-based diffing (#299) If the string has many characters that require escape sequences to print, then we need to take that into consideration and avoid byte-by-byte diffing. Co-authored-by: Damien Neil --- cmp/compare_test.go | 17 +++++++++++++++++ cmp/report_slices.go | 5 ++++- cmp/testdata/diffs | 12 ++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index dc86f01..88b7d45 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1403,6 +1403,23 @@ using the AllowUnexported option.`, "\n"), []byte("\xffoo"), []byte("foo"), []byte("barbaz"), []byte("added"), []byte("here"), []byte("hrmph\xff"), }, reason: "should print text byte slices as strings except those with binary", + }, { + label: label + "/ManyEscapeCharacters", + x: `[ + {"Base32": "NA======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + {"Base32": "NBSWY3A="}, + {"Base32": "NBSWY3DP"} +]`, + y: `[ + {"Base32": "NB======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + {"Base32": "NBSWY3A="}, + {"Base32": "NBSWY3DP"} +]`, + reason: "should use line-based diffing since byte-based diffing is unreadable due to heavy amounts of escaping", }} } diff --git a/cmp/report_slices.go b/cmp/report_slices.go index b38ed68..23e444f 100644 --- a/cmp/report_slices.go +++ b/cmp/report_slices.go @@ -147,7 +147,10 @@ func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode { }) efficiencyLines := float64(esLines.Dist()) / float64(len(esLines)) efficiencyBytes := float64(esBytes.Dist()) / float64(len(esBytes)) - isPureLinedText = efficiencyLines < 4*efficiencyBytes + quotedLength := len(strconv.Quote(sx + sy)) + unquotedLength := len(sx) + len(sy) + escapeExpansionRatio := float64(quotedLength) / float64(unquotedLength) + isPureLinedText = efficiencyLines < 4*efficiencyBytes || escapeExpansionRatio > 1.1 } } diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs index 8bff76f..be77b95 100644 --- a/cmp/testdata/diffs +++ b/cmp/testdata/diffs @@ -1182,6 +1182,18 @@ + {0x68, 0x72, 0x6d, 0x70, 0x68, 0xff}, } >>> TestDiff/Reporter/SliceOfBytesBinary +<<< TestDiff/Reporter/ManyEscapeCharacters + ( + """ + [ +- {"Base32": "NA======"}, ++ {"Base32": "NB======"}, + {"Base32": "NBSQ===="}, + {"Base32": "NBSWY==="}, + ... // 3 identical lines + """ + ) +>>> TestDiff/Reporter/ManyEscapeCharacters <<< TestDiff/EmbeddedStruct/ParentStructA/Inequal teststructs.ParentStructA{ privateStruct: teststructs.privateStruct{