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