blob: 104bb30538bcdfed15dcb979a457580c0a532f7c [file] [log] [blame]
Pragya Arya324337e2020-02-20 14:35:08 +05301// Copyright 2019, The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
David K. Bainbridgec415efe2021-08-19 13:05:21 +00003// license that can be found in the LICENSE file.
Pragya Arya324337e2020-02-20 14:35:08 +05304
5package cmp
6
7import (
8 "fmt"
9 "reflect"
10
11 "github.com/google/go-cmp/cmp/internal/value"
12)
13
Pragya Arya324337e2020-02-20 14:35:08 +053014// numContextRecords is the number of surrounding equal records to print.
15const numContextRecords = 2
16
17type diffMode byte
18
19const (
20 diffUnknown diffMode = 0
21 diffIdentical diffMode = ' '
22 diffRemoved diffMode = '-'
23 diffInserted diffMode = '+'
24)
25
26type typeMode int
27
28const (
29 // emitType always prints the type.
30 emitType typeMode = iota
31 // elideType never prints the type.
32 elideType
33 // autoType prints the type only for composite kinds
34 // (i.e., structs, slices, arrays, and maps).
35 autoType
36)
37
38type formatOptions struct {
39 // DiffMode controls the output mode of FormatDiff.
40 //
41 // If diffUnknown, then produce a diff of the x and y values.
42 // If diffIdentical, then emit values as if they were equal.
43 // If diffRemoved, then only emit x values (ignoring y values).
44 // If diffInserted, then only emit y values (ignoring x values).
45 DiffMode diffMode
46
47 // TypeMode controls whether to print the type for the current node.
48 //
49 // As a general rule of thumb, we always print the type of the next node
50 // after an interface, and always elide the type of the next node after
51 // a slice or map node.
52 TypeMode typeMode
53
54 // formatValueOptions are options specific to printing reflect.Values.
55 formatValueOptions
56}
57
58func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
59 opts.DiffMode = d
60 return opts
61}
62func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
63 opts.TypeMode = t
64 return opts
65}
David K. Bainbridgec415efe2021-08-19 13:05:21 +000066func (opts formatOptions) WithVerbosity(level int) formatOptions {
67 opts.VerbosityLevel = level
68 opts.LimitVerbosity = true
69 return opts
70}
71func (opts formatOptions) verbosity() uint {
72 switch {
73 case opts.VerbosityLevel < 0:
74 return 0
75 case opts.VerbosityLevel > 16:
76 return 16 // some reasonable maximum to avoid shift overflow
77 default:
78 return uint(opts.VerbosityLevel)
79 }
80}
81
82const maxVerbosityPreset = 6
83
84// verbosityPreset modifies the verbosity settings given an index
85// between 0 and maxVerbosityPreset, inclusive.
86func verbosityPreset(opts formatOptions, i int) formatOptions {
87 opts.VerbosityLevel = int(opts.verbosity()) + 2*i
88 if i > 0 {
89 opts.AvoidStringer = true
90 }
91 if i >= maxVerbosityPreset {
92 opts.PrintAddresses = true
93 opts.QualifiedNames = true
94 }
95 return opts
96}
Pragya Arya324337e2020-02-20 14:35:08 +053097
98// FormatDiff converts a valueNode tree into a textNode tree, where the later
99// is a textual representation of the differences detected in the former.
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000100func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {
101 if opts.DiffMode == diffIdentical {
102 opts = opts.WithVerbosity(1)
103 } else if opts.verbosity() < 3 {
104 opts = opts.WithVerbosity(3)
105 }
106
Pragya Arya324337e2020-02-20 14:35:08 +0530107 // Check whether we have specialized formatting for this node.
108 // This is not necessary, but helpful for producing more readable outputs.
109 if opts.CanFormatDiffSlice(v) {
110 return opts.FormatDiffSlice(v)
111 }
112
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000113 var parentKind reflect.Kind
114 if v.parent != nil && v.parent.TransformerName == "" {
115 parentKind = v.parent.Type.Kind()
116 }
117
Pragya Arya324337e2020-02-20 14:35:08 +0530118 // For leaf nodes, format the value based on the reflect.Values alone.
119 if v.MaxDepth == 0 {
120 switch opts.DiffMode {
121 case diffUnknown, diffIdentical:
122 // Format Equal.
123 if v.NumDiff == 0 {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000124 outx := opts.FormatValue(v.ValueX, parentKind, ptrs)
125 outy := opts.FormatValue(v.ValueY, parentKind, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530126 if v.NumIgnored > 0 && v.NumSame == 0 {
127 return textEllipsis
128 } else if outx.Len() < outy.Len() {
129 return outx
130 } else {
131 return outy
132 }
133 }
134
135 // Format unequal.
136 assert(opts.DiffMode == diffUnknown)
137 var list textList
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000138 outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)
139 outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)
140 for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
141 opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)
142 outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)
143 outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)
144 }
Pragya Arya324337e2020-02-20 14:35:08 +0530145 if outx != nil {
146 list = append(list, textRecord{Diff: '-', Value: outx})
147 }
148 if outy != nil {
149 list = append(list, textRecord{Diff: '+', Value: outy})
150 }
151 return opts.WithTypeMode(emitType).FormatType(v.Type, list)
152 case diffRemoved:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000153 return opts.FormatValue(v.ValueX, parentKind, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530154 case diffInserted:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000155 return opts.FormatValue(v.ValueY, parentKind, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530156 default:
157 panic("invalid diff mode")
158 }
159 }
160
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000161 // Register slice element to support cycle detection.
162 if parentKind == reflect.Slice {
163 ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)
164 defer ptrs.Pop()
165 defer func() { out = wrapTrunkReferences(ptrRefs, out) }()
166 }
167
Pragya Arya324337e2020-02-20 14:35:08 +0530168 // Descend into the child value node.
169 if v.TransformerName != "" {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000170 out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
171 out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}
Pragya Arya324337e2020-02-20 14:35:08 +0530172 return opts.FormatType(v.Type, out)
173 } else {
174 switch k := v.Type.Kind(); k {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000175 case reflect.Struct, reflect.Array, reflect.Slice:
176 out = opts.formatDiffList(v.Records, k, ptrs)
177 out = opts.FormatType(v.Type, out)
178 case reflect.Map:
179 // Register map to support cycle detection.
180 ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
181 defer ptrs.Pop()
182
183 out = opts.formatDiffList(v.Records, k, ptrs)
184 out = wrapTrunkReferences(ptrRefs, out)
185 out = opts.FormatType(v.Type, out)
Pragya Arya324337e2020-02-20 14:35:08 +0530186 case reflect.Ptr:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000187 // Register pointer to support cycle detection.
188 ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)
189 defer ptrs.Pop()
190
191 out = opts.FormatDiff(v.Value, ptrs)
192 out = wrapTrunkReferences(ptrRefs, out)
193 out = &textWrap{Prefix: "&", Value: out}
Pragya Arya324337e2020-02-20 14:35:08 +0530194 case reflect.Interface:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000195 out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530196 default:
197 panic(fmt.Sprintf("%v cannot have children", k))
198 }
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000199 return out
Pragya Arya324337e2020-02-20 14:35:08 +0530200 }
201}
202
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000203func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {
Pragya Arya324337e2020-02-20 14:35:08 +0530204 // Derive record name based on the data structure kind.
205 var name string
206 var formatKey func(reflect.Value) string
207 switch k {
208 case reflect.Struct:
209 name = "field"
210 opts = opts.WithTypeMode(autoType)
211 formatKey = func(v reflect.Value) string { return v.String() }
212 case reflect.Slice, reflect.Array:
213 name = "element"
214 opts = opts.WithTypeMode(elideType)
215 formatKey = func(reflect.Value) string { return "" }
216 case reflect.Map:
217 name = "entry"
218 opts = opts.WithTypeMode(elideType)
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000219 formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }
220 }
221
222 maxLen := -1
223 if opts.LimitVerbosity {
224 if opts.DiffMode == diffIdentical {
225 maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...
226 } else {
227 maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...
228 }
229 opts.VerbosityLevel--
Pragya Arya324337e2020-02-20 14:35:08 +0530230 }
231
232 // Handle unification.
233 switch opts.DiffMode {
234 case diffIdentical, diffRemoved, diffInserted:
235 var list textList
236 var deferredEllipsis bool // Add final "..." to indicate records were dropped
237 for _, r := range recs {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000238 if len(list) == maxLen {
239 deferredEllipsis = true
240 break
241 }
242
Pragya Arya324337e2020-02-20 14:35:08 +0530243 // Elide struct fields that are zero value.
244 if k == reflect.Struct {
245 var isZero bool
246 switch opts.DiffMode {
247 case diffIdentical:
248 isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY)
249 case diffRemoved:
250 isZero = value.IsZero(r.Value.ValueX)
251 case diffInserted:
252 isZero = value.IsZero(r.Value.ValueY)
253 }
254 if isZero {
255 continue
256 }
257 }
258 // Elide ignored nodes.
259 if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
260 deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
261 if !deferredEllipsis {
262 list.AppendEllipsis(diffStats{})
263 }
264 continue
265 }
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000266 if out := opts.FormatDiff(r.Value, ptrs); out != nil {
Pragya Arya324337e2020-02-20 14:35:08 +0530267 list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
268 }
269 }
270 if deferredEllipsis {
271 list.AppendEllipsis(diffStats{})
272 }
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000273 return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
Pragya Arya324337e2020-02-20 14:35:08 +0530274 case diffUnknown:
275 default:
276 panic("invalid diff mode")
277 }
278
279 // Handle differencing.
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000280 var numDiffs int
Pragya Arya324337e2020-02-20 14:35:08 +0530281 var list textList
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000282 var keys []reflect.Value // invariant: len(list) == len(keys)
Pragya Arya324337e2020-02-20 14:35:08 +0530283 groups := coalesceAdjacentRecords(name, recs)
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000284 maxGroup := diffStats{Name: name}
Pragya Arya324337e2020-02-20 14:35:08 +0530285 for i, ds := range groups {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000286 if maxLen >= 0 && numDiffs >= maxLen {
287 maxGroup = maxGroup.Append(ds)
288 continue
289 }
290
Pragya Arya324337e2020-02-20 14:35:08 +0530291 // Handle equal records.
292 if ds.NumDiff() == 0 {
293 // Compute the number of leading and trailing records to print.
294 var numLo, numHi int
295 numEqual := ds.NumIgnored + ds.NumIdentical
296 for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
297 if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
298 break
299 }
300 numLo++
301 }
302 for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
303 if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
304 break
305 }
306 numHi++
307 }
308 if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
309 numHi++ // Avoid pointless coalescing of a single equal record
310 }
311
312 // Format the equal values.
313 for _, r := range recs[:numLo] {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000314 out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530315 list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000316 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530317 }
318 if numEqual > numLo+numHi {
319 ds.NumIdentical -= numLo + numHi
320 list.AppendEllipsis(ds)
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000321 for len(keys) < len(list) {
322 keys = append(keys, reflect.Value{})
323 }
Pragya Arya324337e2020-02-20 14:35:08 +0530324 }
325 for _, r := range recs[numEqual-numHi : numEqual] {
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000326 out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530327 list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000328 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530329 }
330 recs = recs[numEqual:]
331 continue
332 }
333
334 // Handle unequal records.
335 for _, r := range recs[:ds.NumDiff()] {
336 switch {
337 case opts.CanFormatDiffSlice(r.Value):
338 out := opts.FormatDiffSlice(r.Value)
339 list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000340 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530341 case r.Value.NumChildren == r.Value.MaxDepth:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000342 outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
343 outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
344 for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {
345 opts2 := verbosityPreset(opts, i)
346 outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)
347 outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)
348 }
Pragya Arya324337e2020-02-20 14:35:08 +0530349 if outx != nil {
350 list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000351 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530352 }
353 if outy != nil {
354 list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000355 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530356 }
357 default:
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000358 out := opts.FormatDiff(r.Value, ptrs)
Pragya Arya324337e2020-02-20 14:35:08 +0530359 list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000360 keys = append(keys, r.Key)
Pragya Arya324337e2020-02-20 14:35:08 +0530361 }
362 }
363 recs = recs[ds.NumDiff():]
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000364 numDiffs += ds.NumDiff()
Pragya Arya324337e2020-02-20 14:35:08 +0530365 }
David K. Bainbridgec415efe2021-08-19 13:05:21 +0000366 if maxGroup.IsZero() {
367 assert(len(recs) == 0)
368 } else {
369 list.AppendEllipsis(maxGroup)
370 for len(keys) < len(list) {
371 keys = append(keys, reflect.Value{})
372 }
373 }
374 assert(len(list) == len(keys))
375
376 // For maps, the default formatting logic uses fmt.Stringer which may
377 // produce ambiguous output. Avoid calling String to disambiguate.
378 if k == reflect.Map {
379 var ambiguous bool
380 seenKeys := map[string]reflect.Value{}
381 for i, currKey := range keys {
382 if currKey.IsValid() {
383 strKey := list[i].Key
384 prevKey, seen := seenKeys[strKey]
385 if seen && prevKey.CanInterface() && currKey.CanInterface() {
386 ambiguous = prevKey.Interface() != currKey.Interface()
387 if ambiguous {
388 break
389 }
390 }
391 seenKeys[strKey] = currKey
392 }
393 }
394 if ambiguous {
395 for i, k := range keys {
396 if k.IsValid() {
397 list[i].Key = formatMapKey(k, true, ptrs)
398 }
399 }
400 }
401 }
402
403 return &textWrap{Prefix: "{", Value: list, Suffix: "}"}
Pragya Arya324337e2020-02-20 14:35:08 +0530404}
405
406// coalesceAdjacentRecords coalesces the list of records into groups of
407// adjacent equal, or unequal counts.
408func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
409 var prevCase int // Arbitrary index into which case last occurred
410 lastStats := func(i int) *diffStats {
411 if prevCase != i {
412 groups = append(groups, diffStats{Name: name})
413 prevCase = i
414 }
415 return &groups[len(groups)-1]
416 }
417 for _, r := range recs {
418 switch rv := r.Value; {
419 case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
420 lastStats(1).NumIgnored++
421 case rv.NumDiff == 0:
422 lastStats(1).NumIdentical++
423 case rv.NumDiff > 0 && !rv.ValueY.IsValid():
424 lastStats(2).NumRemoved++
425 case rv.NumDiff > 0 && !rv.ValueX.IsValid():
426 lastStats(2).NumInserted++
427 default:
428 lastStats(2).NumModified++
429 }
430 }
431 return groups
432}