You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

383 lines
10 KiB

  1. // Copyright 2019, The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE.md file.
  4. package cmp
  5. import (
  6. "bytes"
  7. "fmt"
  8. "math/rand"
  9. "strings"
  10. "time"
  11. "github.com/google/go-cmp/cmp/internal/flags"
  12. )
  13. var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
  14. type indentMode int
  15. func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
  16. if flags.Deterministic || randBool {
  17. // Use regular spaces (U+0020).
  18. switch d {
  19. case diffUnknown, diffIdentical:
  20. b = append(b, " "...)
  21. case diffRemoved:
  22. b = append(b, "- "...)
  23. case diffInserted:
  24. b = append(b, "+ "...)
  25. }
  26. } else {
  27. // Use non-breaking spaces (U+00a0).
  28. switch d {
  29. case diffUnknown, diffIdentical:
  30. b = append(b, "  "...)
  31. case diffRemoved:
  32. b = append(b, "- "...)
  33. case diffInserted:
  34. b = append(b, "+ "...)
  35. }
  36. }
  37. return repeatCount(n).appendChar(b, '\t')
  38. }
  39. type repeatCount int
  40. func (n repeatCount) appendChar(b []byte, c byte) []byte {
  41. for ; n > 0; n-- {
  42. b = append(b, c)
  43. }
  44. return b
  45. }
  46. // textNode is a simplified tree-based representation of structured text.
  47. // Possible node types are textWrap, textList, or textLine.
  48. type textNode interface {
  49. // Len reports the length in bytes of a single-line version of the tree.
  50. // Nested textRecord.Diff and textRecord.Comment fields are ignored.
  51. Len() int
  52. // Equal reports whether the two trees are structurally identical.
  53. // Nested textRecord.Diff and textRecord.Comment fields are compared.
  54. Equal(textNode) bool
  55. // String returns the string representation of the text tree.
  56. // It is not guaranteed that len(x.String()) == x.Len(),
  57. // nor that x.String() == y.String() implies that x.Equal(y).
  58. String() string
  59. // formatCompactTo formats the contents of the tree as a single-line string
  60. // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment
  61. // fields are ignored.
  62. //
  63. // However, not all nodes in the tree should be collapsed as a single-line.
  64. // If a node can be collapsed as a single-line, it is replaced by a textLine
  65. // node. Since the top-level node cannot replace itself, this also returns
  66. // the current node itself.
  67. //
  68. // This does not mutate the receiver.
  69. formatCompactTo([]byte, diffMode) ([]byte, textNode)
  70. // formatExpandedTo formats the contents of the tree as a multi-line string
  71. // to the provided buffer. In order for column alignment to operate well,
  72. // formatCompactTo must be called before calling formatExpandedTo.
  73. formatExpandedTo([]byte, diffMode, indentMode) []byte
  74. }
  75. // textWrap is a wrapper that concatenates a prefix and/or a suffix
  76. // to the underlying node.
  77. type textWrap struct {
  78. Prefix string // e.g., "bytes.Buffer{"
  79. Value textNode // textWrap | textList | textLine
  80. Suffix string // e.g., "}"
  81. }
  82. func (s textWrap) Len() int {
  83. return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
  84. }
  85. func (s1 textWrap) Equal(s2 textNode) bool {
  86. if s2, ok := s2.(textWrap); ok {
  87. return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
  88. }
  89. return false
  90. }
  91. func (s textWrap) String() string {
  92. var d diffMode
  93. var n indentMode
  94. _, s2 := s.formatCompactTo(nil, d)
  95. b := n.appendIndent(nil, d) // Leading indent
  96. b = s2.formatExpandedTo(b, d, n) // Main body
  97. b = append(b, '\n') // Trailing newline
  98. return string(b)
  99. }
  100. func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
  101. n0 := len(b) // Original buffer length
  102. b = append(b, s.Prefix...)
  103. b, s.Value = s.Value.formatCompactTo(b, d)
  104. b = append(b, s.Suffix...)
  105. if _, ok := s.Value.(textLine); ok {
  106. return b, textLine(b[n0:])
  107. }
  108. return b, s
  109. }
  110. func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
  111. b = append(b, s.Prefix...)
  112. b = s.Value.formatExpandedTo(b, d, n)
  113. b = append(b, s.Suffix...)
  114. return b
  115. }
  116. // textList is a comma-separated list of textWrap or textLine nodes.
  117. // The list may be formatted as multi-lines or single-line at the discretion
  118. // of the textList.formatCompactTo method.
  119. type textList []textRecord
  120. type textRecord struct {
  121. Diff diffMode // e.g., 0 or '-' or '+'
  122. Key string // e.g., "MyField"
  123. Value textNode // textWrap | textLine
  124. Comment fmt.Stringer // e.g., "6 identical fields"
  125. }
  126. // AppendEllipsis appends a new ellipsis node to the list if none already
  127. // exists at the end. If cs is non-zero it coalesces the statistics with the
  128. // previous diffStats.
  129. func (s *textList) AppendEllipsis(ds diffStats) {
  130. hasStats := ds != diffStats{}
  131. if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
  132. if hasStats {
  133. *s = append(*s, textRecord{Value: textEllipsis, Comment: ds})
  134. } else {
  135. *s = append(*s, textRecord{Value: textEllipsis})
  136. }
  137. return
  138. }
  139. if hasStats {
  140. (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)
  141. }
  142. }
  143. func (s textList) Len() (n int) {
  144. for i, r := range s {
  145. n += len(r.Key)
  146. if r.Key != "" {
  147. n += len(": ")
  148. }
  149. n += r.Value.Len()
  150. if i < len(s)-1 {
  151. n += len(", ")
  152. }
  153. }
  154. return n
  155. }
  156. func (s1 textList) Equal(s2 textNode) bool {
  157. if s2, ok := s2.(textList); ok {
  158. if len(s1) != len(s2) {
  159. return false
  160. }
  161. for i := range s1 {
  162. r1, r2 := s1[i], s2[i]
  163. if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {
  164. return false
  165. }
  166. }
  167. return true
  168. }
  169. return false
  170. }
  171. func (s textList) String() string {
  172. return textWrap{"{", s, "}"}.String()
  173. }
  174. func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
  175. s = append(textList(nil), s...) // Avoid mutating original
  176. // Determine whether we can collapse this list as a single line.
  177. n0 := len(b) // Original buffer length
  178. var multiLine bool
  179. for i, r := range s {
  180. if r.Diff == diffInserted || r.Diff == diffRemoved {
  181. multiLine = true
  182. }
  183. b = append(b, r.Key...)
  184. if r.Key != "" {
  185. b = append(b, ": "...)
  186. }
  187. b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)
  188. if _, ok := s[i].Value.(textLine); !ok {
  189. multiLine = true
  190. }
  191. if r.Comment != nil {
  192. multiLine = true
  193. }
  194. if i < len(s)-1 {
  195. b = append(b, ", "...)
  196. }
  197. }
  198. // Force multi-lined output when printing a removed/inserted node that
  199. // is sufficiently long.
  200. if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 {
  201. multiLine = true
  202. }
  203. if !multiLine {
  204. return b, textLine(b[n0:])
  205. }
  206. return b, s
  207. }
  208. func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
  209. alignKeyLens := s.alignLens(
  210. func(r textRecord) bool {
  211. _, isLine := r.Value.(textLine)
  212. return r.Key == "" || !isLine
  213. },
  214. func(r textRecord) int { return len(r.Key) },
  215. )
  216. alignValueLens := s.alignLens(
  217. func(r textRecord) bool {
  218. _, isLine := r.Value.(textLine)
  219. return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
  220. },
  221. func(r textRecord) int { return len(r.Value.(textLine)) },
  222. )
  223. // Format the list as a multi-lined output.
  224. n++
  225. for i, r := range s {
  226. b = n.appendIndent(append(b, '\n'), d|r.Diff)
  227. if r.Key != "" {
  228. b = append(b, r.Key+": "...)
  229. }
  230. b = alignKeyLens[i].appendChar(b, ' ')
  231. b = r.Value.formatExpandedTo(b, d|r.Diff, n)
  232. if !r.Value.Equal(textEllipsis) {
  233. b = append(b, ',')
  234. }
  235. b = alignValueLens[i].appendChar(b, ' ')
  236. if r.Comment != nil {
  237. b = append(b, " // "+r.Comment.String()...)
  238. }
  239. }
  240. n--
  241. return n.appendIndent(append(b, '\n'), d)
  242. }
  243. func (s textList) alignLens(
  244. skipFunc func(textRecord) bool,
  245. lenFunc func(textRecord) int,
  246. ) []repeatCount {
  247. var startIdx, endIdx, maxLen int
  248. lens := make([]repeatCount, len(s))
  249. for i, r := range s {
  250. if skipFunc(r) {
  251. for j := startIdx; j < endIdx && j < len(s); j++ {
  252. lens[j] = repeatCount(maxLen - lenFunc(s[j]))
  253. }
  254. startIdx, endIdx, maxLen = i+1, i+1, 0
  255. } else {
  256. if maxLen < lenFunc(r) {
  257. maxLen = lenFunc(r)
  258. }
  259. endIdx = i + 1
  260. }
  261. }
  262. for j := startIdx; j < endIdx && j < len(s); j++ {
  263. lens[j] = repeatCount(maxLen - lenFunc(s[j]))
  264. }
  265. return lens
  266. }
  267. // textLine is a single-line segment of text and is always a leaf node
  268. // in the textNode tree.
  269. type textLine []byte
  270. var (
  271. textNil = textLine("nil")
  272. textEllipsis = textLine("...")
  273. )
  274. func (s textLine) Len() int {
  275. return len(s)
  276. }
  277. func (s1 textLine) Equal(s2 textNode) bool {
  278. if s2, ok := s2.(textLine); ok {
  279. return bytes.Equal([]byte(s1), []byte(s2))
  280. }
  281. return false
  282. }
  283. func (s textLine) String() string {
  284. return string(s)
  285. }
  286. func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
  287. return append(b, s...), s
  288. }
  289. func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {
  290. return append(b, s...)
  291. }
  292. type diffStats struct {
  293. Name string
  294. NumIgnored int
  295. NumIdentical int
  296. NumRemoved int
  297. NumInserted int
  298. NumModified int
  299. }
  300. func (s diffStats) NumDiff() int {
  301. return s.NumRemoved + s.NumInserted + s.NumModified
  302. }
  303. func (s diffStats) Append(ds diffStats) diffStats {
  304. assert(s.Name == ds.Name)
  305. s.NumIgnored += ds.NumIgnored
  306. s.NumIdentical += ds.NumIdentical
  307. s.NumRemoved += ds.NumRemoved
  308. s.NumInserted += ds.NumInserted
  309. s.NumModified += ds.NumModified
  310. return s
  311. }
  312. // String prints a humanly-readable summary of coalesced records.
  313. //
  314. // Example:
  315. // diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
  316. func (s diffStats) String() string {
  317. var ss []string
  318. var sum int
  319. labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}
  320. counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}
  321. for i, n := range counts {
  322. if n > 0 {
  323. ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))
  324. }
  325. sum += n
  326. }
  327. // Pluralize the name (adjusting for some obscure English grammar rules).
  328. name := s.Name
  329. if sum > 1 {
  330. name = name + "s"
  331. if strings.HasSuffix(name, "ys") {
  332. name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"
  333. }
  334. }
  335. // Format the list according to English grammar (with Oxford comma).
  336. switch n := len(ss); n {
  337. case 0:
  338. return ""
  339. case 1, 2:
  340. return strings.Join(ss, " and ") + " " + name
  341. default:
  342. return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name
  343. }
  344. }
  345. type commentString string
  346. func (s commentString) String() string { return string(s) }