From 77ae86f624cb174e21763cffcbbf070eb06cb016 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Wed, 17 Jun 2020 17:25:47 -0700 Subject: [PATCH] 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