| // 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" |
| |
| "github.com/google/go-cmp/cmp/internal/flags" |
| ) |
| |
| var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0 |
| |
| 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., "}" |
| } |
| |
| 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 |
| 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 != diffStats{} |
| if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) { |
| if hasStats { |
| *s = append(*s, textRecord{Value: textEllipsis, Comment: ds}) |
| } else { |
| *s = append(*s, textRecord{Value: textEllipsis}) |
| } |
| 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{"{", s, "}"}.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:]) > 80 { |
| 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 len(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 len(r.Value.(textLine)) }, |
| ) |
| |
| // 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.Value.Equal(textEllipsis) { |
| 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) 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) } |