Path: blob/main/vendor/github.com/google/go-cmp/cmp/report_compare.go
2880 views
// Copyright 2019, The Go Authors. All rights reserved.1// Use of this source code is governed by a BSD-style2// license that can be found in the LICENSE file.34package cmp56import (7"fmt"8"reflect"9)1011// numContextRecords is the number of surrounding equal records to print.12const numContextRecords = 21314type diffMode byte1516const (17diffUnknown diffMode = 018diffIdentical diffMode = ' '19diffRemoved diffMode = '-'20diffInserted diffMode = '+'21)2223type typeMode int2425const (26// emitType always prints the type.27emitType typeMode = iota28// elideType never prints the type.29elideType30// autoType prints the type only for composite kinds31// (i.e., structs, slices, arrays, and maps).32autoType33)3435type formatOptions struct {36// DiffMode controls the output mode of FormatDiff.37//38// If diffUnknown, then produce a diff of the x and y values.39// If diffIdentical, then emit values as if they were equal.40// If diffRemoved, then only emit x values (ignoring y values).41// If diffInserted, then only emit y values (ignoring x values).42DiffMode diffMode4344// TypeMode controls whether to print the type for the current node.45//46// As a general rule of thumb, we always print the type of the next node47// after an interface, and always elide the type of the next node after48// a slice or map node.49TypeMode typeMode5051// formatValueOptions are options specific to printing reflect.Values.52formatValueOptions53}5455func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {56opts.DiffMode = d57return opts58}59func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {60opts.TypeMode = t61return opts62}63func (opts formatOptions) WithVerbosity(level int) formatOptions {64opts.VerbosityLevel = level65opts.LimitVerbosity = true66return opts67}68func (opts formatOptions) verbosity() uint {69switch {70case opts.VerbosityLevel < 0:71return 072case opts.VerbosityLevel > 16:73return 16 // some reasonable maximum to avoid shift overflow74default:75return uint(opts.VerbosityLevel)76}77}7879const maxVerbosityPreset = 68081// verbosityPreset modifies the verbosity settings given an index82// between 0 and maxVerbosityPreset, inclusive.83func verbosityPreset(opts formatOptions, i int) formatOptions {84opts.VerbosityLevel = int(opts.verbosity()) + 2*i85if i > 0 {86opts.AvoidStringer = true87}88if i >= maxVerbosityPreset {89opts.PrintAddresses = true90opts.QualifiedNames = true91}92return opts93}9495// FormatDiff converts a valueNode tree into a textNode tree, where the later96// is a textual representation of the differences detected in the former.97func (opts formatOptions) FormatDiff(v *valueNode, ptrs *pointerReferences) (out textNode) {98if opts.DiffMode == diffIdentical {99opts = opts.WithVerbosity(1)100} else if opts.verbosity() < 3 {101opts = opts.WithVerbosity(3)102}103104// Check whether we have specialized formatting for this node.105// This is not necessary, but helpful for producing more readable outputs.106if opts.CanFormatDiffSlice(v) {107return opts.FormatDiffSlice(v)108}109110var parentKind reflect.Kind111if v.parent != nil && v.parent.TransformerName == "" {112parentKind = v.parent.Type.Kind()113}114115// For leaf nodes, format the value based on the reflect.Values alone.116// As a special case, treat equal []byte as a leaf nodes.117isBytes := v.Type.Kind() == reflect.Slice && v.Type.Elem() == byteType118isEqualBytes := isBytes && v.NumDiff+v.NumIgnored+v.NumTransformed == 0119if v.MaxDepth == 0 || isEqualBytes {120switch opts.DiffMode {121case diffUnknown, diffIdentical:122// Format Equal.123if v.NumDiff == 0 {124outx := opts.FormatValue(v.ValueX, parentKind, ptrs)125outy := opts.FormatValue(v.ValueY, parentKind, ptrs)126if v.NumIgnored > 0 && v.NumSame == 0 {127return textEllipsis128} else if outx.Len() < outy.Len() {129return outx130} else {131return outy132}133}134135// Format unequal.136assert(opts.DiffMode == diffUnknown)137var list textList138outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, parentKind, ptrs)139outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, parentKind, ptrs)140for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {141opts2 := verbosityPreset(opts, i).WithTypeMode(elideType)142outx = opts2.FormatValue(v.ValueX, parentKind, ptrs)143outy = opts2.FormatValue(v.ValueY, parentKind, ptrs)144}145if outx != nil {146list = append(list, textRecord{Diff: '-', Value: outx})147}148if outy != nil {149list = append(list, textRecord{Diff: '+', Value: outy})150}151return opts.WithTypeMode(emitType).FormatType(v.Type, list)152case diffRemoved:153return opts.FormatValue(v.ValueX, parentKind, ptrs)154case diffInserted:155return opts.FormatValue(v.ValueY, parentKind, ptrs)156default:157panic("invalid diff mode")158}159}160161// Register slice element to support cycle detection.162if parentKind == reflect.Slice {163ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, true)164defer ptrs.Pop()165defer func() { out = wrapTrunkReferences(ptrRefs, out) }()166}167168// Descend into the child value node.169if v.TransformerName != "" {170out := opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)171out = &textWrap{Prefix: "Inverse(" + v.TransformerName + ", ", Value: out, Suffix: ")"}172return opts.FormatType(v.Type, out)173} else {174switch k := v.Type.Kind(); k {175case reflect.Struct, reflect.Array, reflect.Slice:176out = opts.formatDiffList(v.Records, k, ptrs)177out = opts.FormatType(v.Type, out)178case reflect.Map:179// Register map to support cycle detection.180ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)181defer ptrs.Pop()182183out = opts.formatDiffList(v.Records, k, ptrs)184out = wrapTrunkReferences(ptrRefs, out)185out = opts.FormatType(v.Type, out)186case reflect.Ptr:187// Register pointer to support cycle detection.188ptrRefs := ptrs.PushPair(v.ValueX, v.ValueY, opts.DiffMode, false)189defer ptrs.Pop()190191out = opts.FormatDiff(v.Value, ptrs)192out = wrapTrunkReferences(ptrRefs, out)193out = &textWrap{Prefix: "&", Value: out}194case reflect.Interface:195out = opts.WithTypeMode(emitType).FormatDiff(v.Value, ptrs)196default:197panic(fmt.Sprintf("%v cannot have children", k))198}199return out200}201}202203func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind, ptrs *pointerReferences) textNode {204// Derive record name based on the data structure kind.205var name string206var formatKey func(reflect.Value) string207switch k {208case reflect.Struct:209name = "field"210opts = opts.WithTypeMode(autoType)211formatKey = func(v reflect.Value) string { return v.String() }212case reflect.Slice, reflect.Array:213name = "element"214opts = opts.WithTypeMode(elideType)215formatKey = func(reflect.Value) string { return "" }216case reflect.Map:217name = "entry"218opts = opts.WithTypeMode(elideType)219formatKey = func(v reflect.Value) string { return formatMapKey(v, false, ptrs) }220}221222maxLen := -1223if opts.LimitVerbosity {224if opts.DiffMode == diffIdentical {225maxLen = ((1 << opts.verbosity()) >> 1) << 2 // 0, 4, 8, 16, 32, etc...226} else {227maxLen = (1 << opts.verbosity()) << 1 // 2, 4, 8, 16, 32, 64, etc...228}229opts.VerbosityLevel--230}231232// Handle unification.233switch opts.DiffMode {234case diffIdentical, diffRemoved, diffInserted:235var list textList236var deferredEllipsis bool // Add final "..." to indicate records were dropped237for _, r := range recs {238if len(list) == maxLen {239deferredEllipsis = true240break241}242243// Elide struct fields that are zero value.244if k == reflect.Struct {245var isZero bool246switch opts.DiffMode {247case diffIdentical:248isZero = r.Value.ValueX.IsZero() || r.Value.ValueY.IsZero()249case diffRemoved:250isZero = r.Value.ValueX.IsZero()251case diffInserted:252isZero = r.Value.ValueY.IsZero()253}254if isZero {255continue256}257}258// Elide ignored nodes.259if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {260deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)261if !deferredEllipsis {262list.AppendEllipsis(diffStats{})263}264continue265}266if out := opts.FormatDiff(r.Value, ptrs); out != nil {267list = append(list, textRecord{Key: formatKey(r.Key), Value: out})268}269}270if deferredEllipsis {271list.AppendEllipsis(diffStats{})272}273return &textWrap{Prefix: "{", Value: list, Suffix: "}"}274case diffUnknown:275default:276panic("invalid diff mode")277}278279// Handle differencing.280var numDiffs int281var list textList282var keys []reflect.Value // invariant: len(list) == len(keys)283groups := coalesceAdjacentRecords(name, recs)284maxGroup := diffStats{Name: name}285for i, ds := range groups {286if maxLen >= 0 && numDiffs >= maxLen {287maxGroup = maxGroup.Append(ds)288continue289}290291// Handle equal records.292if ds.NumDiff() == 0 {293// Compute the number of leading and trailing records to print.294var numLo, numHi int295numEqual := ds.NumIgnored + ds.NumIdentical296for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {297if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {298break299}300numLo++301}302for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {303if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {304break305}306numHi++307}308if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {309numHi++ // Avoid pointless coalescing of a single equal record310}311312// Format the equal values.313for _, r := range recs[:numLo] {314out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)315list = append(list, textRecord{Key: formatKey(r.Key), Value: out})316keys = append(keys, r.Key)317}318if numEqual > numLo+numHi {319ds.NumIdentical -= numLo + numHi320list.AppendEllipsis(ds)321for len(keys) < len(list) {322keys = append(keys, reflect.Value{})323}324}325for _, r := range recs[numEqual-numHi : numEqual] {326out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value, ptrs)327list = append(list, textRecord{Key: formatKey(r.Key), Value: out})328keys = append(keys, r.Key)329}330recs = recs[numEqual:]331continue332}333334// Handle unequal records.335for _, r := range recs[:ds.NumDiff()] {336switch {337case opts.CanFormatDiffSlice(r.Value):338out := opts.FormatDiffSlice(r.Value)339list = append(list, textRecord{Key: formatKey(r.Key), Value: out})340keys = append(keys, r.Key)341case r.Value.NumChildren == r.Value.MaxDepth:342outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)343outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)344for i := 0; i <= maxVerbosityPreset && outx != nil && outy != nil && outx.Equal(outy); i++ {345opts2 := verbosityPreset(opts, i)346outx = opts2.WithDiffMode(diffRemoved).FormatDiff(r.Value, ptrs)347outy = opts2.WithDiffMode(diffInserted).FormatDiff(r.Value, ptrs)348}349if outx != nil {350list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})351keys = append(keys, r.Key)352}353if outy != nil {354list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})355keys = append(keys, r.Key)356}357default:358out := opts.FormatDiff(r.Value, ptrs)359list = append(list, textRecord{Key: formatKey(r.Key), Value: out})360keys = append(keys, r.Key)361}362}363recs = recs[ds.NumDiff():]364numDiffs += ds.NumDiff()365}366if maxGroup.IsZero() {367assert(len(recs) == 0)368} else {369list.AppendEllipsis(maxGroup)370for len(keys) < len(list) {371keys = append(keys, reflect.Value{})372}373}374assert(len(list) == len(keys))375376// For maps, the default formatting logic uses fmt.Stringer which may377// produce ambiguous output. Avoid calling String to disambiguate.378if k == reflect.Map {379var ambiguous bool380seenKeys := map[string]reflect.Value{}381for i, currKey := range keys {382if currKey.IsValid() {383strKey := list[i].Key384prevKey, seen := seenKeys[strKey]385if seen && prevKey.CanInterface() && currKey.CanInterface() {386ambiguous = prevKey.Interface() != currKey.Interface()387if ambiguous {388break389}390}391seenKeys[strKey] = currKey392}393}394if ambiguous {395for i, k := range keys {396if k.IsValid() {397list[i].Key = formatMapKey(k, true, ptrs)398}399}400}401}402403return &textWrap{Prefix: "{", Value: list, Suffix: "}"}404}405406// coalesceAdjacentRecords coalesces the list of records into groups of407// adjacent equal, or unequal counts.408func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {409var prevCase int // Arbitrary index into which case last occurred410lastStats := func(i int) *diffStats {411if prevCase != i {412groups = append(groups, diffStats{Name: name})413prevCase = i414}415return &groups[len(groups)-1]416}417for _, r := range recs {418switch rv := r.Value; {419case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:420lastStats(1).NumIgnored++421case rv.NumDiff == 0:422lastStats(1).NumIdentical++423case rv.NumDiff > 0 && !rv.ValueY.IsValid():424lastStats(2).NumRemoved++425case rv.NumDiff > 0 && !rv.ValueX.IsValid():426lastStats(2).NumInserted++427default:428lastStats(2).NumModified++429}430}431return groups432}433434435