Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

334 lignes
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. "reflect"
  9. "strings"
  10. "unicode"
  11. "unicode/utf8"
  12. "github.com/google/go-cmp/cmp/internal/diff"
  13. )
  14. // CanFormatDiffSlice reports whether we support custom formatting for nodes
  15. // that are slices of primitive kinds or strings.
  16. func (opts formatOptions) CanFormatDiffSlice(v *valueNode) bool {
  17. switch {
  18. case opts.DiffMode != diffUnknown:
  19. return false // Must be formatting in diff mode
  20. case v.NumDiff == 0:
  21. return false // No differences detected
  22. case v.NumIgnored+v.NumCompared+v.NumTransformed > 0:
  23. // TODO: Handle the case where someone uses bytes.Equal on a large slice.
  24. return false // Some custom option was used to determined equality
  25. case !v.ValueX.IsValid() || !v.ValueY.IsValid():
  26. return false // Both values must be valid
  27. }
  28. switch t := v.Type; t.Kind() {
  29. case reflect.String:
  30. case reflect.Array, reflect.Slice:
  31. // Only slices of primitive types have specialized handling.
  32. switch t.Elem().Kind() {
  33. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
  34. reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
  35. reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
  36. default:
  37. return false
  38. }
  39. // If a sufficient number of elements already differ,
  40. // use specialized formatting even if length requirement is not met.
  41. if v.NumDiff > v.NumSame {
  42. return true
  43. }
  44. default:
  45. return false
  46. }
  47. // Use specialized string diffing for longer slices or strings.
  48. const minLength = 64
  49. return v.ValueX.Len() >= minLength && v.ValueY.Len() >= minLength
  50. }
  51. // FormatDiffSlice prints a diff for the slices (or strings) represented by v.
  52. // This provides custom-tailored logic to make printing of differences in
  53. // textual strings and slices of primitive kinds more readable.
  54. func (opts formatOptions) FormatDiffSlice(v *valueNode) textNode {
  55. assert(opts.DiffMode == diffUnknown)
  56. t, vx, vy := v.Type, v.ValueX, v.ValueY
  57. // Auto-detect the type of the data.
  58. var isLinedText, isText, isBinary bool
  59. var sx, sy string
  60. switch {
  61. case t.Kind() == reflect.String:
  62. sx, sy = vx.String(), vy.String()
  63. isText = true // Initial estimate, verify later
  64. case t.Kind() == reflect.Slice && t.Elem() == reflect.TypeOf(byte(0)):
  65. sx, sy = string(vx.Bytes()), string(vy.Bytes())
  66. isBinary = true // Initial estimate, verify later
  67. case t.Kind() == reflect.Array:
  68. // Arrays need to be addressable for slice operations to work.
  69. vx2, vy2 := reflect.New(t).Elem(), reflect.New(t).Elem()
  70. vx2.Set(vx)
  71. vy2.Set(vy)
  72. vx, vy = vx2, vy2
  73. }
  74. if isText || isBinary {
  75. var numLines, lastLineIdx, maxLineLen int
  76. isBinary = false
  77. for i, r := range sx + sy {
  78. if !(unicode.IsPrint(r) || unicode.IsSpace(r)) || r == utf8.RuneError {
  79. isBinary = true
  80. break
  81. }
  82. if r == '\n' {
  83. if maxLineLen < i-lastLineIdx {
  84. lastLineIdx = i - lastLineIdx
  85. }
  86. lastLineIdx = i + 1
  87. numLines++
  88. }
  89. }
  90. isText = !isBinary
  91. isLinedText = isText && numLines >= 4 && maxLineLen <= 256
  92. }
  93. // Format the string into printable records.
  94. var list textList
  95. var delim string
  96. switch {
  97. // If the text appears to be multi-lined text,
  98. // then perform differencing across individual lines.
  99. case isLinedText:
  100. ssx := strings.Split(sx, "\n")
  101. ssy := strings.Split(sy, "\n")
  102. list = opts.formatDiffSlice(
  103. reflect.ValueOf(ssx), reflect.ValueOf(ssy), 1, "line",
  104. func(v reflect.Value, d diffMode) textRecord {
  105. s := formatString(v.Index(0).String())
  106. return textRecord{Diff: d, Value: textLine(s)}
  107. },
  108. )
  109. delim = "\n"
  110. // If the text appears to be single-lined text,
  111. // then perform differencing in approximately fixed-sized chunks.
  112. // The output is printed as quoted strings.
  113. case isText:
  114. list = opts.formatDiffSlice(
  115. reflect.ValueOf(sx), reflect.ValueOf(sy), 64, "byte",
  116. func(v reflect.Value, d diffMode) textRecord {
  117. s := formatString(v.String())
  118. return textRecord{Diff: d, Value: textLine(s)}
  119. },
  120. )
  121. delim = ""
  122. // If the text appears to be binary data,
  123. // then perform differencing in approximately fixed-sized chunks.
  124. // The output is inspired by hexdump.
  125. case isBinary:
  126. list = opts.formatDiffSlice(
  127. reflect.ValueOf(sx), reflect.ValueOf(sy), 16, "byte",
  128. func(v reflect.Value, d diffMode) textRecord {
  129. var ss []string
  130. for i := 0; i < v.Len(); i++ {
  131. ss = append(ss, formatHex(v.Index(i).Uint()))
  132. }
  133. s := strings.Join(ss, ", ")
  134. comment := commentString(fmt.Sprintf("%c|%v|", d, formatASCII(v.String())))
  135. return textRecord{Diff: d, Value: textLine(s), Comment: comment}
  136. },
  137. )
  138. // For all other slices of primitive types,
  139. // then perform differencing in approximately fixed-sized chunks.
  140. // The size of each chunk depends on the width of the element kind.
  141. default:
  142. var chunkSize int
  143. if t.Elem().Kind() == reflect.Bool {
  144. chunkSize = 16
  145. } else {
  146. switch t.Elem().Bits() {
  147. case 8:
  148. chunkSize = 16
  149. case 16:
  150. chunkSize = 12
  151. case 32:
  152. chunkSize = 8
  153. default:
  154. chunkSize = 8
  155. }
  156. }
  157. list = opts.formatDiffSlice(
  158. vx, vy, chunkSize, t.Elem().Kind().String(),
  159. func(v reflect.Value, d diffMode) textRecord {
  160. var ss []string
  161. for i := 0; i < v.Len(); i++ {
  162. switch t.Elem().Kind() {
  163. case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  164. ss = append(ss, fmt.Sprint(v.Index(i).Int()))
  165. case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
  166. ss = append(ss, formatHex(v.Index(i).Uint()))
  167. case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
  168. ss = append(ss, fmt.Sprint(v.Index(i).Interface()))
  169. }
  170. }
  171. s := strings.Join(ss, ", ")
  172. return textRecord{Diff: d, Value: textLine(s)}
  173. },
  174. )
  175. }
  176. // Wrap the output with appropriate type information.
  177. var out textNode = textWrap{"{", list, "}"}
  178. if !isText {
  179. // The "{...}" byte-sequence literal is not valid Go syntax for strings.
  180. // Emit the type for extra clarity (e.g. "string{...}").
  181. if t.Kind() == reflect.String {
  182. opts = opts.WithTypeMode(emitType)
  183. }
  184. return opts.FormatType(t, out)
  185. }
  186. switch t.Kind() {
  187. case reflect.String:
  188. out = textWrap{"strings.Join(", out, fmt.Sprintf(", %q)", delim)}
  189. if t != reflect.TypeOf(string("")) {
  190. out = opts.FormatType(t, out)
  191. }
  192. case reflect.Slice:
  193. out = textWrap{"bytes.Join(", out, fmt.Sprintf(", %q)", delim)}
  194. if t != reflect.TypeOf([]byte(nil)) {
  195. out = opts.FormatType(t, out)
  196. }
  197. }
  198. return out
  199. }
  200. // formatASCII formats s as an ASCII string.
  201. // This is useful for printing binary strings in a semi-legible way.
  202. func formatASCII(s string) string {
  203. b := bytes.Repeat([]byte{'.'}, len(s))
  204. for i := 0; i < len(s); i++ {
  205. if ' ' <= s[i] && s[i] <= '~' {
  206. b[i] = s[i]
  207. }
  208. }
  209. return string(b)
  210. }
  211. func (opts formatOptions) formatDiffSlice(
  212. vx, vy reflect.Value, chunkSize int, name string,
  213. makeRec func(reflect.Value, diffMode) textRecord,
  214. ) (list textList) {
  215. es := diff.Difference(vx.Len(), vy.Len(), func(ix int, iy int) diff.Result {
  216. return diff.BoolResult(vx.Index(ix).Interface() == vy.Index(iy).Interface())
  217. })
  218. appendChunks := func(v reflect.Value, d diffMode) int {
  219. n0 := v.Len()
  220. for v.Len() > 0 {
  221. n := chunkSize
  222. if n > v.Len() {
  223. n = v.Len()
  224. }
  225. list = append(list, makeRec(v.Slice(0, n), d))
  226. v = v.Slice(n, v.Len())
  227. }
  228. return n0 - v.Len()
  229. }
  230. groups := coalesceAdjacentEdits(name, es)
  231. groups = coalesceInterveningIdentical(groups, chunkSize/4)
  232. for i, ds := range groups {
  233. // Print equal.
  234. if ds.NumDiff() == 0 {
  235. // Compute the number of leading and trailing equal bytes to print.
  236. var numLo, numHi int
  237. numEqual := ds.NumIgnored + ds.NumIdentical
  238. for numLo < chunkSize*numContextRecords && numLo+numHi < numEqual && i != 0 {
  239. numLo++
  240. }
  241. for numHi < chunkSize*numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
  242. numHi++
  243. }
  244. if numEqual-(numLo+numHi) <= chunkSize && ds.NumIgnored == 0 {
  245. numHi = numEqual - numLo // Avoid pointless coalescing of single equal row
  246. }
  247. // Print the equal bytes.
  248. appendChunks(vx.Slice(0, numLo), diffIdentical)
  249. if numEqual > numLo+numHi {
  250. ds.NumIdentical -= numLo + numHi
  251. list.AppendEllipsis(ds)
  252. }
  253. appendChunks(vx.Slice(numEqual-numHi, numEqual), diffIdentical)
  254. vx = vx.Slice(numEqual, vx.Len())
  255. vy = vy.Slice(numEqual, vy.Len())
  256. continue
  257. }
  258. // Print unequal.
  259. nx := appendChunks(vx.Slice(0, ds.NumIdentical+ds.NumRemoved+ds.NumModified), diffRemoved)
  260. vx = vx.Slice(nx, vx.Len())
  261. ny := appendChunks(vy.Slice(0, ds.NumIdentical+ds.NumInserted+ds.NumModified), diffInserted)
  262. vy = vy.Slice(ny, vy.Len())
  263. }
  264. assert(vx.Len() == 0 && vy.Len() == 0)
  265. return list
  266. }
  267. // coalesceAdjacentEdits coalesces the list of edits into groups of adjacent
  268. // equal or unequal counts.
  269. func coalesceAdjacentEdits(name string, es diff.EditScript) (groups []diffStats) {
  270. var prevCase int // Arbitrary index into which case last occurred
  271. lastStats := func(i int) *diffStats {
  272. if prevCase != i {
  273. groups = append(groups, diffStats{Name: name})
  274. prevCase = i
  275. }
  276. return &groups[len(groups)-1]
  277. }
  278. for _, e := range es {
  279. switch e {
  280. case diff.Identity:
  281. lastStats(1).NumIdentical++
  282. case diff.UniqueX:
  283. lastStats(2).NumRemoved++
  284. case diff.UniqueY:
  285. lastStats(2).NumInserted++
  286. case diff.Modified:
  287. lastStats(2).NumModified++
  288. }
  289. }
  290. return groups
  291. }
  292. // coalesceInterveningIdentical coalesces sufficiently short (<= windowSize)
  293. // equal groups into adjacent unequal groups that currently result in a
  294. // dual inserted/removed printout. This acts as a high-pass filter to smooth
  295. // out high-frequency changes within the windowSize.
  296. func coalesceInterveningIdentical(groups []diffStats, windowSize int) []diffStats {
  297. groups, groupsOrig := groups[:0], groups
  298. for i, ds := range groupsOrig {
  299. if len(groups) >= 2 && ds.NumDiff() > 0 {
  300. prev := &groups[len(groups)-2] // Unequal group
  301. curr := &groups[len(groups)-1] // Equal group
  302. next := &groupsOrig[i] // Unequal group
  303. hadX, hadY := prev.NumRemoved > 0, prev.NumInserted > 0
  304. hasX, hasY := next.NumRemoved > 0, next.NumInserted > 0
  305. if ((hadX || hasX) && (hadY || hasY)) && curr.NumIdentical <= windowSize {
  306. *prev = (*prev).Append(*curr).Append(*next)
  307. groups = groups[:len(groups)-1] // Truncate off equal group
  308. continue
  309. }
  310. }
  311. groups = append(groups, ds)
  312. }
  313. return groups
  314. }