1// Copyright 2019, The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package cmp 6 7import ( 8 "fmt" 9 "reflect" 10 11 "github.com/google/go-cmp/cmp/internal/value" 12) 13 14// 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} 66func (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} 97 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. 100func (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 107 // 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 113 var parentKind reflect.Kind 114 if v.parent != nil && v.parent.TransformerName == "" { 115 parentKind = v.parent.Type.Kind() 116 } 117 118 // 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 { 124 outx := opts.FormatValue(v.ValueX, parentKind, ptrs) 125 outy := opts.FormatValue(v.ValueY, parentKind, ptrs) 126 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 138 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 } 145 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: 153 return opts.FormatValue(v.ValueX, parentKind, ptrs) 154 case diffInserted: 155 return opts.FormatValue(v.ValueY, parentKind, ptrs) 156 default: 157 panic("invalid diff mode") 158 } 159 } 160 161 // 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 168 // Descend into the child value node. 169 if v.TransformerName != "" { 170 out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) 171 out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"} 172 return opts.FormatType(v.Type, out) 173 } else { 174 switch k := v.Type.Kind(); k { 175 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) 186 case reflect.Ptr: 187 // 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} 194 case reflect.Interface: 195 out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs) 196 default: 197 panic(fmt.Sprintf("%v cannot have children", k)) 198 } 199 return out 200 } 201} 202 203func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode { 204 // 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) 219 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-- 230 } 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 { 238 if len(list) == maxLen { 239 deferredEllipsis = true 240 break 241 } 242 243 // 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 } 266 if out := opts.FormatDiff(r.Value, ptrs); out != nil { 267 list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 268 } 269 } 270 if deferredEllipsis { 271 list.AppendEllipsis(diffStats{}) 272 } 273 return &textWrap{Prefix: "{", Value: list, Suffix: "}"} 274 case diffUnknown: 275 default: 276 panic("invalid diff mode") 277 } 278 279 // Handle differencing. 280 var numDiffs int 281 var list textList 282 var keys []reflect.Value // invariant: len(list) == len(keys) 283 groups := coalesceAdjacentRecords(name, recs) 284 maxGroup := diffStats{Name: name} 285 for i, ds := range groups { 286 if maxLen >= 0 && numDiffs >= maxLen { 287 maxGroup = maxGroup.Append(ds) 288 continue 289 } 290 291 // 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] { 314 out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) 315 list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 316 keys = append(keys, r.Key) 317 } 318 if numEqual > numLo+numHi { 319 ds.NumIdentical -= numLo + numHi 320 list.AppendEllipsis(ds) 321 for len(keys) < len(list) { 322 keys = append(keys, reflect.Value{}) 323 } 324 } 325 for _, r := range recs[numEqual-numHi : numEqual] { 326 out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs) 327 list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 328 keys = append(keys, r.Key) 329 } 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}) 340 keys = append(keys, r.Key) 341 case r.Value.NumChildren == r.Value.MaxDepth: 342 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 } 349 if outx != nil { 350 list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx}) 351 keys = append(keys, r.Key) 352 } 353 if outy != nil { 354 list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy}) 355 keys = append(keys, r.Key) 356 } 357 default: 358 out := opts.FormatDiff(r.Value, ptrs) 359 list = append(list, textRecord{Key: formatKey(r.Key), Value: out}) 360 keys = append(keys, r.Key) 361 } 362 } 363 recs = recs[ds.NumDiff():] 364 numDiffs += ds.NumDiff() 365 } 366 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: "}"} 404} 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} 433