aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2020-06-17 17:25:47 -0700
committerGitHub <noreply@github.com>2020-06-17 17:25:47 -0700
commit77ae86f624cb174e21763cffcbbf070eb06cb016 (patch)
treedd32bd59eb58b50d44dee2efa06ac36f8daa92a1
parentc49bfce0ac9115b09320b47c3b9534cc5afd4579 (diff)
downloadgo-cmp-77ae86f624cb174e21763cffcbbf070eb06cb016.tar.gz
Improve reporting of values with cycles (#217)
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⟫{...}.
-rw-r--r--cmp/internal/value/pointer_purego.go10
-rw-r--r--cmp/internal/value/pointer_unsafe.go10
-rw-r--r--cmp/report.go5
-rw-r--r--cmp/report_compare.go85
-rw-r--r--cmp/report_references.go264
-rw-r--r--cmp/report_reflect.go156
-rw-r--r--cmp/report_slices.go10
-rw-r--r--cmp/report_text.go21
-rw-r--r--cmp/testdata/diffs129
9 files changed, 490 insertions, 200 deletions
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