// 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) }