xref: /aosp_15_r20/external/go-cmp/cmp/report_compare.go (revision 88d15eac089d7f20c739ff1001d56b91872b21a1)
1*88d15eacSSasha Smundak// Copyright 2019, The Go Authors. All rights reserved.
2*88d15eacSSasha Smundak// Use of this source code is governed by a BSD-style
3*88d15eacSSasha Smundak// license that can be found in the LICENSE file.
4*88d15eacSSasha Smundak
5*88d15eacSSasha Smundakpackage cmp
6*88d15eacSSasha Smundak
7*88d15eacSSasha Smundakimport (
8*88d15eacSSasha Smundak	"fmt"
9*88d15eacSSasha Smundak	"reflect"
10*88d15eacSSasha Smundak)
11*88d15eacSSasha Smundak
12*88d15eacSSasha Smundak// numContextRecords is the number of surrounding equal records to print.
13*88d15eacSSasha Smundakconst numContextRecords = 2
14*88d15eacSSasha Smundak
15*88d15eacSSasha Smundaktype diffMode byte
16*88d15eacSSasha Smundak
17*88d15eacSSasha Smundakconst (
18*88d15eacSSasha Smundak	diffUnknown   diffMode = 0
19*88d15eacSSasha Smundak	diffIdentical diffMode = ' '
20*88d15eacSSasha Smundak	diffRemoved   diffMode = '-'
21*88d15eacSSasha Smundak	diffInserted  diffMode = '+'
22*88d15eacSSasha Smundak)
23*88d15eacSSasha Smundak
24*88d15eacSSasha Smundaktype typeMode int
25*88d15eacSSasha Smundak
26*88d15eacSSasha Smundakconst (
27*88d15eacSSasha Smundak	// emitType always prints the type.
28*88d15eacSSasha Smundak	emitType typeMode = iota
29*88d15eacSSasha Smundak	// elideType never prints the type.
30*88d15eacSSasha Smundak	elideType
31*88d15eacSSasha Smundak	// autoType prints the type only for composite kinds
32*88d15eacSSasha Smundak	// (i.e., structs, slices, arrays, and maps).
33*88d15eacSSasha Smundak	autoType
34*88d15eacSSasha Smundak)
35*88d15eacSSasha Smundak
36*88d15eacSSasha Smundaktype formatOptions struct {
37*88d15eacSSasha Smundak	// DiffMode controls the output mode of FormatDiff.
38*88d15eacSSasha Smundak	//
39*88d15eacSSasha Smundak	// If diffUnknown,   then produce a diff of the x and y values.
40*88d15eacSSasha Smundak	// If diffIdentical, then emit values as if they were equal.
41*88d15eacSSasha Smundak	// If diffRemoved,   then only emit x values (ignoring y values).
42*88d15eacSSasha Smundak	// If diffInserted,  then only emit y values (ignoring x values).
43*88d15eacSSasha Smundak	DiffMode diffMode
44*88d15eacSSasha Smundak
45*88d15eacSSasha Smundak	// TypeMode controls whether to print the type for the current node.
46*88d15eacSSasha Smundak	//
47*88d15eacSSasha Smundak	// As a general rule of thumb, we always print the type of the next node
48*88d15eacSSasha Smundak	// after an interface, and always elide the type of the next node after
49*88d15eacSSasha Smundak	// a slice or map node.
50*88d15eacSSasha Smundak	TypeMode typeMode
51*88d15eacSSasha Smundak
52*88d15eacSSasha Smundak	// formatValueOptions are options specific to printing reflect.Values.
53*88d15eacSSasha Smundak	formatValueOptions
54*88d15eacSSasha Smundak}
55*88d15eacSSasha Smundak
56*88d15eacSSasha Smundakfunc (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
57*88d15eacSSasha Smundak	opts.DiffMode = d
58*88d15eacSSasha Smundak	return opts
59*88d15eacSSasha Smundak}
60*88d15eacSSasha Smundakfunc (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
61*88d15eacSSasha Smundak	opts.TypeMode = t
62*88d15eacSSasha Smundak	return opts
63*88d15eacSSasha Smundak}
64*88d15eacSSasha Smundakfunc (opts formatOptions) WithVerbosity(level int) formatOptions {
65*88d15eacSSasha Smundak	opts.VerbosityLevel = level
66*88d15eacSSasha Smundak	opts.LimitVerbosity = true
67*88d15eacSSasha Smundak	return opts
68*88d15eacSSasha Smundak}
69*88d15eacSSasha Smundakfunc (opts formatOptions) verbosity() uint {
70*88d15eacSSasha Smundak	switch {
71*88d15eacSSasha Smundak	case opts.VerbosityLevel < 0:
72*88d15eacSSasha Smundak		return 0
73*88d15eacSSasha Smundak	case opts.VerbosityLevel > 16:
74*88d15eacSSasha Smundak		return 16 // some reasonable maximum to avoid shift overflow
75*88d15eacSSasha Smundak	default:
76*88d15eacSSasha Smundak		return uint(opts.VerbosityLevel)
77*88d15eacSSasha Smundak	}
78*88d15eacSSasha Smundak}
79*88d15eacSSasha Smundak
80*88d15eacSSasha Smundakconst maxVerbosityPreset = 6
81*88d15eacSSasha Smundak
82*88d15eacSSasha Smundak// verbosityPreset modifies the verbosity settings given an index
83*88d15eacSSasha Smundak// between 0 and maxVerbosityPreset, inclusive.
84*88d15eacSSasha Smundakfunc verbosityPreset(opts formatOptions, i int) formatOptions {
85*88d15eacSSasha Smundak	opts.VerbosityLevel = int(opts.verbosity()) + 2*i
86*88d15eacSSasha Smundak	if i > 0 {
87*88d15eacSSasha Smundak		opts.AvoidStringer = true
88*88d15eacSSasha Smundak	}
89*88d15eacSSasha Smundak	if i >= maxVerbosityPreset {
90*88d15eacSSasha Smundak		opts.PrintAddresses = true
91*88d15eacSSasha Smundak		opts.QualifiedNames = true
92*88d15eacSSasha Smundak	}
93*88d15eacSSasha Smundak	return opts
94*88d15eacSSasha Smundak}
95*88d15eacSSasha Smundak
96*88d15eacSSasha Smundak// FormatDiff converts a valueNode tree into a textNode tree, where the later
97*88d15eacSSasha Smundak// is a textual representation of the differences detected in the former.
98*88d15eacSSasha Smundakfunc (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
99*88d15eacSSasha Smundak	if opts.DiffMode == diffIdentical {
100*88d15eacSSasha Smundak		opts = opts.WithVerbosity(1)
101*88d15eacSSasha Smundak	} else if opts.verbosity() < 3 {
102*88d15eacSSasha Smundak		opts = opts.WithVerbosity(3)
103*88d15eacSSasha Smundak	}
104*88d15eacSSasha Smundak
105*88d15eacSSasha Smundak	// Check whether we have specialized formatting for this node.
106*88d15eacSSasha Smundak	// This is not necessary, but helpful for producing more readable outputs.
107*88d15eacSSasha Smundak	if opts.CanFormatDiffSlice(v) {
108*88d15eacSSasha Smundak		return opts.FormatDiffSlice(v)
109*88d15eacSSasha Smundak	}
110*88d15eacSSasha Smundak
111*88d15eacSSasha Smundak	var parentKind reflect.Kind
112*88d15eacSSasha Smundak	if v.parent != nil && v.parent.TransformerName == "" {
113*88d15eacSSasha Smundak		parentKind = v.parent.Type.Kind()
114*88d15eacSSasha Smundak	}
115*88d15eacSSasha Smundak
116*88d15eacSSasha Smundak	// For leaf nodes, format the value based on the reflect.Values alone.
117*88d15eacSSasha Smundak	// As a special case, treat equal []byte as a leaf nodes.
118*88d15eacSSasha Smundak	isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType
119*88d15eacSSasha Smundak	isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0
120*88d15eacSSasha Smundak	if v.MaxDepth == 0 || isEqualBytes {
121*88d15eacSSasha Smundak		switch opts.DiffMode {
122*88d15eacSSasha Smundak		case diffUnknown, diffIdentical:
123*88d15eacSSasha Smundak			// Format Equal.
124*88d15eacSSasha Smundak			if v.NumDiff == 0 {
125*88d15eacSSasha Smundak				outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
126*88d15eacSSasha Smundak				outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
127*88d15eacSSasha Smundak				if v.NumIgnored > 0 && v.NumSame == 0 {
128*88d15eacSSasha Smundak					return textEllipsis
129*88d15eacSSasha Smundak				} else if outx.Len() < outy.Len() {
130*88d15eacSSasha Smundak					return outx
131*88d15eacSSasha Smundak				} else {
132*88d15eacSSasha Smundak					return outy
133*88d15eacSSasha Smundak				}
134*88d15eacSSasha Smundak			}
135*88d15eacSSasha Smundak
136*88d15eacSSasha Smundak			// Format unequal.
137*88d15eacSSasha Smundak			assert(opts.DiffMode == diffUnknown)
138*88d15eacSSasha Smundak			var list textList
139*88d15eacSSasha Smundak			outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
140*88d15eacSSasha Smundak			outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
141*88d15eacSSasha Smundak			for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
142*88d15eacSSasha Smundak				opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
143*88d15eacSSasha Smundak				outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
144*88d15eacSSasha Smundak				outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
145*88d15eacSSasha Smundak			}
146*88d15eacSSasha Smundak			if outx != nil {
147*88d15eacSSasha Smundak				list = append(list, textRecord{Diff: '-', Value: outx})
148*88d15eacSSasha Smundak			}
149*88d15eacSSasha Smundak			if outy != nil {
150*88d15eacSSasha Smundak				list = append(list, textRecord{Diff: '+', Value: outy})
151*88d15eacSSasha Smundak			}
152*88d15eacSSasha Smundak			return opts.WithTypeMode(emitType).FormatType(v.Type, list)
153*88d15eacSSasha Smundak		case diffRemoved:
154*88d15eacSSasha Smundak			return opts.FormatValue(v.ValueX, parentKind, ptrs)
155*88d15eacSSasha Smundak		case diffInserted:
156*88d15eacSSasha Smundak			return opts.FormatValue(v.ValueY, parentKind, ptrs)
157*88d15eacSSasha Smundak		default:
158*88d15eacSSasha Smundak			panic("invalid diff mode")
159*88d15eacSSasha Smundak		}
160*88d15eacSSasha Smundak	}
161*88d15eacSSasha Smundak
162*88d15eacSSasha Smundak	// Register slice element to support cycle detection.
163*88d15eacSSasha Smundak	if parentKind == reflect.Slice {
164*88d15eacSSasha Smundak		ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
165*88d15eacSSasha Smundak		defer ptrs.Pop()
166*88d15eacSSasha Smundak		defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
167*88d15eacSSasha Smundak	}
168*88d15eacSSasha Smundak
169*88d15eacSSasha Smundak	// Descend into the child value node.
170*88d15eacSSasha Smundak	if v.TransformerName != "" {
171*88d15eacSSasha Smundak		out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
172*88d15eacSSasha Smundak		out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
173*88d15eacSSasha Smundak		return opts.FormatType(v.Type, out)
174*88d15eacSSasha Smundak	} else {
175*88d15eacSSasha Smundak		switch k := v.Type.Kind(); k {
176*88d15eacSSasha Smundak		case reflect.Struct, reflect.Array, reflect.Slice:
177*88d15eacSSasha Smundak			out = opts.formatDiffList(v.Records, k, ptrs)
178*88d15eacSSasha Smundak			out = opts.FormatType(v.Type, out)
179*88d15eacSSasha Smundak		case reflect.Map:
180*88d15eacSSasha Smundak			// Register map to support cycle detection.
181*88d15eacSSasha Smundak			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
182*88d15eacSSasha Smundak			defer ptrs.Pop()
183*88d15eacSSasha Smundak
184*88d15eacSSasha Smundak			out = opts.formatDiffList(v.Records, k, ptrs)
185*88d15eacSSasha Smundak			out = wrapTrunkReferences(ptrRefs, out)
186*88d15eacSSasha Smundak			out = opts.FormatType(v.Type, out)
187*88d15eacSSasha Smundak		case reflect.Ptr:
188*88d15eacSSasha Smundak			// Register pointer to support cycle detection.
189*88d15eacSSasha Smundak			ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
190*88d15eacSSasha Smundak			defer ptrs.Pop()
191*88d15eacSSasha Smundak
192*88d15eacSSasha Smundak			out = opts.FormatDiff(v.Value, ptrs)
193*88d15eacSSasha Smundak			out = wrapTrunkReferences(ptrRefs, out)
194*88d15eacSSasha Smundak			out = &textWrap{Prefix: "&", Value: out}
195*88d15eacSSasha Smundak		case reflect.Interface:
196*88d15eacSSasha Smundak			out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
197*88d15eacSSasha Smundak		default:
198*88d15eacSSasha Smundak			panic(fmt.Sprintf("%v cannot have children", k))
199*88d15eacSSasha Smundak		}
200*88d15eacSSasha Smundak		return out
201*88d15eacSSasha Smundak	}
202*88d15eacSSasha Smundak}
203*88d15eacSSasha Smundak
204*88d15eacSSasha Smundakfunc (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
205*88d15eacSSasha Smundak	// Derive record name based on the data structure kind.
206*88d15eacSSasha Smundak	var name string
207*88d15eacSSasha Smundak	var formatKey func(reflect.Value) string
208*88d15eacSSasha Smundak	switch k {
209*88d15eacSSasha Smundak	case reflect.Struct:
210*88d15eacSSasha Smundak		name = "field"
211*88d15eacSSasha Smundak		opts = opts.WithTypeMode(autoType)
212*88d15eacSSasha Smundak		formatKey = func(v reflect.Value) string { return v.String() }
213*88d15eacSSasha Smundak	case reflect.Slice, reflect.Array:
214*88d15eacSSasha Smundak		name = "element"
215*88d15eacSSasha Smundak		opts = opts.WithTypeMode(elideType)
216*88d15eacSSasha Smundak		formatKey = func(reflect.Value) string { return "" }
217*88d15eacSSasha Smundak	case reflect.Map:
218*88d15eacSSasha Smundak		name = "entry"
219*88d15eacSSasha Smundak		opts = opts.WithTypeMode(elideType)
220*88d15eacSSasha Smundak		formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
221*88d15eacSSasha Smundak	}
222*88d15eacSSasha Smundak
223*88d15eacSSasha Smundak	maxLen := -1
224*88d15eacSSasha Smundak	if opts.LimitVerbosity {
225*88d15eacSSasha Smundak		if opts.DiffMode == diffIdentical {
226*88d15eacSSasha Smundak			maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
227*88d15eacSSasha Smundak		} else {
228*88d15eacSSasha Smundak			maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
229*88d15eacSSasha Smundak		}
230*88d15eacSSasha Smundak		opts.VerbosityLevel--
231*88d15eacSSasha Smundak	}
232*88d15eacSSasha Smundak
233*88d15eacSSasha Smundak	// Handle unification.
234*88d15eacSSasha Smundak	switch opts.DiffMode {
235*88d15eacSSasha Smundak	case diffIdentical, diffRemoved, diffInserted:
236*88d15eacSSasha Smundak		var list textList
237*88d15eacSSasha Smundak		var deferredEllipsis bool // Add final "..." to indicate records were dropped
238*88d15eacSSasha Smundak		for _, r := range recs {
239*88d15eacSSasha Smundak			if len(list) == maxLen {
240*88d15eacSSasha Smundak				deferredEllipsis = true
241*88d15eacSSasha Smundak				break
242*88d15eacSSasha Smundak			}
243*88d15eacSSasha Smundak
244*88d15eacSSasha Smundak			// Elide struct fields that are zero value.
245*88d15eacSSasha Smundak			if k == reflect.Struct {
246*88d15eacSSasha Smundak				var isZero bool
247*88d15eacSSasha Smundak				switch opts.DiffMode {
248*88d15eacSSasha Smundak				case diffIdentical:
249*88d15eacSSasha Smundak					isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero()
250*88d15eacSSasha Smundak				case diffRemoved:
251*88d15eacSSasha Smundak					isZero = r.Value.ValueX.IsZero()
252*88d15eacSSasha Smundak				case diffInserted:
253*88d15eacSSasha Smundak					isZero = r.Value.ValueY.IsZero()
254*88d15eacSSasha Smundak				}
255*88d15eacSSasha Smundak				if isZero {
256*88d15eacSSasha Smundak					continue
257*88d15eacSSasha Smundak				}
258*88d15eacSSasha Smundak			}
259*88d15eacSSasha Smundak			// Elide ignored nodes.
260*88d15eacSSasha Smundak			if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
261*88d15eacSSasha Smundak				deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
262*88d15eacSSasha Smundak				if !deferredEllipsis {
263*88d15eacSSasha Smundak					list.AppendEllipsis(diffStats{})
264*88d15eacSSasha Smundak				}
265*88d15eacSSasha Smundak				continue
266*88d15eacSSasha Smundak			}
267*88d15eacSSasha Smundak			if out := opts.FormatDiff(r.Value, ptrs); out != nil {
268*88d15eacSSasha Smundak				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
269*88d15eacSSasha Smundak			}
270*88d15eacSSasha Smundak		}
271*88d15eacSSasha Smundak		if deferredEllipsis {
272*88d15eacSSasha Smundak			list.AppendEllipsis(diffStats{})
273*88d15eacSSasha Smundak		}
274*88d15eacSSasha Smundak		return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
275*88d15eacSSasha Smundak	case diffUnknown:
276*88d15eacSSasha Smundak	default:
277*88d15eacSSasha Smundak		panic("invalid diff mode")
278*88d15eacSSasha Smundak	}
279*88d15eacSSasha Smundak
280*88d15eacSSasha Smundak	// Handle differencing.
281*88d15eacSSasha Smundak	var numDiffs int
282*88d15eacSSasha Smundak	var list textList
283*88d15eacSSasha Smundak	var keys []reflect.Value // invariant: len(list) == len(keys)
284*88d15eacSSasha Smundak	groups := coalesceAdjacentRecords(name, recs)
285*88d15eacSSasha Smundak	maxGroup := diffStats{Name: name}
286*88d15eacSSasha Smundak	for i, ds := range groups {
287*88d15eacSSasha Smundak		if maxLen >= 0 && numDiffs >= maxLen {
288*88d15eacSSasha Smundak			maxGroup = maxGroup.Append(ds)
289*88d15eacSSasha Smundak			continue
290*88d15eacSSasha Smundak		}
291*88d15eacSSasha Smundak
292*88d15eacSSasha Smundak		// Handle equal records.
293*88d15eacSSasha Smundak		if ds.NumDiff() == 0 {
294*88d15eacSSasha Smundak			// Compute the number of leading and trailing records to print.
295*88d15eacSSasha Smundak			var numLo, numHi int
296*88d15eacSSasha Smundak			numEqual := ds.NumIgnored + ds.NumIdentical
297*88d15eacSSasha Smundak			for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
298*88d15eacSSasha Smundak				if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
299*88d15eacSSasha Smundak					break
300*88d15eacSSasha Smundak				}
301*88d15eacSSasha Smundak				numLo++
302*88d15eacSSasha Smundak			}
303*88d15eacSSasha Smundak			for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
304*88d15eacSSasha Smundak				if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
305*88d15eacSSasha Smundak					break
306*88d15eacSSasha Smundak				}
307*88d15eacSSasha Smundak				numHi++
308*88d15eacSSasha Smundak			}
309*88d15eacSSasha Smundak			if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
310*88d15eacSSasha Smundak				numHi++ // Avoid pointless coalescing of a single equal record
311*88d15eacSSasha Smundak			}
312*88d15eacSSasha Smundak
313*88d15eacSSasha Smundak			// Format the equal values.
314*88d15eacSSasha Smundak			for _, r := range recs[:numLo] {
315*88d15eacSSasha Smundak				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
316*88d15eacSSasha Smundak				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
317*88d15eacSSasha Smundak				keys = append(keys, r.Key)
318*88d15eacSSasha Smundak			}
319*88d15eacSSasha Smundak			if numEqual > numLo+numHi {
320*88d15eacSSasha Smundak				ds.NumIdentical -= numLo + numHi
321*88d15eacSSasha Smundak				list.AppendEllipsis(ds)
322*88d15eacSSasha Smundak				for len(keys) < len(list) {
323*88d15eacSSasha Smundak					keys = append(keys, reflect.Value{})
324*88d15eacSSasha Smundak				}
325*88d15eacSSasha Smundak			}
326*88d15eacSSasha Smundak			for _, r := range recs[numEqual-numHi : numEqual] {
327*88d15eacSSasha Smundak				out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
328*88d15eacSSasha Smundak				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
329*88d15eacSSasha Smundak				keys = append(keys, r.Key)
330*88d15eacSSasha Smundak			}
331*88d15eacSSasha Smundak			recs = recs[numEqual:]
332*88d15eacSSasha Smundak			continue
333*88d15eacSSasha Smundak		}
334*88d15eacSSasha Smundak
335*88d15eacSSasha Smundak		// Handle unequal records.
336*88d15eacSSasha Smundak		for _, r := range recs[:ds.NumDiff()] {
337*88d15eacSSasha Smundak			switch {
338*88d15eacSSasha Smundak			case opts.CanFormatDiffSlice(r.Value):
339*88d15eacSSasha Smundak				out := opts.FormatDiffSlice(r.Value)
340*88d15eacSSasha Smundak				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
341*88d15eacSSasha Smundak				keys = append(keys, r.Key)
342*88d15eacSSasha Smundak			case r.Value.NumChildren == r.Value.MaxDepth:
343*88d15eacSSasha Smundak				outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
344*88d15eacSSasha Smundak				outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
345*88d15eacSSasha Smundak				for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
346*88d15eacSSasha Smundak					opts2 := verbosityPreset(opts, i)
347*88d15eacSSasha Smundak					outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
348*88d15eacSSasha Smundak					outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
349*88d15eacSSasha Smundak				}
350*88d15eacSSasha Smundak				if outx != nil {
351*88d15eacSSasha Smundak					list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
352*88d15eacSSasha Smundak					keys = append(keys, r.Key)
353*88d15eacSSasha Smundak				}
354*88d15eacSSasha Smundak				if outy != nil {
355*88d15eacSSasha Smundak					list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
356*88d15eacSSasha Smundak					keys = append(keys, r.Key)
357*88d15eacSSasha Smundak				}
358*88d15eacSSasha Smundak			default:
359*88d15eacSSasha Smundak				out := opts.FormatDiff(r.Value, ptrs)
360*88d15eacSSasha Smundak				list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
361*88d15eacSSasha Smundak				keys = append(keys, r.Key)
362*88d15eacSSasha Smundak			}
363*88d15eacSSasha Smundak		}
364*88d15eacSSasha Smundak		recs = recs[ds.NumDiff():]
365*88d15eacSSasha Smundak		numDiffs += ds.NumDiff()
366*88d15eacSSasha Smundak	}
367*88d15eacSSasha Smundak	if maxGroup.IsZero() {
368*88d15eacSSasha Smundak		assert(len(recs) == 0)
369*88d15eacSSasha Smundak	} else {
370*88d15eacSSasha Smundak		list.AppendEllipsis(maxGroup)
371*88d15eacSSasha Smundak		for len(keys) < len(list) {
372*88d15eacSSasha Smundak			keys = append(keys, reflect.Value{})
373*88d15eacSSasha Smundak		}
374*88d15eacSSasha Smundak	}
375*88d15eacSSasha Smundak	assert(len(list) == len(keys))
376*88d15eacSSasha Smundak
377*88d15eacSSasha Smundak	// For maps, the default formatting logic uses fmt.Stringer which may
378*88d15eacSSasha Smundak	// produce ambiguous output. Avoid calling String to disambiguate.
379*88d15eacSSasha Smundak	if k == reflect.Map {
380*88d15eacSSasha Smundak		var ambiguous bool
381*88d15eacSSasha Smundak		seenKeys := map[string]reflect.Value{}
382*88d15eacSSasha Smundak		for i, currKey := range keys {
383*88d15eacSSasha Smundak			if currKey.IsValid() {
384*88d15eacSSasha Smundak				strKey := list[i].Key
385*88d15eacSSasha Smundak				prevKey, seen := seenKeys[strKey]
386*88d15eacSSasha Smundak				if seen && prevKey.CanInterface() && currKey.CanInterface() {
387*88d15eacSSasha Smundak					ambiguous = prevKey.Interface() != currKey.Interface()
388*88d15eacSSasha Smundak					if ambiguous {
389*88d15eacSSasha Smundak						break
390*88d15eacSSasha Smundak					}
391*88d15eacSSasha Smundak				}
392*88d15eacSSasha Smundak				seenKeys[strKey] = currKey
393*88d15eacSSasha Smundak			}
394*88d15eacSSasha Smundak		}
395*88d15eacSSasha Smundak		if ambiguous {
396*88d15eacSSasha Smundak			for i, k := range keys {
397*88d15eacSSasha Smundak				if k.IsValid() {
398*88d15eacSSasha Smundak					list[i].Key = formatMapKey(k, true, ptrs)
399*88d15eacSSasha Smundak				}
400*88d15eacSSasha Smundak			}
401*88d15eacSSasha Smundak		}
402*88d15eacSSasha Smundak	}
403*88d15eacSSasha Smundak
404*88d15eacSSasha Smundak	return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
405*88d15eacSSasha Smundak}
406*88d15eacSSasha Smundak
407*88d15eacSSasha Smundak// coalesceAdjacentRecords coalesces the list of records into groups of
408*88d15eacSSasha Smundak// adjacent equal, or unequal counts.
409*88d15eacSSasha Smundakfunc coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
410*88d15eacSSasha Smundak	var prevCase int // Arbitrary index into which case last occurred
411*88d15eacSSasha Smundak	lastStats := func(i int) *diffStats {
412*88d15eacSSasha Smundak		if prevCase != i {
413*88d15eacSSasha Smundak			groups = append(groups, diffStats{Name: name})
414*88d15eacSSasha Smundak			prevCase = i
415*88d15eacSSasha Smundak		}
416*88d15eacSSasha Smundak		return &groups[len(groups)-1]
417*88d15eacSSasha Smundak	}
418*88d15eacSSasha Smundak	for _, r := range recs {
419*88d15eacSSasha Smundak		switch rv := r.Value; {
420*88d15eacSSasha Smundak		case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
421*88d15eacSSasha Smundak			lastStats(1).NumIgnored++
422*88d15eacSSasha Smundak		case rv.NumDiff == 0:
423*88d15eacSSasha Smundak			lastStats(1).NumIdentical++
424*88d15eacSSasha Smundak		case rv.NumDiff > 0 && !rv.ValueY.IsValid():
425*88d15eacSSasha Smundak			lastStats(2).NumRemoved++
426*88d15eacSSasha Smundak		case rv.NumDiff > 0 && !rv.ValueX.IsValid():
427*88d15eacSSasha Smundak			lastStats(2).NumInserted++
428*88d15eacSSasha Smundak		default:
429*88d15eacSSasha Smundak			lastStats(2).NumModified++
430*88d15eacSSasha Smundak		}
431*88d15eacSSasha Smundak	}
432*88d15eacSSasha Smundak	return groups
433*88d15eacSSasha Smundak}
434