432 lines
12 KiB
Go
432 lines
12 KiB
Go
|
// 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 (
|
|||
|
"bytes"
|
|||
|
"fmt"
|
|||
|
"math/rand"
|
|||
|
"strings"
|
|||
|
"time"
|
|||
|
"unicode/utf8"
|
|||
|
|
|||
|
"github.com/google/go-cmp/cmp/internal/flags"
|
|||
|
)
|
|||
|
|
|||
|
var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
|
|||
|
|
|||
|
const maxColumnLength = 80
|
|||
|
|
|||
|
type indentMode int
|
|||
|
|
|||
|
func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
|
|||
|
// The output of Diff is documented as being unstable to provide future
|
|||
|
// flexibility in changing the output for more humanly readable reports.
|
|||
|
// This logic intentionally introduces instability to the exact output
|
|||
|
// so that users can detect accidental reliance on stability early on,
|
|||
|
// rather than much later when an actual change to the format occurs.
|
|||
|
if flags.Deterministic || randBool {
|
|||
|
// Use regular spaces (U+0020).
|
|||
|
switch d {
|
|||
|
case diffUnknown, diffIdentical:
|
|||
|
b = append(b, " "...)
|
|||
|
case diffRemoved:
|
|||
|
b = append(b, "- "...)
|
|||
|
case diffInserted:
|
|||
|
b = append(b, "+ "...)
|
|||
|
}
|
|||
|
} else {
|
|||
|
// Use non-breaking spaces (U+00a0).
|
|||
|
switch d {
|
|||
|
case diffUnknown, diffIdentical:
|
|||
|
b = append(b, " "...)
|
|||
|
case diffRemoved:
|
|||
|
b = append(b, "- "...)
|
|||
|
case diffInserted:
|
|||
|
b = append(b, "+ "...)
|
|||
|
}
|
|||
|
}
|
|||
|
return repeatCount(n).appendChar(b, '\t')
|
|||
|
}
|
|||
|
|
|||
|
type repeatCount int
|
|||
|
|
|||
|
func (n repeatCount) appendChar(b []byte, c byte) []byte {
|
|||
|
for ; n > 0; n-- {
|
|||
|
b = append(b, c)
|
|||
|
}
|
|||
|
return b
|
|||
|
}
|
|||
|
|
|||
|
// textNode is a simplified tree-based representation of structured text.
|
|||
|
// Possible node types are textWrap, textList, or textLine.
|
|||
|
type textNode interface {
|
|||
|
// Len reports the length in bytes of a single-line version of the tree.
|
|||
|
// Nested textRecord.Diff and textRecord.Comment fields are ignored.
|
|||
|
Len() int
|
|||
|
// Equal reports whether the two trees are structurally identical.
|
|||
|
// Nested textRecord.Diff and textRecord.Comment fields are compared.
|
|||
|
Equal(textNode) bool
|
|||
|
// String returns the string representation of the text tree.
|
|||
|
// It is not guaranteed that len(x.String()) == x.Len(),
|
|||
|
// nor that x.String() == y.String() implies that x.Equal(y).
|
|||
|
String() string
|
|||
|
|
|||
|
// formatCompactTo formats the contents of the tree as a single-line string
|
|||
|
// to the provided buffer. Any nested textRecord.Diff and textRecord.Comment
|
|||
|
// fields are ignored.
|
|||
|
//
|
|||
|
// However, not all nodes in the tree should be collapsed as a single-line.
|
|||
|
// If a node can be collapsed as a single-line, it is replaced by a textLine
|
|||
|
// node. Since the top-level node cannot replace itself, this also returns
|
|||
|
// the current node itself.
|
|||
|
//
|
|||
|
// This does not mutate the receiver.
|
|||
|
formatCompactTo([]byte, diffMode) ([]byte, textNode)
|
|||
|
// formatExpandedTo formats the contents of the tree as a multi-line string
|
|||
|
// to the provided buffer. In order for column alignment to operate well,
|
|||
|
// formatCompactTo must be called before calling formatExpandedTo.
|
|||
|
formatExpandedTo([]byte, diffMode, indentMode) []byte
|
|||
|
}
|
|||
|
|
|||
|
// textWrap is a wrapper that concatenates a prefix and/or a suffix
|
|||
|
// to the underlying node.
|
|||
|
type textWrap struct {
|
|||
|
Prefix string // e.g., "bytes.Buffer{"
|
|||
|
Value textNode // textWrap | textList | textLine
|
|||
|
Suffix string // e.g., "}"
|
|||
|
Metadata interface{} // arbitrary metadata; has no effect on formatting
|
|||
|
}
|
|||
|
|
|||
|
func (s *textWrap) Len() int {
|
|||
|
return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
|
|||
|
}
|
|||
|
func (s1 *textWrap) Equal(s2 textNode) bool {
|
|||
|
if s2, ok := s2.(*textWrap); ok {
|
|||
|
return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
func (s *textWrap) String() string {
|
|||
|
var d diffMode
|
|||
|
var n indentMode
|
|||
|
_, s2 := s.formatCompactTo(nil, d)
|
|||
|
b := n.appendIndent(nil, d) // Leading indent
|
|||
|
b = s2.formatExpandedTo(b, d, n) // Main body
|
|||
|
b = append(b, '\n') // Trailing newline
|
|||
|
return string(b)
|
|||
|
}
|
|||
|
func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
|||
|
n0 := len(b) // Original buffer length
|
|||
|
b = append(b, s.Prefix...)
|
|||
|
b, s.Value = s.Value.formatCompactTo(b, d)
|
|||
|
b = append(b, s.Suffix...)
|
|||
|
if _, ok := s.Value.(textLine); ok {
|
|||
|
return b, textLine(b[n0:])
|
|||
|
}
|
|||
|
return b, s
|
|||
|
}
|
|||
|
func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
|
|||
|
b = append(b, s.Prefix...)
|
|||
|
b = s.Value.formatExpandedTo(b, d, n)
|
|||
|
b = append(b, s.Suffix...)
|
|||
|
return b
|
|||
|
}
|
|||
|
|
|||
|
// textList is a comma-separated list of textWrap or textLine nodes.
|
|||
|
// The list may be formatted as multi-lines or single-line at the discretion
|
|||
|
// of the textList.formatCompactTo method.
|
|||
|
type textList []textRecord
|
|||
|
type textRecord struct {
|
|||
|
Diff diffMode // e.g., 0 or '-' or '+'
|
|||
|
Key string // e.g., "MyField"
|
|||
|
Value textNode // textWrap | textLine
|
|||
|
ElideComma bool // avoid trailing comma
|
|||
|
Comment fmt.Stringer // e.g., "6 identical fields"
|
|||
|
}
|
|||
|
|
|||
|
// AppendEllipsis appends a new ellipsis node to the list if none already
|
|||
|
// exists at the end. If cs is non-zero it coalesces the statistics with the
|
|||
|
// previous diffStats.
|
|||
|
func (s *textList) AppendEllipsis(ds diffStats) {
|
|||
|
hasStats := !ds.IsZero()
|
|||
|
if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
|
|||
|
if hasStats {
|
|||
|
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})
|
|||
|
} else {
|
|||
|
*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
if hasStats {
|
|||
|
(*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func (s textList) Len() (n int) {
|
|||
|
for i, r := range s {
|
|||
|
n += len(r.Key)
|
|||
|
if r.Key != "" {
|
|||
|
n += len(": ")
|
|||
|
}
|
|||
|
n += r.Value.Len()
|
|||
|
if i < len(s)-1 {
|
|||
|
n += len(", ")
|
|||
|
}
|
|||
|
}
|
|||
|
return n
|
|||
|
}
|
|||
|
|
|||
|
func (s1 textList) Equal(s2 textNode) bool {
|
|||
|
if s2, ok := s2.(textList); ok {
|
|||
|
if len(s1) != len(s2) {
|
|||
|
return false
|
|||
|
}
|
|||
|
for i := range s1 {
|
|||
|
r1, r2 := s1[i], s2[i]
|
|||
|
if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
return true
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
func (s textList) String() string {
|
|||
|
return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()
|
|||
|
}
|
|||
|
|
|||
|
func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
|||
|
s = append(textList(nil), s...) // Avoid mutating original
|
|||
|
|
|||
|
// Determine whether we can collapse this list as a single line.
|
|||
|
n0 := len(b) // Original buffer length
|
|||
|
var multiLine bool
|
|||
|
for i, r := range s {
|
|||
|
if r.Diff == diffInserted || r.Diff == diffRemoved {
|
|||
|
multiLine = true
|
|||
|
}
|
|||
|
b = append(b, r.Key...)
|
|||
|
if r.Key != "" {
|
|||
|
b = append(b, ": "...)
|
|||
|
}
|
|||
|
b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)
|
|||
|
if _, ok := s[i].Value.(textLine); !ok {
|
|||
|
multiLine = true
|
|||
|
}
|
|||
|
if r.Comment != nil {
|
|||
|
multiLine = true
|
|||
|
}
|
|||
|
if i < len(s)-1 {
|
|||
|
b = append(b, ", "...)
|
|||
|
}
|
|||
|
}
|
|||
|
// Force multi-lined output when printing a removed/inserted node that
|
|||
|
// is sufficiently long.
|
|||
|
if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {
|
|||
|
multiLine = true
|
|||
|
}
|
|||
|
if !multiLine {
|
|||
|
return b, textLine(b[n0:])
|
|||
|
}
|
|||
|
return b, s
|
|||
|
}
|
|||
|
|
|||
|
func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
|
|||
|
alignKeyLens := s.alignLens(
|
|||
|
func(r textRecord) bool {
|
|||
|
_, isLine := r.Value.(textLine)
|
|||
|
return r.Key == "" || !isLine
|
|||
|
},
|
|||
|
func(r textRecord) int { return utf8.RuneCountInString(r.Key) },
|
|||
|
)
|
|||
|
alignValueLens := s.alignLens(
|
|||
|
func(r textRecord) bool {
|
|||
|
_, isLine := r.Value.(textLine)
|
|||
|
return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
|
|||
|
},
|
|||
|
func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },
|
|||
|
)
|
|||
|
|
|||
|
// Format lists of simple lists in a batched form.
|
|||
|
// If the list is sequence of only textLine values,
|
|||
|
// then batch multiple values on a single line.
|
|||
|
var isSimple bool
|
|||
|
for _, r := range s {
|
|||
|
_, isLine := r.Value.(textLine)
|
|||
|
isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil
|
|||
|
if !isSimple {
|
|||
|
break
|
|||
|
}
|
|||
|
}
|
|||
|
if isSimple {
|
|||
|
n++
|
|||
|
var batch []byte
|
|||
|
emitBatch := func() {
|
|||
|
if len(batch) > 0 {
|
|||
|
b = n.appendIndent(append(b, '\n'), d)
|
|||
|
b = append(b, bytes.TrimRight(batch, " ")...)
|
|||
|
batch = batch[:0]
|
|||
|
}
|
|||
|
}
|
|||
|
for _, r := range s {
|
|||
|
line := r.Value.(textLine)
|
|||
|
if len(batch)+len(line)+len(", ") > maxColumnLength {
|
|||
|
emitBatch()
|
|||
|
}
|
|||
|
batch = append(batch, line...)
|
|||
|
batch = append(batch, ", "...)
|
|||
|
}
|
|||
|
emitBatch()
|
|||
|
n--
|
|||
|
return n.appendIndent(append(b, '\n'), d)
|
|||
|
}
|
|||
|
|
|||
|
// Format the list as a multi-lined output.
|
|||
|
n++
|
|||
|
for i, r := range s {
|
|||
|
b = n.appendIndent(append(b, '\n'), d|r.Diff)
|
|||
|
if r.Key != "" {
|
|||
|
b = append(b, r.Key+": "...)
|
|||
|
}
|
|||
|
b = alignKeyLens[i].appendChar(b, ' ')
|
|||
|
|
|||
|
b = r.Value.formatExpandedTo(b, d|r.Diff, n)
|
|||
|
if !r.ElideComma {
|
|||
|
b = append(b, ',')
|
|||
|
}
|
|||
|
b = alignValueLens[i].appendChar(b, ' ')
|
|||
|
|
|||
|
if r.Comment != nil {
|
|||
|
b = append(b, " // "+r.Comment.String()...)
|
|||
|
}
|
|||
|
}
|
|||
|
n--
|
|||
|
|
|||
|
return n.appendIndent(append(b, '\n'), d)
|
|||
|
}
|
|||
|
|
|||
|
func (s textList) alignLens(
|
|||
|
skipFunc func(textRecord) bool,
|
|||
|
lenFunc func(textRecord) int,
|
|||
|
) []repeatCount {
|
|||
|
var startIdx, endIdx, maxLen int
|
|||
|
lens := make([]repeatCount, len(s))
|
|||
|
for i, r := range s {
|
|||
|
if skipFunc(r) {
|
|||
|
for j := startIdx; j < endIdx && j < len(s); j++ {
|
|||
|
lens[j] = repeatCount(maxLen - lenFunc(s[j]))
|
|||
|
}
|
|||
|
startIdx, endIdx, maxLen = i+1, i+1, 0
|
|||
|
} else {
|
|||
|
if maxLen < lenFunc(r) {
|
|||
|
maxLen = lenFunc(r)
|
|||
|
}
|
|||
|
endIdx = i + 1
|
|||
|
}
|
|||
|
}
|
|||
|
for j := startIdx; j < endIdx && j < len(s); j++ {
|
|||
|
lens[j] = repeatCount(maxLen - lenFunc(s[j]))
|
|||
|
}
|
|||
|
return lens
|
|||
|
}
|
|||
|
|
|||
|
// textLine is a single-line segment of text and is always a leaf node
|
|||
|
// in the textNode tree.
|
|||
|
type textLine []byte
|
|||
|
|
|||
|
var (
|
|||
|
textNil = textLine("nil")
|
|||
|
textEllipsis = textLine("...")
|
|||
|
)
|
|||
|
|
|||
|
func (s textLine) Len() int {
|
|||
|
return len(s)
|
|||
|
}
|
|||
|
func (s1 textLine) Equal(s2 textNode) bool {
|
|||
|
if s2, ok := s2.(textLine); ok {
|
|||
|
return bytes.Equal([]byte(s1), []byte(s2))
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
func (s textLine) String() string {
|
|||
|
return string(s)
|
|||
|
}
|
|||
|
func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
|
|||
|
return append(b, s...), s
|
|||
|
}
|
|||
|
func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {
|
|||
|
return append(b, s...)
|
|||
|
}
|
|||
|
|
|||
|
type diffStats struct {
|
|||
|
Name string
|
|||
|
NumIgnored int
|
|||
|
NumIdentical int
|
|||
|
NumRemoved int
|
|||
|
NumInserted int
|
|||
|
NumModified int
|
|||
|
}
|
|||
|
|
|||
|
func (s diffStats) IsZero() bool {
|
|||
|
s.Name = ""
|
|||
|
return s == diffStats{}
|
|||
|
}
|
|||
|
|
|||
|
func (s diffStats) NumDiff() int {
|
|||
|
return s.NumRemoved + s.NumInserted + s.NumModified
|
|||
|
}
|
|||
|
|
|||
|
func (s diffStats) Append(ds diffStats) diffStats {
|
|||
|
assert(s.Name == ds.Name)
|
|||
|
s.NumIgnored += ds.NumIgnored
|
|||
|
s.NumIdentical += ds.NumIdentical
|
|||
|
s.NumRemoved += ds.NumRemoved
|
|||
|
s.NumInserted += ds.NumInserted
|
|||
|
s.NumModified += ds.NumModified
|
|||
|
return s
|
|||
|
}
|
|||
|
|
|||
|
// String prints a humanly-readable summary of coalesced records.
|
|||
|
//
|
|||
|
// Example:
|
|||
|
// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
|
|||
|
func (s diffStats) String() string {
|
|||
|
var ss []string
|
|||
|
var sum int
|
|||
|
labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}
|
|||
|
counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}
|
|||
|
for i, n := range counts {
|
|||
|
if n > 0 {
|
|||
|
ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))
|
|||
|
}
|
|||
|
sum += n
|
|||
|
}
|
|||
|
|
|||
|
// Pluralize the name (adjusting for some obscure English grammar rules).
|
|||
|
name := s.Name
|
|||
|
if sum > 1 {
|
|||
|
name += "s"
|
|||
|
if strings.HasSuffix(name, "ys") {
|
|||
|
name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Format the list according to English grammar (with Oxford comma).
|
|||
|
switch n := len(ss); n {
|
|||
|
case 0:
|
|||
|
return ""
|
|||
|
case 1, 2:
|
|||
|
return strings.Join(ss, " and ") + " " + name
|
|||
|
default:
|
|||
|
return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type commentString string
|
|||
|
|
|||
|
func (s commentString) String() string { return string(s) }
|