123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- // Copyright 2019, 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"
- "github.com/google/go-cmp/cmp/internal/value"
- )
- // TODO: Enforce limits?
- // * Enforce maximum number of records to print per node?
- // * Enforce maximum size in bytes allowed?
- // * As a heuristic, use less verbosity for equal nodes than unequal nodes.
- // TODO: Enforce unique outputs?
- // * Avoid Stringer methods if it results in same output?
- // * Print pointer address if outputs still equal?
- // numContextRecords is the number of surrounding equal records to print.
- const numContextRecords = 2
- type diffMode byte
- const (
- diffUnknown diffMode = 0
- diffIdentical diffMode = ' '
- diffRemoved diffMode = '-'
- diffInserted diffMode = '+'
- )
- type typeMode int
- const (
- // emitType always prints the type.
- emitType typeMode = iota
- // elideType never prints the type.
- elideType
- // autoType prints the type only for composite kinds
- // (i.e., structs, slices, arrays, and maps).
- autoType
- )
- type formatOptions struct {
- // DiffMode controls the output mode of FormatDiff.
- //
- // If diffUnknown, then produce a diff of the x and y values.
- // If diffIdentical, then emit values as if they were equal.
- // If diffRemoved, then only emit x values (ignoring y values).
- // If diffInserted, then only emit y values (ignoring x values).
- DiffMode diffMode
- // TypeMode controls whether to print the type for the current node.
- //
- // As a general rule of thumb, we always print the type of the next node
- // after an interface, and always elide the type of the next node after
- // a slice or map node.
- TypeMode typeMode
- // formatValueOptions are options specific to printing reflect.Values.
- formatValueOptions
- }
- func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
- opts.DiffMode = d
- return opts
- }
- func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
- opts.TypeMode = t
- return opts
- }
- // 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 {
- // Check whether we have specialized formatting for this node.
- // This is not necessary, but helpful for producing more readable outputs.
- if opts.CanFormatDiffSlice(v) {
- return opts.FormatDiffSlice(v)
- }
- // For leaf nodes, format the value based on the reflect.Values alone.
- if v.MaxDepth == 0 {
- switch opts.DiffMode {
- case diffUnknown, diffIdentical:
- // Format Equal.
- if v.NumDiff == 0 {
- outx := opts.FormatValue(v.ValueX, visitedPointers{})
- outy := opts.FormatValue(v.ValueY, visitedPointers{})
- if v.NumIgnored > 0 && v.NumSame == 0 {
- return textEllipsis
- } else if outx.Len() < outy.Len() {
- return outx
- } else {
- return outy
- }
- }
- // Format unequal.
- assert(opts.DiffMode == diffUnknown)
- var list textList
- outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{})
- outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{})
- if outx != nil {
- list = append(list, textRecord{Diff: '-', Value: outx})
- }
- if outy != nil {
- list = append(list, textRecord{Diff: '+', Value: outy})
- }
- return opts.WithTypeMode(emitType).FormatType(v.Type, list)
- case diffRemoved:
- return opts.FormatValue(v.ValueX, visitedPointers{})
- case diffInserted:
- return opts.FormatValue(v.ValueY, visitedPointers{})
- default:
- panic("invalid diff mode")
- }
- }
- // Descend into the child value node.
- if v.TransformerName != "" {
- out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
- out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
- 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.Ptr:
- return textWrap{"&", opts.FormatDiff(v.Value), ""}
- case reflect.Interface:
- return opts.WithTypeMode(emitType).FormatDiff(v.Value)
- default:
- panic(fmt.Sprintf("%v cannot have children", k))
- }
- }
- }
- func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
- // Derive record name based on the data structure kind.
- var name string
- var formatKey func(reflect.Value) string
- switch k {
- case reflect.Struct:
- name = "field"
- opts = opts.WithTypeMode(autoType)
- formatKey = func(v reflect.Value) string { return v.String() }
- case reflect.Slice, reflect.Array:
- name = "element"
- opts = opts.WithTypeMode(elideType)
- formatKey = func(reflect.Value) string { return "" }
- case reflect.Map:
- name = "entry"
- opts = opts.WithTypeMode(elideType)
- formatKey = formatMapKey
- }
- // Handle unification.
- switch opts.DiffMode {
- case diffIdentical, diffRemoved, diffInserted:
- var list textList
- var deferredEllipsis bool // Add final "..." to indicate records were dropped
- for _, r := range recs {
- // Elide struct fields that are zero value.
- if k == reflect.Struct {
- var isZero bool
- switch opts.DiffMode {
- case diffIdentical:
- isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueY)
- case diffRemoved:
- isZero = value.IsZero(r.Value.ValueX)
- case diffInserted:
- isZero = value.IsZero(r.Value.ValueY)
- }
- if isZero {
- continue
- }
- }
- // Elide ignored nodes.
- if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
- deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
- if !deferredEllipsis {
- list.AppendEllipsis(diffStats{})
- }
- continue
- }
- if out := opts.FormatDiff(r.Value); out != nil {
- list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
- }
- }
- if deferredEllipsis {
- list.AppendEllipsis(diffStats{})
- }
- return textWrap{"{", list, "}"}
- case diffUnknown:
- default:
- panic("invalid diff mode")
- }
- // Handle differencing.
- var list textList
- groups := coalesceAdjacentRecords(name, recs)
- for i, ds := range groups {
- // Handle equal records.
- if ds.NumDiff() == 0 {
- // Compute the number of leading and trailing records to print.
- var numLo, numHi int
- numEqual := ds.NumIgnored + ds.NumIdentical
- for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
- if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
- break
- }
- numLo++
- }
- for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
- if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
- break
- }
- numHi++
- }
- if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
- numHi++ // Avoid pointless coalescing of a single equal record
- }
- // Format the equal values.
- for _, r := range recs[:numLo] {
- out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
- list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
- }
- if numEqual > numLo+numHi {
- ds.NumIdentical -= numLo + numHi
- list.AppendEllipsis(ds)
- }
- for _, r := range recs[numEqual-numHi : numEqual] {
- out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
- list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
- }
- recs = recs[numEqual:]
- continue
- }
- // Handle unequal records.
- for _, r := range recs[:ds.NumDiff()] {
- switch {
- case opts.CanFormatDiffSlice(r.Value):
- out := opts.FormatDiffSlice(r.Value)
- list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
- case r.Value.NumChildren == r.Value.MaxDepth:
- outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
- outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
- if outx != nil {
- list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
- }
- if outy != nil {
- list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
- }
- default:
- out := opts.FormatDiff(r.Value)
- list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
- }
- }
- recs = recs[ds.NumDiff():]
- }
- assert(len(recs) == 0)
- return textWrap{"{", list, "}"}
- }
- // coalesceAdjacentRecords coalesces the list of records into groups of
- // adjacent equal, or unequal counts.
- func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
- var prevCase int // Arbitrary index into which case last occurred
- lastStats := func(i int) *diffStats {
- if prevCase != i {
- groups = append(groups, diffStats{Name: name})
- prevCase = i
- }
- return &groups[len(groups)-1]
- }
- for _, r := range recs {
- switch rv := r.Value; {
- case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
- lastStats(1).NumIgnored++
- case rv.NumDiff == 0:
- lastStats(1).NumIdentical++
- case rv.NumDiff > 0 && !rv.ValueY.IsValid():
- lastStats(2).NumRemoved++
- case rv.NumDiff > 0 && !rv.ValueX.IsValid():
- lastStats(2).NumInserted++
- default:
- lastStats(2).NumModified++
- }
- }
- return groups
- }
|