report_text.go 11 KB

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