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.
 
 
 

297 lines
8.9 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. "fmt"
  7. "reflect"
  8. "github.com/google/go-cmp/cmp/internal/value"
  9. )
  10. // TODO: Enforce limits?
  11. // * Enforce maximum number of records to print per node?
  12. // * Enforce maximum size in bytes allowed?
  13. // * As a heuristic, use less verbosity for equal nodes than unequal nodes.
  14. // TODO: Enforce unique outputs?
  15. // * Avoid Stringer methods if it results in same output?
  16. // * Print pointer address if outputs still equal?
  17. // numContextRecords is the number of surrounding equal records to print.
  18. const numContextRecords = 2
  19. type diffMode byte
  20. const (
  21. diffUnknown diffMode = 0
  22. diffIdentical diffMode = ' '
  23. diffRemoved diffMode = '-'
  24. diffInserted diffMode = '+'
  25. )
  26. type typeMode int
  27. const (
  28. // emitType always prints the type.
  29. emitType typeMode = iota
  30. // elideType never prints the type.
  31. elideType
  32. // autoType prints the type only for composite kinds
  33. // (i.e., structs, slices, arrays, and maps).
  34. autoType
  35. )
  36. type formatOptions struct {
  37. // DiffMode controls the output mode of FormatDiff.
  38. //
  39. // If diffUnknown, then produce a diff of the x and y values.
  40. // If diffIdentical, then emit values as if they were equal.
  41. // If diffRemoved, then only emit x values (ignoring y values).
  42. // If diffInserted, then only emit y values (ignoring x values).
  43. DiffMode diffMode
  44. // TypeMode controls whether to print the type for the current node.
  45. //
  46. // As a general rule of thumb, we always print the type of the next node
  47. // after an interface, and always elide the type of the next node after
  48. // a slice or map node.
  49. TypeMode typeMode
  50. // formatValueOptions are options specific to printing reflect.Values.
  51. formatValueOptions
  52. }
  53. func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
  54. opts.DiffMode = d
  55. return opts
  56. }
  57. func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
  58. opts.TypeMode = t
  59. return opts
  60. }
  61. // FormatDiff converts a valueNode tree into a textNode tree, where the later
  62. // is a textual representation of the differences detected in the former.
  63. func (opts formatOptions) FormatDiff(v *valueNode) textNode {
  64. // Check whether we have specialized formatting for this node.
  65. // This is not necessary, but helpful for producing more readable outputs.
  66. if opts.CanFormatDiffSlice(v) {
  67. return opts.FormatDiffSlice(v)
  68. }
  69. // For leaf nodes, format the value based on the reflect.Values alone.
  70. if v.MaxDepth == 0 {
  71. switch opts.DiffMode {
  72. case diffUnknown, diffIdentical:
  73. // Format Equal.
  74. if v.NumDiff == 0 {
  75. outx := opts.FormatValue(v.ValueX, visitedPointers{})
  76. outy := opts.FormatValue(v.ValueY, visitedPointers{})
  77. if v.NumIgnored > 0 && v.NumSame == 0 {
  78. return textEllipsis
  79. } else if outx.Len() < outy.Len() {
  80. return outx
  81. } else {
  82. return outy
  83. }
  84. }
  85. // Format unequal.
  86. assert(opts.DiffMode == diffUnknown)
  87. var list textList
  88. outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{})
  89. outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{})
  90. if outx != nil {
  91. list = append(list, textRecord{Diff: '-', Value: outx})
  92. }
  93. if outy != nil {
  94. list = append(list, textRecord{Diff: '+', Value: outy})
  95. }
  96. return opts.WithTypeMode(emitType).FormatType(v.Type, list)
  97. case diffRemoved:
  98. return opts.FormatValue(v.ValueX, visitedPointers{})
  99. case diffInserted:
  100. return opts.FormatValue(v.ValueY, visitedPointers{})
  101. default:
  102. panic("invalid diff mode")
  103. }
  104. }
  105. // Descend into the child value node.
  106. if v.TransformerName != "" {
  107. out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
  108. out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
  109. return opts.FormatType(v.Type, out)
  110. } else {
  111. switch k := v.Type.Kind(); k {
  112. case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map:
  113. return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k))
  114. case reflect.Ptr:
  115. return textWrap{"&", opts.FormatDiff(v.Value), ""}
  116. case reflect.Interface:
  117. return opts.WithTypeMode(emitType).FormatDiff(v.Value)
  118. default:
  119. panic(fmt.Sprintf("%v cannot have children", k))
  120. }
  121. }
  122. }
  123. func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
  124. // Derive record name based on the data structure kind.
  125. var name string
  126. var formatKey func(reflect.Value) string
  127. switch k {
  128. case reflect.Struct:
  129. name = "field"
  130. opts = opts.WithTypeMode(autoType)
  131. formatKey = func(v reflect.Value) string { return v.String() }
  132. case reflect.Slice, reflect.Array:
  133. name = "element"
  134. opts = opts.WithTypeMode(elideType)
  135. formatKey = func(reflect.Value) string { return "" }
  136. case reflect.Map:
  137. name = "entry"
  138. opts = opts.WithTypeMode(elideType)
  139. formatKey = formatMapKey
  140. }
  141. // Handle unification.
  142. switch opts.DiffMode {
  143. case diffIdentical, diffRemoved, diffInserted:
  144. var list textList
  145. var deferredEllipsis bool // Add final "..." to indicate records were dropped
  146. for _, r := range recs {
  147. // Elide struct fields that are zero value.
  148. if k == reflect.Struct {
  149. var isZero bool
  150. switch opts.DiffMode {
  151. case diffIdentical:
  152. isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueX)
  153. case diffRemoved:
  154. isZero = value.IsZero(r.Value.ValueX)
  155. case diffInserted:
  156. isZero = value.IsZero(r.Value.ValueY)
  157. }
  158. if isZero {
  159. continue
  160. }
  161. }
  162. // Elide ignored nodes.
  163. if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
  164. deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
  165. if !deferredEllipsis {
  166. list.AppendEllipsis(diffStats{})
  167. }
  168. continue
  169. }
  170. if out := opts.FormatDiff(r.Value); out != nil {
  171. list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
  172. }
  173. }
  174. if deferredEllipsis {
  175. list.AppendEllipsis(diffStats{})
  176. }
  177. return textWrap{"{", list, "}"}
  178. case diffUnknown:
  179. default:
  180. panic("invalid diff mode")
  181. }
  182. // Handle differencing.
  183. var list textList
  184. groups := coalesceAdjacentRecords(name, recs)
  185. for i, ds := range groups {
  186. // Handle equal records.
  187. if ds.NumDiff() == 0 {
  188. // Compute the number of leading and trailing records to print.
  189. var numLo, numHi int
  190. numEqual := ds.NumIgnored + ds.NumIdentical
  191. for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
  192. if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
  193. break
  194. }
  195. numLo++
  196. }
  197. for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
  198. if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
  199. break
  200. }
  201. numHi++
  202. }
  203. if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
  204. numHi++ // Avoid pointless coalescing of a single equal record
  205. }
  206. // Format the equal values.
  207. for _, r := range recs[:numLo] {
  208. out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
  209. list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
  210. }
  211. if numEqual > numLo+numHi {
  212. ds.NumIdentical -= numLo + numHi
  213. list.AppendEllipsis(ds)
  214. }
  215. for _, r := range recs[numEqual-numHi : numEqual] {
  216. out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
  217. list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
  218. }
  219. recs = recs[numEqual:]
  220. continue
  221. }
  222. // Handle unequal records.
  223. for _, r := range recs[:ds.NumDiff()] {
  224. switch {
  225. case opts.CanFormatDiffSlice(r.Value):
  226. out := opts.FormatDiffSlice(r.Value)
  227. list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
  228. case r.Value.NumChildren == r.Value.MaxDepth:
  229. outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
  230. outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
  231. if outx != nil {
  232. list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
  233. }
  234. if outy != nil {
  235. list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
  236. }
  237. default:
  238. out := opts.FormatDiff(r.Value)
  239. list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
  240. }
  241. }
  242. recs = recs[ds.NumDiff():]
  243. }
  244. assert(len(recs) == 0)
  245. return textWrap{"{", list, "}"}
  246. }
  247. // coalesceAdjacentRecords coalesces the list of records into groups of
  248. // adjacent equal, or unequal counts.
  249. func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
  250. var prevCase int // Arbitrary index into which case last occurred
  251. lastStats := func(i int) *diffStats {
  252. if prevCase != i {
  253. groups = append(groups, diffStats{Name: name})
  254. prevCase = i
  255. }
  256. return &groups[len(groups)-1]
  257. }
  258. for _, r := range recs {
  259. switch rv := r.Value; {
  260. case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
  261. lastStats(1).NumIgnored++
  262. case rv.NumDiff == 0:
  263. lastStats(1).NumIdentical++
  264. case rv.NumDiff > 0 && !rv.ValueY.IsValid():
  265. lastStats(2).NumRemoved++
  266. case rv.NumDiff > 0 && !rv.ValueX.IsValid():
  267. lastStats(2).NumInserted++
  268. default:
  269. lastStats(2).NumModified++
  270. }
  271. }
  272. return groups
  273. }