report_text.go 10 KB

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