Use triple-quote formatting for multiline strings (#229)
For strings, []bytes containing text data, Error method output, and
String method output, use the triple-quoted syntax.
This improves readability by presenting the data more naturally
compared to a single-line quoted string with many escaped characters.
diff --git a/cmp/compare_test.go b/cmp/compare_test.go
index b34530b..ba39bde 100644
--- a/cmp/compare_test.go
+++ b/cmp/compare_test.go
@@ -1134,6 +1134,19 @@
wantEqual: false,
reason: "avoid triple-quote syntax due to visual equivalence of differences",
}, {
+ label: label + "/TripleQuoteStringer",
+ x: []fmt.Stringer{
+ bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
+ bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n)\n\nfunc main() {\n\tfmt.Println(\"My favorite number is\", rand.Intn(10))\n}\n")),
+ },
+ y: []fmt.Stringer{
+ bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello, playground\")\n}\n")),
+ bytes.NewBuffer([]byte("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n)\n\nfunc main() {\n\tfmt.Printf(\"Now you have %g problems.\\n\", math.Sqrt(7))\n}\n")),
+ },
+ opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })},
+ wantEqual: false,
+ reason: "multi-line String output should be formatted with triple quote",
+ }, {
label: label + "/LimitMaximumBytesDiffs",
x: []byte("\xcd====\x06\x1f\xc2\xcc\xc2-S=====\x1d\xdfa\xae\x98\x9fH======ǰ\xb7=======\xef====:\\\x94\xe6J\xc7=====\xb4======\n\n\xf7\x94===========\xf2\x9c\xc0f=====4\xf6\xf1\xc3\x17\x82======n\x16`\x91D\xc6\x06=======\x1cE====.===========\xc4\x18=======\x8a\x8d\x0e====\x87\xb1\xa5\x8e\xc3=====z\x0f1\xaeU======G,=======5\xe75\xee\x82\xf4\xce====\x11r===========\xaf]=======z\x05\xb3\x91\x88%\xd2====\n1\x89=====i\xb7\x055\xe6\x81\xd2=============\x883=@̾====\x14\x05\x96%^t\x04=====\xe7Ȉ\x90\x1d============="),
y: []byte("\\====|\x96\xe7SB\xa0\xab=====\xf0\xbd\xa5q\xab\x17;======\xabP\x00=======\xeb====\xa5\x14\xe6O(\xe4=====(======/c@?===========\xd9x\xed\x13=====J\xfc\x918B\x8d======a8A\xebs\x04\xae=======\aC====\x1c===========\x91\"=======uؾ====s\xec\x845\a=====;\xabS9t======\x1f\x1b=======\x80\xab/\xed+:;====\xeaI===========\xabl=======\xb9\xe9\xfdH\x93\x8e\u007f====ח\xe5=====Ig\x88m\xf5\x01V=============\xf7+4\xb0\x92E====\x9fj\xf8&\xd0h\xf9=====\xeeΨ\r\xbf============="),
diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go
index 28e0e92..786f671 100644
--- a/cmp/report_reflect.go
+++ b/cmp/report_reflect.go
@@ -5,6 +5,7 @@
package cmp
import (
+ "bytes"
"fmt"
"reflect"
"strconv"
@@ -138,14 +139,7 @@
}
}()
if prefix != "" {
- maxLen := len(strVal)
- if opts.LimitVerbosity {
- maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
- }
- if len(strVal) > maxLen+len(textEllipsis) {
- return textLine(prefix + formatString(strVal[:maxLen]) + string(textEllipsis))
- }
- return textLine(prefix + formatString(strVal))
+ return opts.formatString(prefix, strVal)
}
}
}
@@ -177,14 +171,7 @@
case reflect.Complex64, reflect.Complex128:
return textLine(fmt.Sprint(v.Complex()))
case reflect.String:
- maxLen := v.Len()
- if opts.LimitVerbosity {
- maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
- }
- if v.Len() > maxLen+len(textEllipsis) {
- return textLine(formatString(v.String()[:maxLen]) + string(textEllipsis))
- }
- return textLine(formatString(v.String()))
+ return opts.formatString("", v.String())
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
return textLine(formatPointer(value.PointerOf(v), true))
case reflect.Struct:
@@ -216,6 +203,17 @@
if v.IsNil() {
return textNil
}
+
+ // Check whether this is a []byte of text data.
+ if t.Elem() == reflect.TypeOf(byte(0)) {
+ b := v.Bytes()
+ isPrintSpace := func(r rune) bool { return unicode.IsPrint(r) && unicode.IsSpace(r) }
+ if len(b) > 0 && utf8.Valid(b) && len(bytes.TrimFunc(b, isPrintSpace)) == 0 {
+ out = opts.formatString("", string(b))
+ return opts.WithTypeMode(emitType).FormatType(t, out)
+ }
+ }
+
fallthrough
case reflect.Array:
maxLen := v.Len()
@@ -301,6 +299,49 @@
}
}
+func (opts formatOptions) formatString(prefix, s string) textNode {
+ maxLen := len(s)
+ maxLines := strings.Count(s, "\n") + 1
+ if opts.LimitVerbosity {
+ maxLen = (1 << opts.verbosity()) << 5 // 32, 64, 128, 256, etc...
+ maxLines = (1 << opts.verbosity()) << 2 // 4, 8, 16, 32, 64, etc...
+ }
+
+ // For multiline strings, use the triple-quote syntax,
+ // but only use it when printing removed or inserted nodes since
+ // we only want the extra verbosity for those cases.
+ lines := strings.Split(strings.TrimSuffix(s, "\n"), "\n")
+ isTripleQuoted := len(lines) >= 4 && (opts.DiffMode == '-' || opts.DiffMode == '+')
+ for i := 0; i < len(lines) && isTripleQuoted; i++ {
+ lines[i] = strings.TrimPrefix(strings.TrimSuffix(lines[i], "\r"), "\r") // trim leading/trailing carriage returns for legacy Windows endline support
+ isPrintable := func(r rune) bool {
+ return unicode.IsPrint(r) || r == '\t' // specially treat tab as printable
+ }
+ line := lines[i]
+ isTripleQuoted = !strings.HasPrefix(strings.TrimPrefix(line, prefix), `"""`) && !strings.HasPrefix(line, "...") && strings.TrimFunc(line, isPrintable) == "" && len(line) <= maxLen
+ }
+ if isTripleQuoted {
+ var list textList
+ list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
+ for i, line := range lines {
+ if numElided := len(lines) - i; i == maxLines-1 && numElided > 1 {
+ comment := commentString(fmt.Sprintf("%d elided lines", numElided))
+ list = append(list, textRecord{Diff: opts.DiffMode, Value: textEllipsis, ElideComma: true, Comment: comment})
+ break
+ }
+ list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(line), ElideComma: true})
+ }
+ list = append(list, textRecord{Diff: opts.DiffMode, Value: textLine(prefix + `"""`), ElideComma: true})
+ return &textWrap{Prefix: "(", Value: list, Suffix: ")"}
+ }
+
+ // Format the string as a single-line quoted string.
+ if len(s) > maxLen+len(textEllipsis) {
+ return textLine(prefix + formatString(s[:maxLen]) + string(textEllipsis))
+ }
+ return textLine(prefix + formatString(s))
+}
+
// formatMapKey formats v as if it were a map key.
// The result is guaranteed to be a single line.
func formatMapKey(v reflect.Value, disambiguate bool, ptrs *pointerReferences) string {
diff --git a/cmp/testdata/diffs b/cmp/testdata/diffs
index 05fa3fd..dee035d 100644
--- a/cmp/testdata/diffs
+++ b/cmp/testdata/diffs
@@ -730,6 +730,39 @@
... // 7 identical lines
}, "\n")
>>> TestDiff/Reporter/AvoidTripleQuoteIdenticalWhitespace
+<<< TestDiff/Reporter/TripleQuoteStringer
+ []fmt.Stringer{
+ s"package main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hel"...,
+- (
+- s"""
+- package main
+-
+- import (
+- "fmt"
+- "math/rand"
+- )
+-
+- func main() {
+- fmt.Println("My favorite number is", rand.Intn(10))
+- }
+- s"""
+- ),
++ (
++ s"""
++ package main
++
++ import (
++ "fmt"
++ "math"
++ )
++
++ func main() {
++ fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
++ }
++ s"""
++ ),
+ }
+>>> TestDiff/Reporter/TripleQuoteStringer
<<< TestDiff/Reporter/LimitMaximumBytesDiffs
[]uint8{
- 0xcd, 0x3d, 0x3d, 0x3d, 0x3d, 0x06, 0x1f, 0xc2, 0xcc, 0xc2, 0x2d, 0x53, // -|.====.....-S|