Path: blob/main/vendor/github.com/google/go-cmp/cmp/report_text.go
2880 views
// Copyright 2019, The Go Authors. All rights reserved.1// Use of this source code is governed by a BSD-style2// license that can be found in the LICENSE file.34package cmp56import (7"bytes"8"fmt"9"math/rand"10"strings"11"time"12"unicode/utf8"1314"github.com/google/go-cmp/cmp/internal/flags"15)1617var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 01819const maxColumnLength = 802021type indentMode int2223func (n indentMode) appendIndent(b []byte, d diffMode) []byte {24// The output of Diff is documented as being unstable to provide future25// flexibility in changing the output for more humanly readable reports.26// This logic intentionally introduces instability to the exact output27// so that users can detect accidental reliance on stability early on,28// rather than much later when an actual change to the format occurs.29if flags.Deterministic || randBool {30// Use regular spaces (U+0020).31switch d {32case diffUnknown, diffIdentical:33b = append(b, " "...)34case diffRemoved:35b = append(b, "- "...)36case diffInserted:37b = append(b, "+ "...)38}39} else {40// Use non-breaking spaces (U+00a0).41switch d {42case diffUnknown, diffIdentical:43b = append(b, " "...)44case diffRemoved:45b = append(b, "- "...)46case diffInserted:47b = append(b, "+ "...)48}49}50return repeatCount(n).appendChar(b, '\t')51}5253type repeatCount int5455func (n repeatCount) appendChar(b []byte, c byte) []byte {56for ; n > 0; n-- {57b = append(b, c)58}59return b60}6162// textNode is a simplified tree-based representation of structured text.63// Possible node types are textWrap, textList, or textLine.64type textNode interface {65// Len reports the length in bytes of a single-line version of the tree.66// Nested textRecord.Diff and textRecord.Comment fields are ignored.67Len() int68// Equal reports whether the two trees are structurally identical.69// Nested textRecord.Diff and textRecord.Comment fields are compared.70Equal(textNode) bool71// String returns the string representation of the text tree.72// It is not guaranteed that len(x.String()) == x.Len(),73// nor that x.String() == y.String() implies that x.Equal(y).74String() string7576// formatCompactTo formats the contents of the tree as a single-line string77// to the provided buffer. Any nested textRecord.Diff and textRecord.Comment78// fields are ignored.79//80// However, not all nodes in the tree should be collapsed as a single-line.81// If a node can be collapsed as a single-line, it is replaced by a textLine82// node. Since the top-level node cannot replace itself, this also returns83// the current node itself.84//85// This does not mutate the receiver.86formatCompactTo([]byte, diffMode) ([]byte, textNode)87// formatExpandedTo formats the contents of the tree as a multi-line string88// to the provided buffer. In order for column alignment to operate well,89// formatCompactTo must be called before calling formatExpandedTo.90formatExpandedTo([]byte, diffMode, indentMode) []byte91}9293// textWrap is a wrapper that concatenates a prefix and/or a suffix94// to the underlying node.95type textWrap struct {96Prefix string // e.g., "bytes.Buffer{"97Value textNode // textWrap | textList | textLine98Suffix string // e.g., "}"99Metadata interface{} // arbitrary metadata; has no effect on formatting100}101102func (s *textWrap) Len() int {103return len(s.Prefix) + s.Value.Len() + len(s.Suffix)104}105func (s1 *textWrap) Equal(s2 textNode) bool {106if s2, ok := s2.(*textWrap); ok {107return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix108}109return false110}111func (s *textWrap) String() string {112var d diffMode113var n indentMode114_, s2 := s.formatCompactTo(nil, d)115b := n.appendIndent(nil, d) // Leading indent116b = s2.formatExpandedTo(b, d, n) // Main body117b = append(b, '\n') // Trailing newline118return string(b)119}120func (s *textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {121n0 := len(b) // Original buffer length122b = append(b, s.Prefix...)123b, s.Value = s.Value.formatCompactTo(b, d)124b = append(b, s.Suffix...)125if _, ok := s.Value.(textLine); ok {126return b, textLine(b[n0:])127}128return b, s129}130func (s *textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {131b = append(b, s.Prefix...)132b = s.Value.formatExpandedTo(b, d, n)133b = append(b, s.Suffix...)134return b135}136137// textList is a comma-separated list of textWrap or textLine nodes.138// The list may be formatted as multi-lines or single-line at the discretion139// of the textList.formatCompactTo method.140type textList []textRecord141type textRecord struct {142Diff diffMode // e.g., 0 or '-' or '+'143Key string // e.g., "MyField"144Value textNode // textWrap | textLine145ElideComma bool // avoid trailing comma146Comment fmt.Stringer // e.g., "6 identical fields"147}148149// AppendEllipsis appends a new ellipsis node to the list if none already150// exists at the end. If cs is non-zero it coalesces the statistics with the151// previous diffStats.152func (s *textList) AppendEllipsis(ds diffStats) {153hasStats := !ds.IsZero()154if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {155if hasStats {156*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true, Comment: ds})157} else {158*s = append(*s, textRecord{Value: textEllipsis, ElideComma: true})159}160return161}162if hasStats {163(*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)164}165}166167func (s textList) Len() (n int) {168for i, r := range s {169n += len(r.Key)170if r.Key != "" {171n += len(": ")172}173n += r.Value.Len()174if i < len(s)-1 {175n += len(", ")176}177}178return n179}180181func (s1 textList) Equal(s2 textNode) bool {182if s2, ok := s2.(textList); ok {183if len(s1) != len(s2) {184return false185}186for i := range s1 {187r1, r2 := s1[i], s2[i]188if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {189return false190}191}192return true193}194return false195}196197func (s textList) String() string {198return (&textWrap{Prefix: "{", Value: s, Suffix: "}"}).String()199}200201func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {202s = append(textList(nil), s...) // Avoid mutating original203204// Determine whether we can collapse this list as a single line.205n0 := len(b) // Original buffer length206var multiLine bool207for i, r := range s {208if r.Diff == diffInserted || r.Diff == diffRemoved {209multiLine = true210}211b = append(b, r.Key...)212if r.Key != "" {213b = append(b, ": "...)214}215b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)216if _, ok := s[i].Value.(textLine); !ok {217multiLine = true218}219if r.Comment != nil {220multiLine = true221}222if i < len(s)-1 {223b = append(b, ", "...)224}225}226// Force multi-lined output when printing a removed/inserted node that227// is sufficiently long.228if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > maxColumnLength {229multiLine = true230}231if !multiLine {232return b, textLine(b[n0:])233}234return b, s235}236237func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {238alignKeyLens := s.alignLens(239func(r textRecord) bool {240_, isLine := r.Value.(textLine)241return r.Key == "" || !isLine242},243func(r textRecord) int { return utf8.RuneCountInString(r.Key) },244)245alignValueLens := s.alignLens(246func(r textRecord) bool {247_, isLine := r.Value.(textLine)248return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil249},250func(r textRecord) int { return utf8.RuneCount(r.Value.(textLine)) },251)252253// Format lists of simple lists in a batched form.254// If the list is sequence of only textLine values,255// then batch multiple values on a single line.256var isSimple bool257for _, r := range s {258_, isLine := r.Value.(textLine)259isSimple = r.Diff == 0 && r.Key == "" && isLine && r.Comment == nil260if !isSimple {261break262}263}264if isSimple {265n++266var batch []byte267emitBatch := func() {268if len(batch) > 0 {269b = n.appendIndent(append(b, '\n'), d)270b = append(b, bytes.TrimRight(batch, " ")...)271batch = batch[:0]272}273}274for _, r := range s {275line := r.Value.(textLine)276if len(batch)+len(line)+len(", ") > maxColumnLength {277emitBatch()278}279batch = append(batch, line...)280batch = append(batch, ", "...)281}282emitBatch()283n--284return n.appendIndent(append(b, '\n'), d)285}286287// Format the list as a multi-lined output.288n++289for i, r := range s {290b = n.appendIndent(append(b, '\n'), d|r.Diff)291if r.Key != "" {292b = append(b, r.Key+": "...)293}294b = alignKeyLens[i].appendChar(b, ' ')295296b = r.Value.formatExpandedTo(b, d|r.Diff, n)297if !r.ElideComma {298b = append(b, ',')299}300b = alignValueLens[i].appendChar(b, ' ')301302if r.Comment != nil {303b = append(b, " // "+r.Comment.String()...)304}305}306n--307308return n.appendIndent(append(b, '\n'), d)309}310311func (s textList) alignLens(312skipFunc func(textRecord) bool,313lenFunc func(textRecord) int,314) []repeatCount {315var startIdx, endIdx, maxLen int316lens := make([]repeatCount, len(s))317for i, r := range s {318if skipFunc(r) {319for j := startIdx; j < endIdx && j < len(s); j++ {320lens[j] = repeatCount(maxLen - lenFunc(s[j]))321}322startIdx, endIdx, maxLen = i+1, i+1, 0323} else {324if maxLen < lenFunc(r) {325maxLen = lenFunc(r)326}327endIdx = i + 1328}329}330for j := startIdx; j < endIdx && j < len(s); j++ {331lens[j] = repeatCount(maxLen - lenFunc(s[j]))332}333return lens334}335336// textLine is a single-line segment of text and is always a leaf node337// in the textNode tree.338type textLine []byte339340var (341textNil = textLine("nil")342textEllipsis = textLine("...")343)344345func (s textLine) Len() int {346return len(s)347}348func (s1 textLine) Equal(s2 textNode) bool {349if s2, ok := s2.(textLine); ok {350return bytes.Equal([]byte(s1), []byte(s2))351}352return false353}354func (s textLine) String() string {355return string(s)356}357func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {358return append(b, s...), s359}360func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {361return append(b, s...)362}363364type diffStats struct {365Name string366NumIgnored int367NumIdentical int368NumRemoved int369NumInserted int370NumModified int371}372373func (s diffStats) IsZero() bool {374s.Name = ""375return s == diffStats{}376}377378func (s diffStats) NumDiff() int {379return s.NumRemoved + s.NumInserted + s.NumModified380}381382func (s diffStats) Append(ds diffStats) diffStats {383assert(s.Name == ds.Name)384s.NumIgnored += ds.NumIgnored385s.NumIdentical += ds.NumIdentical386s.NumRemoved += ds.NumRemoved387s.NumInserted += ds.NumInserted388s.NumModified += ds.NumModified389return s390}391392// String prints a humanly-readable summary of coalesced records.393//394// Example:395//396// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"397func (s diffStats) String() string {398var ss []string399var sum int400labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}401counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}402for i, n := range counts {403if n > 0 {404ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))405}406sum += n407}408409// Pluralize the name (adjusting for some obscure English grammar rules).410name := s.Name411if sum > 1 {412name += "s"413if strings.HasSuffix(name, "ys") {414name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"415}416}417418// Format the list according to English grammar (with Oxford comma).419switch n := len(ss); n {420case 0:421return ""422case 1, 2:423return strings.Join(ss, " and ") + " " + name424default:425return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name426}427}428429type commentString string430431func (s commentString) String() string { return string(s) }432433434