aboutsummaryrefslogtreecommitdiff
path: root/cmp
diff options
context:
space:
mode:
Diffstat (limited to 'cmp')
-rw-r--r--cmp/compare.go21
-rw-r--r--cmp/compare_test.go1201
-rw-r--r--cmp/example_test.go201
-rw-r--r--cmp/internal/flags/flags.go9
-rw-r--r--cmp/internal/flags/toolchain_legacy.go10
-rw-r--r--cmp/internal/flags/toolchain_recent.go10
-rw-r--r--cmp/internal/value/format.go280
-rw-r--r--cmp/internal/value/format_test.go93
-rw-r--r--cmp/internal/value/zero.go45
-rw-r--r--cmp/internal/value/zero_test.go45
-rw-r--r--cmp/path.go9
-rw-r--r--cmp/report.go71
-rw-r--r--cmp/report_compare.go290
-rw-r--r--cmp/report_reflect.go279
-rw-r--r--cmp/report_text.go382
-rw-r--r--cmp/report_value.go120
16 files changed, 2115 insertions, 951 deletions
diff --git a/cmp/compare.go b/cmp/compare.go
index 939a3b5..2762733 100644
--- a/cmp/compare.go
+++ b/cmp/compare.go
@@ -32,6 +32,7 @@ import (
"strings"
"github.com/google/go-cmp/cmp/internal/diff"
+ "github.com/google/go-cmp/cmp/internal/flags"
"github.com/google/go-cmp/cmp/internal/function"
"github.com/google/go-cmp/cmp/internal/value"
)
@@ -109,9 +110,15 @@ func Equal(x, y interface{}, opts ...Option) bool {
// Diff returns a human-readable report of the differences between two values.
// It returns an empty string if and only if Equal returns true for the same
-// input values and options. The output string will use the "-" symbol to
-// indicate elements removed from x, and the "+" symbol to indicate elements
-// added to y.
+// input values and options.
+//
+// The output is displayed as a literal in pseudo-Go syntax.
+// At the start of each line, a "-" prefix indicates an element removed from x,
+// a "+" prefix to indicates an element added to y, and the lack of a prefix
+// indicates an element common to both x and y. If possible, the output
+// uses fmt.Stringer.String or error.Error methods to produce more humanly
+// readable outputs. In such cases, the string is prefixed with either an
+// 's' or 'e' character, respectively, to indicate that the method was called.
//
// Do not depend on this output being stable.
func Diff(x, y interface{}, opts ...Option) string {
@@ -373,10 +380,10 @@ func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) {
// Otherwise, it returns the input value as is.
func sanitizeValue(v reflect.Value, t reflect.Type) reflect.Value {
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/22143).
- // The upstream fix landed in Go1.10, so we can remove this when drop support
- // for Go1.9 and below.
- if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
- return reflect.New(t).Elem()
+ if !flags.AtLeastGo110 {
+ if v.Kind() == reflect.Interface && v.IsNil() && v.Type() != t {
+ return reflect.New(t).Elem()
+ }
}
return v
}
diff --git a/cmp/compare_test.go b/cmp/compare_test.go
index 8c2cf34..33a4791 100644
--- a/cmp/compare_test.go
+++ b/cmp/compare_test.go
@@ -14,7 +14,6 @@ import (
"math/rand"
"reflect"
"regexp"
- "runtime"
"sort"
"strings"
"sync"
@@ -23,11 +22,17 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/google/go-cmp/cmp/internal/flags"
+
pb "github.com/google/go-cmp/cmp/internal/testprotos"
ts "github.com/google/go-cmp/cmp/internal/teststructs"
)
-var now = time.Now()
+func init() {
+ flags.Deterministic = true
+}
+
+var now = time.Date(2009, time.November, 10, 23, 00, 00, 00, time.UTC)
func intPtr(n int) *int { return &n }
@@ -73,7 +78,8 @@ func TestDiff(t *testing.T) {
if gotPanic != "" {
t.Fatalf("unexpected panic message: %s\nreason: %v", gotPanic, tt.reason)
}
- if got, want := strings.TrimSpace(gotDiff), strings.TrimSpace(tt.wantDiff); got != want {
+ tt.wantDiff = strings.TrimPrefix(tt.wantDiff, "\n")
+ if gotDiff != tt.wantDiff {
t.Fatalf("difference message:\ngot:\n%s\nwant:\n%s\nreason: %v", gotDiff, tt.wantDiff, tt.reason)
}
} else {
@@ -181,10 +187,17 @@ func comparerTests() []test {
x: struct{ A, B, C int }{1, 2, 3},
y: struct{ A, B, C int }{1, 2, 3},
}, {
- label: label,
- x: struct{ A, B, C int }{1, 2, 3},
- y: struct{ A, B, C int }{1, 2, 4},
- wantDiff: "root.C:\n\t-: 3\n\t+: 4\n",
+ label: label,
+ x: struct{ A, B, C int }{1, 2, 3},
+ y: struct{ A, B, C int }{1, 2, 4},
+ wantDiff: `
+ struct{ A int; B int; C int }{
+ A: 1,
+ B: 2,
+- C: 3,
++ C: 4,
+ }
+`,
}, {
label: label,
x: struct{ a, b, c int }{1, 2, 3},
@@ -195,10 +208,15 @@ func comparerTests() []test {
x: &struct{ A *int }{intPtr(4)},
y: &struct{ A *int }{intPtr(4)},
}, {
- label: label,
- x: &struct{ A *int }{intPtr(4)},
- y: &struct{ A *int }{intPtr(5)},
- wantDiff: "*root.A:\n\t-: 4\n\t+: 5\n",
+ label: label,
+ x: &struct{ A *int }{intPtr(4)},
+ y: &struct{ A *int }{intPtr(5)},
+ wantDiff: `
+ &struct{ A *int }{
+- A: &4,
++ A: &5,
+ }
+`,
}, {
label: label,
x: &struct{ A *int }{intPtr(4)},
@@ -218,10 +236,15 @@ func comparerTests() []test {
x: &struct{ R *bytes.Buffer }{},
y: &struct{ R *bytes.Buffer }{},
}, {
- label: label,
- x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)},
- y: &struct{ R *bytes.Buffer }{},
- wantDiff: "root.R:\n\t-: s\"\"\n\t+: <nil>\n",
+ label: label,
+ x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)},
+ y: &struct{ R *bytes.Buffer }{},
+ wantDiff: `
+ &struct{ R *bytes.Buffer }{
+- R: s"",
++ R: nil,
+ }
+`,
}, {
label: label,
x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)},
@@ -276,9 +299,12 @@ func comparerTests() []test {
return x.String() == y.String()
})},
wantDiff: `
-{[]*regexp.Regexp}[1]:
- -: s"a*b*c*"
- +: s"a*b*d*"`,
+ []*regexp.Regexp{
+ nil,
+- s"a*b*c*",
++ s"a*b*d*",
+ }
+`,
}, {
label: label,
x: func() ***int {
@@ -308,9 +334,11 @@ func comparerTests() []test {
return &c
}(),
wantDiff: `
-***{***int}:
- -: 0
- +: 1`,
+ &&&int(
+- 0,
++ 1,
+ )
+`,
}, {
label: label,
x: []int{1, 2, 3, 4, 5}[:3],
@@ -326,93 +354,117 @@ func comparerTests() []test {
y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")},
opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })},
wantDiff: `
-root:
- -: s"hello"
- +: s"hello2"`,
+ struct{ fmt.Stringer }(
+- s"hello",
++ s"hello2",
+ )
+`,
}, {
label: label,
x: md5.Sum([]byte{'a'}),
y: md5.Sum([]byte{'b'}),
wantDiff: `
-{[16]uint8}[0]:
- -: 0x0c
- +: 0x92
-{[16]uint8}[1]:
- -: 0xc1
- +: 0xeb
-{[16]uint8}[2]:
- -: 0x75
- +: 0x5f
-{[16]uint8}[3]:
- -: 0xb9
- +: 0xfe
-{[16]uint8}[4]:
- -: 0xc0
- +: 0xe6
-{[16]uint8}[5]:
- -: 0xf1
- +: 0xae
-{[16]uint8}[6]:
- -: 0xb6
- +: 0x2f
-{[16]uint8}[7]:
- -: 0xa8
- +: 0xec
-{[16]uint8}[8]:
- -: 0x31
- +: 0x3a
-{[16]uint8}[9]:
- -: 0xc3
- +: 0xd7
-{[16]uint8}[10]:
- -: 0x99
- +: 0x1c
-{[16]uint8}[11->?]:
- -: 0xe2
- +: <non-existent>
-{[16]uint8}[12->?]:
- -: 0x69
- +: <non-existent>
-{[16]uint8}[?->12]:
- -: <non-existent>
- +: 0x75
-{[16]uint8}[?->13]:
- -: <non-existent>
- +: 0x31
-{[16]uint8}[14]:
- -: 0x26
- +: 0x57
-{[16]uint8}[15]:
- -: 0x61
- +: 0x8f`,
+ [16]uint8{
+- 0x0c,
++ 0x92,
+- 0xc1,
++ 0xeb,
+- 0x75,
++ 0x5f,
+- 0xb9,
++ 0xfe,
+- 0xc0,
++ 0xe6,
+- 0xf1,
++ 0xae,
+- 0xb6,
++ 0x2f,
+- 0xa8,
++ 0xec,
+- 0x31,
++ 0x3a,
+- 0xc3,
++ 0xd7,
+- 0x99,
++ 0x1c,
+- 0xe2,
+- 0x69,
+ 0x77,
++ 0x75,
++ 0x31,
+- 0x26,
++ 0x57,
+- 0x61,
++ 0x8f,
+ }
+`,
}, {
label: label,
x: new(fmt.Stringer),
y: nil,
wantDiff: `
-root:
- -: &<nil>
- +: <non-existent>`,
+ interface{}(
+- &fmt.Stringer(nil),
+ )
+`,
}, {
label: label,
x: makeTarHeaders('0'),
y: makeTarHeaders('\x00'),
wantDiff: `
-{[]cmp_test.tarHeader}[0].Typeflag:
- -: 0x30
- +: 0x00
-{[]cmp_test.tarHeader}[1].Typeflag:
- -: 0x30
- +: 0x00
-{[]cmp_test.tarHeader}[2].Typeflag:
- -: 0x30
- +: 0x00
-{[]cmp_test.tarHeader}[3].Typeflag:
- -: 0x30
- +: 0x00
-{[]cmp_test.tarHeader}[4].Typeflag:
- -: 0x30
- +: 0x00`,
+ []cmp_test.tarHeader{
+ {
+ ... // 4 identical fields
+ Size: 1,
+ ModTime: s"2009-11-10 23:00:00 +0000 UTC",
+- Typeflag: 0x30,
++ Typeflag: 0x00,
+ Linkname: "",
+ Uname: "user",
+ ... // 6 identical fields
+ },
+ {
+ ... // 4 identical fields
+ Size: 2,
+ ModTime: s"2009-11-11 00:00:00 +0000 UTC",
+- Typeflag: 0x30,
++ Typeflag: 0x00,
+ Linkname: "",
+ Uname: "user",
+ ... // 6 identical fields
+ },
+ {
+ ... // 4 identical fields
+ Size: 4,
+ ModTime: s"2009-11-11 01:00:00 +0000 UTC",
+- Typeflag: 0x30,
++ Typeflag: 0x00,
+ Linkname: "",
+ Uname: "user",
+ ... // 6 identical fields
+ },
+ {
+ ... // 4 identical fields
+ Size: 8,
+ ModTime: s"2009-11-11 02:00:00 +0000 UTC",
+- Typeflag: 0x30,
++ Typeflag: 0x00,
+ Linkname: "",
+ Uname: "user",
+ ... // 6 identical fields
+ },
+ {
+ ... // 4 identical fields
+ Size: 16,
+ ModTime: s"2009-11-11 03:00:00 +0000 UTC",
+- Typeflag: 0x30,
++ Typeflag: 0x00,
+ Linkname: "",
+ Uname: "user",
+ ... // 6 identical fields
+ },
+ }
+`,
}, {
label: label,
x: make([]int, 1000),
@@ -465,50 +517,49 @@ root:
}),
},
wantDiff: `
-λ({[]int}[0]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[1]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[2]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[3]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[4]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[5]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[6]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[7]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[8]):
- -: float64(NaN)
- +: float64(NaN)
-λ({[]int}[9]):
- -: float64(NaN)
- +: float64(NaN)`,
+ []int{
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+- Inverse(λ, float64(NaN)),
++ Inverse(λ, float64(NaN)),
+ }
+`,
}, {
// Ensure reasonable Stringer formatting of map keys.
label: label,
x: map[*pb.Stringer]*pb.Stringer{{"hello"}: {"world"}},
y: map[*pb.Stringer]*pb.Stringer(nil),
wantDiff: `
-{map[*testprotos.Stringer]*testprotos.Stringer}:
- -: map[*testprotos.Stringer]*testprotos.Stringer{s"hello": s"world"}
- +: map[*testprotos.Stringer]*testprotos.Stringer(nil)`,
+ map[*testprotos.Stringer]*testprotos.Stringer(
+- {⟪0xdeadf00f⟫: s"world"},
++ nil,
+ )
+`,
}, {
// Ensure Stringer avoids double-quote escaping if possible.
- label: label,
- x: []*pb.Stringer{{`multi\nline\nline\nline`}},
- wantDiff: "root:\n\t-: []*testprotos.Stringer{s`multi\\nline\\nline\\nline`}\n\t+: <non-existent>",
+ label: label,
+ x: []*pb.Stringer{{`multi\nline\nline\nline`}},
+ wantDiff: strings.Replace(`
+ interface{}(
+- []*testprotos.Stringer{s'multi\nline\nline\nline'},
+ )
+`, "'", "`", -1),
}, {
label: label,
x: struct{ I Iface2 }{},
@@ -541,16 +592,49 @@ root:
x: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63, "name": "Sammy Sosa"}},
y: []interface{}{map[string]interface{}{"avg": 0.278, "hr": 65.0, "name": "Mark McGwire"}, map[string]interface{}{"avg": 0.288, "hr": 63.0, "name": "Sammy Sosa"}},
wantDiff: `
-root[0]["hr"]:
- -: int(65)
- +: float64(65)
-root[1]["hr"]:
- -: int(63)
- +: float64(63)`,
+ []interface{}{
+ map[string]interface{}{
+ "avg": float64(0.278),
+- "hr": int(65),
++ "hr": float64(65),
+ "name": string("Mark McGwire"),
+ },
+ map[string]interface{}{
+ "avg": float64(0.288),
+- "hr": int(63),
++ "hr": float64(63),
+ "name": string("Sammy Sosa"),
+ },
+ }
+`,
}, {
label: label,
- x: struct{ _ string }{},
- y: struct{ _ string }{},
+ x: map[*int]string{
+ new(int): "hello",
+ },
+ y: map[*int]string{
+ new(int): "world",
+ },
+ wantDiff: `
+ map[*int]string{
+- ⟪0xdeadf00f⟫: "hello",
++ ⟪0xdeadf00f⟫: "world",
+ }
+`,
+ }, {
+ label: label,
+ x: intPtr(0),
+ y: intPtr(0),
+ opts: []cmp.Option{
+ cmp.Comparer(func(x, y *int) bool { return x == y }),
+ },
+ // TODO: This output is unhelpful and should show the address.
+ wantDiff: `
+ (*int)(
+- &0,
++ &0,
+ )
+`,
}, {
label: label,
x: [2][]int{
@@ -574,9 +658,16 @@ root[1]["hr"]:
}, cmp.Ignore()),
},
wantDiff: `
-{[2][]int}[1][5->3]:
- -: 20
- +: 2`,
+ [2][]int{
+ {..., 1, 2, 3, ..., 4, 5, 6, 7, ..., 8, ..., 9, ...},
+ {
+ ... // 6 ignored and 1 identical elements
+- 20,
++ 2,
+ ... // 3 ignored elements
+ },
+ }
+`,
reason: "all zero slice elements are ignored (even if missing)",
}, {
label: label,
@@ -601,9 +692,15 @@ root[1]["hr"]:
}, cmp.Ignore()),
},
wantDiff: `
-{[2]map[string]int}[1]["keep2"]:
- -: <non-existent>
- +: 2`,
+ [2]map[string]int{
+ {"KEEP3": 3, "keep1": 1, "keep2": 2, ...},
+ {
+ ... // 2 ignored entries
+ "keep1": 1,
++ "keep2": 2,
+ },
+ }
+`,
reason: "all zero map entries are ignored (even if missing)",
}}
}
@@ -638,9 +735,11 @@ func transformerTests() []test {
cmp.Transformer("λ", func(in uint32) uint64 { return uint64(in) }),
},
wantDiff: `
-λ(λ(λ({uint8}))):
- -: 0x00
- +: 0x01`,
+ uint8(Inverse(λ, uint16(Inverse(λ, uint32(Inverse(λ, uint64(
+- 0x00,
++ 0x01,
+ )))))))
+`,
}, {
label: label,
x: 0,
@@ -665,12 +764,15 @@ func transformerTests() []test {
),
},
wantDiff: `
-λ({[]int}[1]):
- -: -5
- +: 3
-λ({[]int}[3]):
- -: -1
- +: -5`,
+ []int{
+ Inverse(λ, int64(0)),
+- Inverse(λ, int64(-5)),
++ Inverse(λ, int64(3)),
+ Inverse(λ, int64(0)),
+- Inverse(λ, int64(-1)),
++ Inverse(λ, int64(-5)),
+ }
+`,
}, {
label: label,
x: 0,
@@ -678,15 +780,17 @@ func transformerTests() []test {
opts: []cmp.Option{
cmp.Transformer("λ", func(in int) interface{} {
if in == 0 {
- return "string"
+ return "zero"
}
return float64(in)
}),
},
wantDiff: `
-λ({int}):
- -: "string"
- +: 1`,
+ int(Inverse(λ, interface{}(
+- string("zero"),
++ float64(1),
+ )))
+`,
}, {
label: label,
x: `{
@@ -726,18 +830,32 @@ func transformerTests() []test {
}),
},
wantDiff: `
-ParseJSON({string})["address"]["city"]:
- -: "Los Angeles"
- +: "New York"
-ParseJSON({string})["address"]["state"]:
- -: "CA"
- +: "NY"
-ParseJSON({string})["phoneNumbers"][0]["number"]:
- -: "212 555-4321"
- +: "212 555-1234"
-ParseJSON({string})["spouse"]:
- -: <non-existent>
- +: interface {}(nil)`,
+ string(Inverse(ParseJSON, map[string]interface{}{
+ "address": map[string]interface{}{
+- "city": string("Los Angeles"),
++ "city": string("New York"),
+ "postalCode": string("10021-3100"),
+- "state": string("CA"),
++ "state": string("NY"),
+ "streetAddress": string("21 2nd Street"),
+ },
+ "age": float64(25),
+ "children": []interface{}{},
+ "firstName": string("John"),
+ "isAlive": bool(true),
+ "lastName": string("Smith"),
+ "phoneNumbers": []interface{}{
+ map[string]interface{}{
+- "number": string("212 555-4321"),
++ "number": string("212 555-1234"),
+ "type": string("home"),
+ },
+ map[string]interface{}{"number": string("646 555-4567"), "type": string("office")},
+ map[string]interface{}{"number": string("123 456-7890"), "type": string("mobile")},
+ },
++ "spouse": nil,
+ }))
+`,
}, {
label: label,
x: StringBytes{String: "some\nmulti\nLine\nstring", Bytes: []byte("some\nmulti\nline\nbytes")},
@@ -747,12 +865,28 @@ ParseJSON({string})["spouse"]:
transformOnce("SplitBytes", func(b []byte) [][]byte { return bytes.Split(b, []byte("\n")) }),
},
wantDiff: `
-SplitString({cmp_test.StringBytes}.String)[2]:
- -: "Line"
- +: "line"
-SplitBytes({cmp_test.StringBytes}.Bytes)[3][0]:
- -: 0x62
- +: 0x42`,
+ cmp_test.StringBytes{
+ String: Inverse(SplitString, []string{
+ "some",
+ "multi",
+- "Line",
++ "line",
+ "string",
+ }),
+ Bytes: []uint8(Inverse(SplitBytes, [][]uint8{
+ {0x73, 0x6f, 0x6d, 0x65},
+ {0x6d, 0x75, 0x6c, 0x74, 0x69},
+ {0x6c, 0x69, 0x6e, 0x65},
+ {
+- 0x62,
++ 0x42,
+ 0x79,
+ 0x74,
+ ... // 2 identical elements
+ },
+ })),
+ }
+`,
}, {
x: "a\nb\nc\n",
y: "a\nb\nc\n",
@@ -866,10 +1000,8 @@ func embeddedTests() []test {
}
// TODO(dsnet): Workaround for reflect bug (https://golang.org/issue/21122).
- // The upstream fix landed in Go1.10, so we can remove this when dropping
- // support for Go1.9 and below.
wantPanicNotGo110 := func(s string) string {
- if v := runtime.Version(); strings.HasPrefix(v, "go1.8") || strings.HasPrefix(v, "go1.9") {
+ if !flags.AtLeastGo110 {
return ""
}
return s
@@ -910,12 +1042,15 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructA{}, privateStruct),
},
wantDiff: `
-{teststructs.ParentStructA}.privateStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructA}.privateStruct.private:
- -: 2
- +: 3`,
+ teststructs.ParentStructA{
+ privateStruct: teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ }
+`,
}, {
label: label + "ParentStructB",
x: ts.ParentStructB{},
@@ -955,12 +1090,15 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}),
},
wantDiff: `
-{teststructs.ParentStructB}.PublicStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructB}.PublicStruct.private:
- -: 2
- +: 3`,
+ teststructs.ParentStructB{
+ PublicStruct: teststructs.PublicStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ }
+`,
}, {
label: label + "ParentStructC",
x: ts.ParentStructC{},
@@ -996,18 +1134,19 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructC{}, privateStruct),
},
wantDiff: `
-{teststructs.ParentStructC}.privateStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructC}.privateStruct.private:
- -: 2
- +: 3
-{teststructs.ParentStructC}.Public:
- -: 3
- +: 4
-{teststructs.ParentStructC}.private:
- -: 4
- +: 5`,
+ teststructs.ParentStructC{
+ privateStruct: teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ }
+`,
}, {
label: label + "ParentStructD",
x: ts.ParentStructD{},
@@ -1047,18 +1186,19 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}),
},
wantDiff: `
-{teststructs.ParentStructD}.PublicStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructD}.PublicStruct.private:
- -: 2
- +: 3
-{teststructs.ParentStructD}.Public:
- -: 3
- +: 4
-{teststructs.ParentStructD}.private:
- -: 4
- +: 5`,
+ teststructs.ParentStructD{
+ PublicStruct: teststructs.PublicStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ }
+`,
}, {
label: label + "ParentStructE",
x: ts.ParentStructE{},
@@ -1106,18 +1246,21 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct),
},
wantDiff: `
-{teststructs.ParentStructE}.privateStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructE}.privateStruct.private:
- -: 2
- +: 3
-{teststructs.ParentStructE}.PublicStruct.Public:
- -: 3
- +: 4
-{teststructs.ParentStructE}.PublicStruct.private:
- -: 4
- +: 5`,
+ teststructs.ParentStructE{
+ privateStruct: teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ PublicStruct: teststructs.PublicStruct{
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ },
+ }
+`,
}, {
label: label + "ParentStructF",
x: ts.ParentStructF{},
@@ -1165,24 +1308,25 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct),
},
wantDiff: `
-{teststructs.ParentStructF}.privateStruct.Public:
- -: 1
- +: 2
-{teststructs.ParentStructF}.privateStruct.private:
- -: 2
- +: 3
-{teststructs.ParentStructF}.PublicStruct.Public:
- -: 3
- +: 4
-{teststructs.ParentStructF}.PublicStruct.private:
- -: 4
- +: 5
-{teststructs.ParentStructF}.Public:
- -: 5
- +: 6
-{teststructs.ParentStructF}.private:
- -: 6
- +: 7`,
+ teststructs.ParentStructF{
+ privateStruct: teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ PublicStruct: teststructs.PublicStruct{
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ },
+- Public: 5,
++ Public: 6,
+- private: 6,
++ private: 7,
+ }
+`,
}, {
label: label + "ParentStructG",
x: ts.ParentStructG{},
@@ -1218,12 +1362,15 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructG{}, privateStruct),
},
wantDiff: `
-{*teststructs.ParentStructG}.privateStruct.Public:
- -: 1
- +: 2
-{*teststructs.ParentStructG}.privateStruct.private:
- -: 2
- +: 3`,
+ &teststructs.ParentStructG{
+ privateStruct: &teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ }
+`,
}, {
label: label + "ParentStructH",
x: ts.ParentStructH{},
@@ -1263,12 +1410,15 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}),
},
wantDiff: `
-{*teststructs.ParentStructH}.PublicStruct.Public:
- -: 1
- +: 2
-{*teststructs.ParentStructH}.PublicStruct.private:
- -: 2
- +: 3`,
+ &teststructs.ParentStructH{
+ PublicStruct: &teststructs.PublicStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ }
+`,
}, {
label: label + "ParentStructI",
x: ts.ParentStructI{},
@@ -1319,18 +1469,21 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct),
},
wantDiff: `
-{*teststructs.ParentStructI}.privateStruct.Public:
- -: 1
- +: 2
-{*teststructs.ParentStructI}.privateStruct.private:
- -: 2
- +: 3
-{*teststructs.ParentStructI}.PublicStruct.Public:
- -: 3
- +: 4
-{*teststructs.ParentStructI}.PublicStruct.private:
- -: 4
- +: 5`,
+ &teststructs.ParentStructI{
+ privateStruct: &teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ PublicStruct: &teststructs.PublicStruct{
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ },
+ }
+`,
}, {
label: label + "ParentStructJ",
x: ts.ParentStructJ{},
@@ -1374,30 +1527,33 @@ func embeddedTests() []test {
cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct),
},
wantDiff: `
-{*teststructs.ParentStructJ}.privateStruct.Public:
- -: 1
- +: 2
-{*teststructs.ParentStructJ}.privateStruct.private:
- -: 2
- +: 3
-{*teststructs.ParentStructJ}.PublicStruct.Public:
- -: 3
- +: 4
-{*teststructs.ParentStructJ}.PublicStruct.private:
- -: 4
- +: 5
-{*teststructs.ParentStructJ}.Public.Public:
- -: 7
- +: 8
-{*teststructs.ParentStructJ}.Public.private:
- -: 8
- +: 9
-{*teststructs.ParentStructJ}.private.Public:
- -: 5
- +: 6
-{*teststructs.ParentStructJ}.private.private:
- -: 6
- +: 7`,
+ &teststructs.ParentStructJ{
+ privateStruct: &teststructs.privateStruct{
+- Public: 1,
++ Public: 2,
+- private: 2,
++ private: 3,
+ },
+ PublicStruct: &teststructs.PublicStruct{
+- Public: 3,
++ Public: 4,
+- private: 4,
++ private: 5,
+ },
+ Public: teststructs.PublicStruct{
+- Public: 7,
++ Public: 8,
+- private: 8,
++ private: 9,
+ },
+ private: teststructs.privateStruct{
+- Public: 5,
++ Public: 6,
+- private: 6,
++ private: 7,
+ },
+ }
+`,
}}
}
@@ -1444,9 +1600,11 @@ func methodTests() []test {
x: ts.StructB{X: "NotEqual"},
y: ts.StructB{X: "not_equal"},
wantDiff: `
-{teststructs.StructB}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructB{
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructB",
x: ts.StructB{X: "NotEqual"},
@@ -1469,9 +1627,11 @@ func methodTests() []test {
x: ts.StructD{X: "NotEqual"},
y: ts.StructD{X: "not_equal"},
wantDiff: `
-{teststructs.StructD}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructD{
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructD",
x: ts.StructD{X: "NotEqual"},
@@ -1486,9 +1646,11 @@ func methodTests() []test {
x: ts.StructE{X: "NotEqual"},
y: ts.StructE{X: "not_equal"},
wantDiff: `
-{teststructs.StructE}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructE{
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructE",
x: ts.StructE{X: "NotEqual"},
@@ -1503,9 +1665,11 @@ func methodTests() []test {
x: ts.StructF{X: "NotEqual"},
y: ts.StructF{X: "not_equal"},
wantDiff: `
-{teststructs.StructF}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructF{
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructF",
x: &ts.StructF{X: "NotEqual"},
@@ -1515,41 +1679,65 @@ func methodTests() []test {
x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"},
y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructA1",
- x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"},
- y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructA1",
+ x: ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"},
+ y: ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ teststructs.StructA1{
+ StructA: teststructs.StructA{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructA1",
x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "equal"},
y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructA1",
- x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"},
- y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{*teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructA1",
+ x: &ts.StructA1{StructA: ts.StructA{X: "NotEqual"}, X: "NotEqual"},
+ y: &ts.StructA1{StructA: ts.StructA{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ &teststructs.StructA1{
+ StructA: teststructs.StructA{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructB1",
x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"},
y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"},
opts: []cmp.Option{derefTransform},
}, {
- label: label + "StructB1",
- x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"},
- y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"},
- opts: []cmp.Option{derefTransform},
- wantDiff: "{teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructB1",
+ x: ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"},
+ y: ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"},
+ opts: []cmp.Option{derefTransform},
+ wantDiff: `
+ teststructs.StructB1{
+ StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})),
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructB1",
x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "equal"},
y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "equal"},
opts: []cmp.Option{derefTransform},
}, {
- label: label + "StructB1",
- x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"},
- y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"},
- opts: []cmp.Option{derefTransform},
- wantDiff: "{*teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructB1",
+ x: &ts.StructB1{StructB: ts.StructB{X: "NotEqual"}, X: "NotEqual"},
+ y: &ts.StructB1{StructB: ts.StructB{X: "not_equal"}, X: "not_equal"},
+ opts: []cmp.Option{derefTransform},
+ wantDiff: `
+ &teststructs.StructB1{
+ StructB: teststructs.StructB(Inverse(Ref, &teststructs.StructB{X: "NotEqual"})),
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructC1",
x: ts.StructC1{StructC: ts.StructC{X: "NotEqual"}, X: "NotEqual"},
@@ -1563,12 +1751,13 @@ func methodTests() []test {
x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"},
y: ts.StructD1{StructD: ts.StructD{X: "not_equal"}, X: "not_equal"},
wantDiff: `
-{teststructs.StructD1}.StructD.X:
- -: "NotEqual"
- +: "not_equal"
-{teststructs.StructD1}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructD1{
+- StructD: teststructs.StructD{X: "NotEqual"},
++ StructD: teststructs.StructD{X: "not_equal"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructD1",
x: ts.StructD1{StructD: ts.StructD{X: "NotEqual"}, X: "NotEqual"},
@@ -1583,12 +1772,13 @@ func methodTests() []test {
x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"},
y: ts.StructE1{StructE: ts.StructE{X: "not_equal"}, X: "not_equal"},
wantDiff: `
-{teststructs.StructE1}.StructE.X:
- -: "NotEqual"
- +: "not_equal"
-{teststructs.StructE1}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructE1{
+- StructE: teststructs.StructE{X: "NotEqual"},
++ StructE: teststructs.StructE{X: "not_equal"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructE1",
x: ts.StructE1{StructE: ts.StructE{X: "NotEqual"}, X: "NotEqual"},
@@ -1603,12 +1793,13 @@ func methodTests() []test {
x: ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"},
y: ts.StructF1{StructF: ts.StructF{X: "not_equal"}, X: "not_equal"},
wantDiff: `
-{teststructs.StructF1}.StructF.X:
- -: "NotEqual"
- +: "not_equal"
-{teststructs.StructF1}.X:
- -: "NotEqual"
- +: "not_equal"`,
+ teststructs.StructF1{
+- StructF: teststructs.StructF{X: "NotEqual"},
++ StructF: teststructs.StructF{X: "not_equal"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructF1",
x: &ts.StructF1{StructF: ts.StructF{X: "NotEqual"}, X: "NotEqual"},
@@ -1618,37 +1809,61 @@ func methodTests() []test {
x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"},
y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructA2",
- x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"},
- y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructA2",
+ x: ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"},
+ y: ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ teststructs.StructA2{
+ StructA: &teststructs.StructA{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructA2",
x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "equal"},
y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructA2",
- x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"},
- y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{*teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructA2",
+ x: &ts.StructA2{StructA: &ts.StructA{X: "NotEqual"}, X: "NotEqual"},
+ y: &ts.StructA2{StructA: &ts.StructA{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ &teststructs.StructA2{
+ StructA: &teststructs.StructA{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructB2",
x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"},
y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructB2",
- x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"},
- y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructB2",
+ x: ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"},
+ y: ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ teststructs.StructB2{
+ StructB: &teststructs.StructB{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructB2",
x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "equal"},
y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "equal"},
}, {
- label: label + "StructB2",
- x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"},
- y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"},
- wantDiff: "{*teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructB2",
+ x: &ts.StructB2{StructB: &ts.StructB{X: "NotEqual"}, X: "NotEqual"},
+ y: &ts.StructB2{StructB: &ts.StructB{X: "not_equal"}, X: "not_equal"},
+ wantDiff: `
+ &teststructs.StructB2{
+ StructB: &teststructs.StructB{X: "NotEqual"},
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "StructC2",
x: ts.StructC2{StructC: &ts.StructC{X: "NotEqual"}, X: "NotEqual"},
@@ -1682,10 +1897,15 @@ func methodTests() []test {
x: &ts.StructF2{StructF: &ts.StructF{X: "NotEqual"}, X: "NotEqual"},
y: &ts.StructF2{StructF: &ts.StructF{X: "not_equal"}, X: "not_equal"},
}, {
- label: label + "StructNo",
- x: ts.StructNo{X: "NotEqual"},
- y: ts.StructNo{X: "not_equal"},
- wantDiff: "{teststructs.StructNo}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n",
+ label: label + "StructNo",
+ x: ts.StructNo{X: "NotEqual"},
+ y: ts.StructNo{X: "not_equal"},
+ wantDiff: `
+ teststructs.StructNo{
+- X: "NotEqual",
++ X: "not_equal",
+ }
+`,
}, {
label: label + "AssignA",
x: ts.AssignA(func() int { return 0 }),
@@ -1790,8 +2010,32 @@ func project1Tests() []test {
y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, {
Args: &pb.MetaData{Stringer: pb.Stringer{X: "metadata2"}},
}}},
- opts: []cmp.Option{cmp.Comparer(pb.Equal)},
- wantDiff: "{teststructs.Eagle}.Slaps[4].Args:\n\t-: s\"metadata\"\n\t+: s\"metadata2\"\n",
+ opts: []cmp.Option{cmp.Comparer(pb.Equal)},
+ wantDiff: `
+ teststructs.Eagle{
+ ... // 4 identical fields
+ Dreamers: nil,
+ Prong: 0,
+ Slaps: []teststructs.Slap{
+ ... // 2 identical elements
+ {},
+ {},
+ {
+ Name: "",
+ Desc: "",
+ DescLong: "",
+- Args: s"metadata",
++ Args: s"metadata2",
+ Tense: 0,
+ Interval: 0,
+ ... // 3 identical fields
+ },
+ },
+ StateGoverner: "",
+ PrankRating: "",
+ ... // 2 identical fields
+ }
+`,
}, {
label: label,
x: createEagle(),
@@ -1814,21 +2058,78 @@ func project1Tests() []test {
}(),
opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)},
wantDiff: `
-{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.ID:
- -: "southbay2"
- +: "southbay"
-*{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.State:
- -: testprotos.Goat_States(6)
- +: testprotos.Goat_States(5)
-{teststructs.Eagle}.Slaps[0].Immutable.MildSlap:
- -: false
- +: true
-{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[1->?]:
- -: "bar"
- +: <non-existent>
-{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[2->?]:
- -: "baz"
- +: <non-existent>`,
+ teststructs.Eagle{
+ ... // 2 identical fields
+ Desc: "some description",
+ DescLong: "",
+ Dreamers: []teststructs.Dreamer{
+ {},
+ {
+ ... // 4 identical fields
+ ContSlaps: nil,
+ ContSlapsInterval: 0,
+ Animal: []interface{}{
+ teststructs.Goat{
+ Target: "corporation",
+ Slaps: nil,
+ FunnyPrank: "",
+ Immutable: &teststructs.GoatImmutable{
+- ID: "southbay2",
++ ID: "southbay",
+- State: &6,
++ State: &5,
+ Started: s"2009-11-10 23:00:00 +0000 UTC",
+ Stopped: s"0001-01-01 00:00:00 +0000 UTC",
+ ... // 1 ignored and 1 identical fields
+ },
+ },
+ teststructs.Donkey{},
+ },
+ Ornamental: false,
+ Amoeba: 53,
+ ... // 5 identical fields
+ },
+ },
+ Prong: 0,
+ Slaps: []teststructs.Slap{
+ {
+ ... // 6 identical fields
+ Homeland: 0x00,
+ FunnyPrank: "",
+ Immutable: &teststructs.SlapImmutable{
+ ID: "immutableSlap",
+ Out: nil,
+- MildSlap: false,
++ MildSlap: true,
+ PrettyPrint: "",
+ State: nil,
+ Started: s"2009-11-10 23:00:00 +0000 UTC",
+ Stopped: s"0001-01-01 00:00:00 +0000 UTC",
+ LastUpdate: s"0001-01-01 00:00:00 +0000 UTC",
+ LoveRadius: &teststructs.LoveRadius{
+ Summer: &teststructs.SummerLove{
+ Summary: &teststructs.SummerLoveSummary{
+ Devices: []string{
+ "foo",
+- "bar",
+- "baz",
+ },
+ ChangeType: []testprotos.SummerType{1, 2, 3},
+ ... // 1 ignored field
+ },
+ ... // 1 ignored field
+ },
+ ... // 1 ignored field
+ },
+ ... // 1 ignored field
+ },
+ },
+ },
+ StateGoverner: "",
+ PrankRating: "",
+ ... // 2 identical fields
+ }
+`,
}}
}
@@ -1908,12 +2209,21 @@ func project2Tests() []test {
}(),
opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish},
wantDiff: `
-{teststructs.GermBatch}.DirtyGerms[18][0->?]:
- -: s"germ2"
- +: <non-existent>
-{teststructs.GermBatch}.DirtyGerms[18][?->2]:
- -: <non-existent>
- +: s"germ2"`,
+ teststructs.GermBatch{
+ DirtyGerms: map[int32][]*testprotos.Germ{
+ 17: {s"germ1"},
+ 18: {
+- s"germ2",
+ s"germ3",
+ s"germ4",
++ s"germ2",
+ },
+ },
+ CleanGerms: nil,
+ GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"},
+ ... // 7 identical fields
+ }
+`,
}, {
label: label,
x: createBatch(),
@@ -1940,18 +2250,32 @@ func project2Tests() []test {
}(),
opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish},
wantDiff: `
-{teststructs.GermBatch}.DirtyGerms[17]:
- -: <non-existent>
- +: []*testprotos.Germ{s"germ1"}
-Sort({teststructs.GermBatch}.DirtyGerms[18])[2->?]:
- -: s"germ4"
- +: <non-existent>
-{teststructs.GermBatch}.DishMap[1]:
- -: (*teststructs.Dish)(nil)
- +: &teststructs.Dish{err: &errors.errorString{s: "unexpected EOF"}}
-{teststructs.GermBatch}.GermStrain:
- -: 421
- +: 22`,
+ teststructs.GermBatch{
+ DirtyGerms: map[int32][]*testprotos.Germ{
++ 17: {s"germ1"},
+ 18: Inverse(Sort, []*testprotos.Germ{
+ s"germ2",
+ s"germ3",
+- s"germ4",
+ }),
+ },
+ CleanGerms: nil,
+ GermMap: map[int32]*testprotos.Germ{13: s"germ13", 21: s"germ21"},
+ DishMap: map[int32]*teststructs.Dish{
+ 0: &{err: &errors.errorString{s: "EOF"}},
+- 1: nil,
++ 1: &{err: &errors.errorString{s: "unexpected EOF"}},
+ 2: &{pb: &testprotos.Dish{Stringer: testprotos.Stringer{X: "dish"}}},
+ },
+ HasPreviousResult: true,
+ DirtyID: 10,
+ CleanID: 0,
+- GermStrain: 421,
++ GermStrain: 22,
+ TotalDirtyGerms: 0,
+ InfectedAt: s"2009-11-10 23:00:00 +0000 UTC",
+ }
+`,
}}
}
@@ -2022,21 +2346,24 @@ func project3Tests() []test {
}(),
opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable},
wantDiff: `
-{teststructs.Dirt}.table:
- -: &teststructs.MockTable{state: []string{"a", "c"}}
- +: &teststructs.MockTable{state: []string{"a", "b", "c"}}
-{teststructs.Dirt}.Discord:
- -: teststructs.DiscordState(554)
- +: teststructs.DiscordState(500)
-λ({teststructs.Dirt}.Proto):
- -: s"blah"
- +: s"proto"
-{teststructs.Dirt}.wizard["albus"]:
- -: s"dumbledore"
- +: <non-existent>
-{teststructs.Dirt}.wizard["harry"]:
- -: s"potter"
- +: s"otter"`,
+ teststructs.Dirt{
+- table: &teststructs.MockTable{state: []string{"a", "c"}},
++ table: &teststructs.MockTable{state: []string{"a", "b", "c"}},
+ ts: 12345,
+- Discord: 554,
++ Discord: 500,
+- Proto: testprotos.Dirt(Inverse(λ, s"blah")),
++ Proto: testprotos.Dirt(Inverse(λ, s"proto")),
+ wizard: map[string]*testprotos.Wizard{
+- "albus": s"dumbledore",
+- "harry": s"potter",
++ "harry": s"otter",
+ },
+ sadistic: nil,
+ lastTime: 54321,
+ ... // 1 ignored field
+ }
+`,
}}
}
@@ -2115,21 +2442,47 @@ func project4Tests() []test {
}(),
opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)},
wantDiff: `
-{teststructs.Cartel}.Headquarter.subDivisions[0->?]:
- -: "alpha"
- +: <non-existent>
-{teststructs.Cartel}.Headquarter.publicMessage[2]:
- -: 0x03
- +: 0x04
-{teststructs.Cartel}.Headquarter.publicMessage[3]:
- -: 0x04
- +: 0x03
-{teststructs.Cartel}.poisons[0].poisonType:
- -: testprotos.PoisonType(1)
- +: testprotos.PoisonType(5)
-{teststructs.Cartel}.poisons[1->?]:
- -: &teststructs.Poison{poisonType: testprotos.PoisonType(2), manufacturer: "acme2"}
- +: <non-existent>`,
+ teststructs.Cartel{
+ Headquarter: teststructs.Headquarter{
+ id: 0x05,
+ location: "moon",
+ subDivisions: []string{
+- "alpha",
+ "bravo",
+ "charlie",
+ },
+ incorporatedDate: s"0001-01-01 00:00:00 +0000 UTC",
+ metaData: s"metadata",
+ privateMessage: nil,
+ publicMessage: []uint8{
+ 0x01,
+ 0x02,
+- 0x03,
++ 0x04,
+- 0x04,
++ 0x03,
+ 0x05,
+ },
+ horseBack: "abcdef",
+ rattle: "",
+ ... // 5 identical fields
+ },
+ source: "mars",
+ creationDate: s"0001-01-01 00:00:00 +0000 UTC",
+ boss: "al capone",
+ lastCrimeDate: s"0001-01-01 00:00:00 +0000 UTC",
+ poisons: []*teststructs.Poison{
+ &{
+- poisonType: 1,
++ poisonType: 5,
+ expiration: s"2009-11-10 23:00:00 +0000 UTC",
+ manufacturer: "acme",
+ potency: 0,
+ },
+- &{poisonType: 2, manufacturer: "acme2"},
+ },
+ }
+`,
}}
}
diff --git a/cmp/example_test.go b/cmp/example_test.go
index 5507e0b..5954780 100644
--- a/cmp/example_test.go
+++ b/cmp/example_test.go
@@ -7,9 +7,11 @@ package cmp_test
import (
"fmt"
"math"
+ "net"
"reflect"
"sort"
"strings"
+ "time"
"github.com/google/go-cmp/cmp"
)
@@ -18,108 +20,41 @@ import (
// fundamental options and filters and not in terms of what cool things you can
// do with them since that overlaps with cmp/cmpopts.
-// Use Diff for printing out human-readable errors for test cases comparing
-// nested or structured data.
+// Use Diff to print out a human-readable report of differences for tests
+// comparing nested or structured data.
func ExampleDiff_testing() {
- // Code under test:
- type ShipManifest struct {
- Name string
- Crew map[string]string
- Androids int
- Stolen bool
- }
-
- // AddCrew tries to add the given crewmember to the manifest.
- AddCrew := func(m *ShipManifest, name, title string) {
- if m.Crew == nil {
- m.Crew = make(map[string]string)
- }
- m.Crew[title] = name
- }
+ // Let got be the hypothetical value obtained from some logic under test
+ // and want be the expected golden data.
+ got, want := MakeGatewayInfo()
- // Test function:
- tests := []struct {
- desc string
- before *ShipManifest
- name, title string
- after *ShipManifest
- }{
- {
- desc: "add to empty",
- before: &ShipManifest{},
- name: "Zaphod Beeblebrox",
- title: "Galactic President",
- after: &ShipManifest{
- Crew: map[string]string{
- "Zaphod Beeblebrox": "Galactic President",
- },
- },
- },
- {
- desc: "add another",
- before: &ShipManifest{
- Crew: map[string]string{
- "Zaphod Beeblebrox": "Galactic President",
- },
- },
- name: "Trillian",
- title: "Human",
- after: &ShipManifest{
- Crew: map[string]string{
- "Zaphod Beeblebrox": "Galactic President",
- "Trillian": "Human",
- },
- },
- },
- {
- desc: "overwrite",
- before: &ShipManifest{
- Crew: map[string]string{
- "Zaphod Beeblebrox": "Galactic President",
- },
- },
- name: "Zaphod Beeblebrox",
- title: "Just this guy, you know?",
- after: &ShipManifest{
- Crew: map[string]string{
- "Zaphod Beeblebrox": "Just this guy, you know?",
- },
- },
- },
- }
-
- var t fakeT
- for _, test := range tests {
- AddCrew(test.before, test.name, test.title)
- if diff := cmp.Diff(test.before, test.after); diff != "" {
- t.Errorf("%s: after AddCrew, manifest differs: (-want +got)\n%s", test.desc, diff)
- }
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}
// Output:
- // add to empty: after AddCrew, manifest differs: (-want +got)
- // {*cmp_test.ShipManifest}.Crew["Galactic President"]:
- // -: "Zaphod Beeblebrox"
- // +: <non-existent>
- // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]:
- // -: <non-existent>
- // +: "Galactic President"
- //
- // add another: after AddCrew, manifest differs: (-want +got)
- // {*cmp_test.ShipManifest}.Crew["Human"]:
- // -: "Trillian"
- // +: <non-existent>
- // {*cmp_test.ShipManifest}.Crew["Trillian"]:
- // -: <non-existent>
- // +: "Human"
- //
- // overwrite: after AddCrew, manifest differs: (-want +got)
- // {*cmp_test.ShipManifest}.Crew["Just this guy, you know?"]:
- // -: "Zaphod Beeblebrox"
- // +: <non-existent>
- // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]:
- // -: "Galactic President"
- // +: "Just this guy, you know?"
+ // MakeGatewayInfo() mismatch (-want +got):
+ // cmp_test.Gateway{
+ // SSID: "CoffeeShopWiFi",
+ // - IPAddress: s"192.168.0.2",
+ // + IPAddress: s"192.168.0.1",
+ // NetMask: net.IPMask{0xff, 0xff, 0x00, 0x00},
+ // Clients: []cmp_test.Client{
+ // ... // 2 identical elements
+ // {Hostname: "macchiato", IPAddress: s"192.168.0.153", LastSeen: s"2009-11-10 23:39:43 +0000 UTC"},
+ // {Hostname: "espresso", IPAddress: s"192.168.0.121"},
+ // {
+ // Hostname: "latte",
+ // - IPAddress: s"192.168.0.221",
+ // + IPAddress: s"192.168.0.219",
+ // LastSeen: s"2009-11-10 23:00:23 +0000 UTC",
+ // },
+ // + {
+ // + Hostname: "americano",
+ // + IPAddress: s"192.168.0.188",
+ // + LastSeen: s"2009-11-10 23:03:05 +0000 UTC",
+ // + },
+ // },
+ // }
}
// Approximate equality for floats can be handled by defining a custom
@@ -364,6 +299,78 @@ func ExampleOption_transformComplex() {
// false
}
+type (
+ Gateway struct {
+ SSID string
+ IPAddress net.IP
+ NetMask net.IPMask
+ Clients []Client
+ }
+ Client struct {
+ Hostname string
+ IPAddress net.IP
+ LastSeen time.Time
+ }
+)
+
+func MakeGatewayInfo() (x, y Gateway) {
+ x = Gateway{
+ SSID: "CoffeeShopWiFi",
+ IPAddress: net.IPv4(192, 168, 0, 1),
+ NetMask: net.IPv4Mask(255, 255, 0, 0),
+ Clients: []Client{{
+ Hostname: "ristretto",
+ IPAddress: net.IPv4(192, 168, 0, 116),
+ }, {
+ Hostname: "aribica",
+ IPAddress: net.IPv4(192, 168, 0, 104),
+ LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC),
+ }, {
+ Hostname: "macchiato",
+ IPAddress: net.IPv4(192, 168, 0, 153),
+ LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC),
+ }, {
+ Hostname: "espresso",
+ IPAddress: net.IPv4(192, 168, 0, 121),
+ }, {
+ Hostname: "latte",
+ IPAddress: net.IPv4(192, 168, 0, 219),
+ LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC),
+ }, {
+ Hostname: "americano",
+ IPAddress: net.IPv4(192, 168, 0, 188),
+ LastSeen: time.Date(2009, time.November, 10, 23, 3, 5, 0, time.UTC),
+ }},
+ }
+ y = Gateway{
+ SSID: "CoffeeShopWiFi",
+ IPAddress: net.IPv4(192, 168, 0, 2),
+ NetMask: net.IPv4Mask(255, 255, 0, 0),
+ Clients: []Client{{
+ Hostname: "ristretto",
+ IPAddress: net.IPv4(192, 168, 0, 116),
+ }, {
+ Hostname: "aribica",
+ IPAddress: net.IPv4(192, 168, 0, 104),
+ LastSeen: time.Date(2009, time.November, 10, 23, 6, 32, 0, time.UTC),
+ }, {
+ Hostname: "macchiato",
+ IPAddress: net.IPv4(192, 168, 0, 153),
+ LastSeen: time.Date(2009, time.November, 10, 23, 39, 43, 0, time.UTC),
+ }, {
+ Hostname: "espresso",
+ IPAddress: net.IPv4(192, 168, 0, 121),
+ }, {
+ Hostname: "latte",
+ IPAddress: net.IPv4(192, 168, 0, 221),
+ LastSeen: time.Date(2009, time.November, 10, 23, 0, 23, 0, time.UTC),
+ }},
+ }
+ return x, y
+}
+
+var t fakeT
+
type fakeT struct{}
func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) }
diff --git a/cmp/internal/flags/flags.go b/cmp/internal/flags/flags.go
new file mode 100644
index 0000000..a9e7fc0
--- /dev/null
+++ b/cmp/internal/flags/flags.go
@@ -0,0 +1,9 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package flags
+
+// Deterministic controls whether the output of Diff should be deterministic.
+// This is only used for testing.
+var Deterministic bool
diff --git a/cmp/internal/flags/toolchain_legacy.go b/cmp/internal/flags/toolchain_legacy.go
new file mode 100644
index 0000000..01aed0a
--- /dev/null
+++ b/cmp/internal/flags/toolchain_legacy.go
@@ -0,0 +1,10 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+// +build !go1.10
+
+package flags
+
+// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10.
+const AtLeastGo110 = false
diff --git a/cmp/internal/flags/toolchain_recent.go b/cmp/internal/flags/toolchain_recent.go
new file mode 100644
index 0000000..c0b667f
--- /dev/null
+++ b/cmp/internal/flags/toolchain_recent.go
@@ -0,0 +1,10 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+// +build go1.10
+
+package flags
+
+// AtLeastGo110 reports whether the Go toolchain is at least Go 1.10.
+const AtLeastGo110 = true
diff --git a/cmp/internal/value/format.go b/cmp/internal/value/format.go
deleted file mode 100644
index bafb2d1..0000000
--- a/cmp/internal/value/format.go
+++ /dev/null
@@ -1,280 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-// Package value provides functionality for reflect.Value types.
-package value
-
-import (
- "fmt"
- "reflect"
- "strconv"
- "strings"
- "unicode"
-)
-
-var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
-
-// Format formats the value v as a string.
-//
-// This is similar to fmt.Sprintf("%+v", v) except this:
-// * Prints the type unless it can be elided
-// * Avoids printing struct fields that are zero
-// * Prints a nil-slice as being nil, not empty
-// * Prints map entries in deterministic order
-func Format(v reflect.Value, conf FormatConfig) string {
- conf.printType = true
- conf.followPointers = true
- conf.realPointers = true
- return formatAny(v, conf, visited{})
-}
-
-type FormatConfig struct {
- UseStringer bool // Should the String method be used if available?
- printType bool // Should we print the type before the value?
- PrintPrimitiveType bool // Should we print the type of primitives?
- followPointers bool // Should we recursively follow pointers?
- realPointers bool // Should we print the real address of pointers?
-}
-
-func formatAny(v reflect.Value, conf FormatConfig, m visited) string {
- // TODO: Should this be a multi-line printout in certain situations?
-
- if !v.IsValid() {
- return "<non-existent>"
- }
- if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
- if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
- return "<nil>"
- }
-
- const stringerPrefix = "s" // Indicates that the String method was used
- s := v.Interface().(fmt.Stringer).String()
- return stringerPrefix + formatString(s)
- }
-
- switch v.Kind() {
- case reflect.Bool:
- return formatPrimitive(v.Type(), v.Bool(), conf)
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return formatPrimitive(v.Type(), v.Int(), conf)
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr {
- // Unnamed uints are usually bytes or words, so use hexadecimal.
- return formatPrimitive(v.Type(), formatHex(v.Uint()), conf)
- }
- return formatPrimitive(v.Type(), v.Uint(), conf)
- case reflect.Float32, reflect.Float64:
- return formatPrimitive(v.Type(), v.Float(), conf)
- case reflect.Complex64, reflect.Complex128:
- return formatPrimitive(v.Type(), v.Complex(), conf)
- case reflect.String:
- return formatPrimitive(v.Type(), formatString(v.String()), conf)
- case reflect.UnsafePointer, reflect.Chan, reflect.Func:
- return formatPointer(v, conf)
- case reflect.Ptr:
- if v.IsNil() {
- if conf.printType {
- return fmt.Sprintf("(%v)(nil)", v.Type())
- }
- return "<nil>"
- }
- if m.Visit(v) || !conf.followPointers {
- return formatPointer(v, conf)
- }
- return "&" + formatAny(v.Elem(), conf, m)
- case reflect.Interface:
- if v.IsNil() {
- if conf.printType {
- return fmt.Sprintf("%v(nil)", v.Type())
- }
- return "<nil>"
- }
- return formatAny(v.Elem(), conf, m)
- case reflect.Slice:
- if v.IsNil() {
- if conf.printType {
- return fmt.Sprintf("%v(nil)", v.Type())
- }
- return "<nil>"
- }
- fallthrough
- case reflect.Array:
- var ss []string
- subConf := conf
- subConf.printType = v.Type().Elem().Kind() == reflect.Interface
- for i := 0; i < v.Len(); i++ {
- vi := v.Index(i)
- if vi.CanAddr() { // Check for recursive elements
- p := vi.Addr()
- if m.Visit(p) {
- subConf := conf
- subConf.printType = true
- ss = append(ss, "*"+formatPointer(p, subConf))
- continue
- }
- }
- ss = append(ss, formatAny(vi, subConf, m))
- }
- s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
- if conf.printType {
- return v.Type().String() + s
- }
- return s
- case reflect.Map:
- if v.IsNil() {
- if conf.printType {
- return fmt.Sprintf("%v(nil)", v.Type())
- }
- return "<nil>"
- }
- if m.Visit(v) {
- return formatPointer(v, conf)
- }
-
- var ss []string
- keyConf, valConf := conf, conf
- keyConf.printType = v.Type().Key().Kind() == reflect.Interface
- keyConf.followPointers = false
- valConf.printType = v.Type().Elem().Kind() == reflect.Interface
- for _, k := range SortKeys(v.MapKeys()) {
- sk := formatAny(k, keyConf, m)
- sv := formatAny(v.MapIndex(k), valConf, m)
- ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
- }
- s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
- if conf.printType {
- return v.Type().String() + s
- }
- return s
- case reflect.Struct:
- var ss []string
- subConf := conf
- subConf.printType = true
- for i := 0; i < v.NumField(); i++ {
- vv := v.Field(i)
- if isZero(vv) {
- continue // Elide zero value fields
- }
- name := v.Type().Field(i).Name
- subConf.UseStringer = conf.UseStringer
- s := formatAny(vv, subConf, m)
- ss = append(ss, fmt.Sprintf("%s: %s", name, s))
- }
- s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
- if conf.printType {
- return v.Type().String() + s
- }
- return s
- default:
- panic(fmt.Sprintf("%v kind not handled", v.Kind()))
- }
-}
-
-func formatString(s string) string {
- // Use quoted string if it the same length as a raw string literal.
- // Otherwise, attempt to use the raw string form.
- qs := strconv.Quote(s)
- if len(qs) == 1+len(s)+1 {
- return qs
- }
-
- // Disallow newlines to ensure output is a single line.
- // Only allow printable runes for readability purposes.
- rawInvalid := func(r rune) bool {
- return r == '`' || r == '\n' || !unicode.IsPrint(r)
- }
- if strings.IndexFunc(s, rawInvalid) < 0 {
- return "`" + s + "`"
- }
- return qs
-}
-
-func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string {
- if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") {
- return fmt.Sprintf("%v(%v)", t, v)
- }
- return fmt.Sprintf("%v", v)
-}
-
-func formatPointer(v reflect.Value, conf FormatConfig) string {
- p := v.Pointer()
- if !conf.realPointers {
- p = 0 // For deterministic printing purposes
- }
- s := formatHex(uint64(p))
- if conf.printType {
- return fmt.Sprintf("(%v)(%s)", v.Type(), s)
- }
- return s
-}
-
-func formatHex(u uint64) string {
- var f string
- switch {
- case u <= 0xff:
- f = "0x%02x"
- case u <= 0xffff:
- f = "0x%04x"
- case u <= 0xffffff:
- f = "0x%06x"
- case u <= 0xffffffff:
- f = "0x%08x"
- case u <= 0xffffffffff:
- f = "0x%010x"
- case u <= 0xffffffffffff:
- f = "0x%012x"
- case u <= 0xffffffffffffff:
- f = "0x%014x"
- case u <= 0xffffffffffffffff:
- f = "0x%016x"
- }
- return fmt.Sprintf(f, u)
-}
-
-// isZero reports whether v is the zero value.
-// This does not rely on Interface and so can be used on unexported fields.
-func isZero(v reflect.Value) bool {
- switch v.Kind() {
- case reflect.Bool:
- return v.Bool() == false
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
- return v.Int() == 0
- case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
- return v.Uint() == 0
- case reflect.Float32, reflect.Float64:
- return v.Float() == 0
- case reflect.Complex64, reflect.Complex128:
- return v.Complex() == 0
- case reflect.String:
- return v.String() == ""
- case reflect.UnsafePointer:
- return v.Pointer() == 0
- case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
- return v.IsNil()
- case reflect.Array:
- for i := 0; i < v.Len(); i++ {
- if !isZero(v.Index(i)) {
- return false
- }
- }
- return true
- case reflect.Struct:
- for i := 0; i < v.NumField(); i++ {
- if !isZero(v.Field(i)) {
- return false
- }
- }
- return true
- }
- return false
-}
-
-type visited map[Pointer]bool
-
-func (m visited) Visit(v reflect.Value) bool {
- p := PointerOf(v)
- visited := m[p]
- m[p] = true
- return visited
-}
diff --git a/cmp/internal/value/format_test.go b/cmp/internal/value/format_test.go
deleted file mode 100644
index d676da2..0000000
--- a/cmp/internal/value/format_test.go
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright 2017, The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE.md file.
-
-package value
-
-import (
- "bytes"
- "io"
- "reflect"
- "testing"
-)
-
-func TestFormat(t *testing.T) {
- type key struct {
- a int
- b string
- c chan bool
- }
-
- tests := []struct {
- in interface{}
- want string
- }{{
- in: []int{},
- want: "[]int{}",
- }, {
- in: []int(nil),
- want: "[]int(nil)",
- }, {
- in: []int{1, 2, 3, 4, 5},
- want: "[]int{1, 2, 3, 4, 5}",
- }, {
- in: []interface{}{1, true, "hello", struct{ A, B int }{1, 2}},
- want: "[]interface {}{1, true, \"hello\", struct { A int; B int }{A: 1, B: 2}}",
- }, {
- in: []struct{ A, B int }{{1, 2}, {0, 4}, {}},
- want: "[]struct { A int; B int }{{A: 1, B: 2}, {B: 4}, {}}",
- }, {
- in: map[*int]string{new(int): "hello"},
- want: "map[*int]string{0x00: \"hello\"}",
- }, {
- in: map[key]string{{}: "hello"},
- want: "map[value.key]string{{}: \"hello\"}",
- }, {
- in: map[key]string{{a: 5, b: "key", c: make(chan bool)}: "hello"},
- want: "map[value.key]string{{a: 5, b: \"key\", c: (chan bool)(0x00)}: \"hello\"}",
- }, {
- in: map[io.Reader]string{new(bytes.Reader): "hello"},
- want: "map[io.Reader]string{(*bytes.Reader)(0x00): \"hello\"}",
- }, {
- in: func() interface{} {
- var a = []interface{}{nil}
- a[0] = a
- return a
- }(),
- want: "[]interface {}{[]interface {}{*(*interface {})(0x00)}}",
- }, {
- in: func() interface{} {
- type A *A
- var a A
- a = &a
- return a
- }(),
- want: "&(value.A)(0x00)",
- }, {
- in: func() interface{} {
- type A map[*A]A
- a := make(A)
- a[&a] = a
- return a
- }(),
- want: "value.A{0x00: 0x00}",
- }, {
- in: func() interface{} {
- var a [2]interface{}
- a[0] = &a
- return a
- }(),
- want: "[2]interface {}{&[2]interface {}{(*[2]interface {})(0x00), interface {}(nil)}, interface {}(nil)}",
- }}
-
- for i, tt := range tests {
- // Intentionally retrieve the value through an unexported field to
- // ensure the format logic does not depend on read-write access
- // to the reflect.Value.
- v := reflect.ValueOf(struct{ x interface{} }{tt.in}).Field(0)
- got := formatAny(v, FormatConfig{UseStringer: true, printType: true, followPointers: true}, visited{})
- if got != tt.want {
- t.Errorf("test %d, Format():\ngot %q\nwant %q", i, got, tt.want)
- }
- }
-}
diff --git a/cmp/internal/value/zero.go b/cmp/internal/value/zero.go
new file mode 100644
index 0000000..d13a12c
--- /dev/null
+++ b/cmp/internal/value/zero.go
@@ -0,0 +1,45 @@
+// Copyright 2017, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package value
+
+import "reflect"
+
+// IsZero reports whether v is the zero value.
+// This does not rely on Interface and so can be used on unexported fields.
+func IsZero(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Bool:
+ return v.Bool() == false
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Complex64, reflect.Complex128:
+ return v.Complex() == 0
+ case reflect.String:
+ return v.String() == ""
+ case reflect.UnsafePointer:
+ return v.Pointer() == 0
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
+ return v.IsNil()
+ case reflect.Array:
+ for i := 0; i < v.Len(); i++ {
+ if !IsZero(v.Index(i)) {
+ return false
+ }
+ }
+ return true
+ case reflect.Struct:
+ for i := 0; i < v.NumField(); i++ {
+ if !IsZero(v.Field(i)) {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
diff --git a/cmp/internal/value/zero_test.go b/cmp/internal/value/zero_test.go
new file mode 100644
index 0000000..536d50b
--- /dev/null
+++ b/cmp/internal/value/zero_test.go
@@ -0,0 +1,45 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package value
+
+import (
+ "archive/tar"
+ "reflect"
+ "testing"
+)
+
+func TestIsZero(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ want bool
+ }{
+ {0, true},
+ {1, false},
+ {"", true},
+ {"foo", false},
+ {[]byte(nil), true},
+ {[]byte{}, false},
+ {map[string]bool(nil), true},
+ {map[string]bool{}, false},
+ {tar.Header{}, true},
+ {&tar.Header{}, false},
+ {tar.Header{Name: "foo"}, false},
+ {(chan bool)(nil), true},
+ {make(chan bool), false},
+ {(func(*testing.T))(nil), true},
+ {TestIsZero, false},
+ {[...]int{0, 0, 0}, true},
+ {[...]int{0, 1, 0}, false},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ got := IsZero(reflect.ValueOf(tt.in))
+ if got != tt.want {
+ t.Errorf("IsZero(%v) = %v, want %v", tt.in, got, tt.want)
+ }
+ })
+ }
+}
diff --git a/cmp/path.go b/cmp/path.go
index ad6c0f5..ea9575c 100644
--- a/cmp/path.go
+++ b/cmp/path.go
@@ -198,15 +198,6 @@ func (pa Path) GoString() string {
ssPre = append(ssPre, s.trans.name+"(")
ssPost = append(ssPost, ")")
continue
- case *typeAssertion:
- // As a special-case, elide type assertions on anonymous types
- // since they are typically generated dynamically and can be very
- // verbose. For example, some transforms return interface{} because
- // of Go's lack of generics, but typically take in and return the
- // exact same concrete type.
- if s.Type().PkgPath() == "" {
- continue
- }
}
ssPost = append(ssPost, s.String())
}
diff --git a/cmp/report.go b/cmp/report.go
index 60f3f37..6810a50 100644
--- a/cmp/report.go
+++ b/cmp/report.go
@@ -4,59 +4,48 @@
package cmp
-import (
- "fmt"
- "reflect"
- "strings"
-
- "github.com/google/go-cmp/cmp/internal/value"
-)
-
+// defaultReporter implements the reporter interface.
+//
+// As Equal serially calls the PushStep, Report, and PopStep methods, the
+// defaultReporter constructs a tree-based representation of the compared value
+// and the result of each comparison (see valueNode).
+//
+// When the String method is called, the FormatDiff method transforms the
+// valueNode tree into a textNode tree, which is a tree-based representation
+// of the textual output (see textNode).
+//
+// Lastly, the textNode.String method produces the final report as a string.
type defaultReporter struct {
- curPath Path
-
- diffs []string // List of differences, possibly truncated
- ndiffs int // Total number of differences
- nbytes int // Number of bytes in diffs
- nlines int // Number of lines in diffs
+ root *valueNode
+ curr *valueNode
}
func (r *defaultReporter) PushStep(ps PathStep) {
- r.curPath.push(ps)
+ r.curr = r.curr.PushStep(ps)
+ if r.root == nil {
+ r.root = r.curr
+ }
}
func (r *defaultReporter) Report(f reportFlags) {
- if f&reportUnequal > 0 {
- vx, vy := r.curPath.Last().Values()
- r.report(vx, vy, r.curPath)
- }
+ r.curr.Report(f)
}
func (r *defaultReporter) PopStep() {
- r.curPath.pop()
+ r.curr = r.curr.PopStep()
}
-func (r *defaultReporter) report(x, y reflect.Value, p Path) {
- const maxBytes = 4096
- const maxLines = 256
- r.ndiffs++
- if r.nbytes < maxBytes && r.nlines < maxLines {
- sx := value.Format(x, value.FormatConfig{UseStringer: true})
- sy := value.Format(y, value.FormatConfig{UseStringer: true})
- if sx == sy {
- // Unhelpful output, so use more exact formatting.
- sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true})
- sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true})
- }
- s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
- r.diffs = append(r.diffs, s)
- r.nbytes += len(s)
- r.nlines += strings.Count(s, "\n")
+// String provides a full report of the differences detected as a structured
+// literal in pseudo-Go syntax. String may only be called after the entire tree
+// has been traversed.
+func (r *defaultReporter) String() string {
+ assert(r.root != nil && r.curr == nil)
+ if r.root.NumDiff == 0 {
+ return ""
}
+ return formatOptions{}.FormatDiff(r.root).String()
}
-func (r *defaultReporter) String() string {
- s := strings.Join(r.diffs, "")
- if r.ndiffs == len(r.diffs) {
- return s
+func assert(ok bool) {
+ if !ok {
+ panic("assertion failure")
}
- return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs))
}
diff --git a/cmp/report_compare.go b/cmp/report_compare.go
new file mode 100644
index 0000000..2d782fe
--- /dev/null
+++ b/cmp/report_compare.go
@@ -0,0 +1,290 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmp
+
+import (
+ "fmt"
+ "reflect"
+
+ "github.com/google/go-cmp/cmp/internal/value"
+)
+
+// TODO: Enforce limits?
+// * Enforce maximum number of records to print per node?
+// * Enforce maximum size in bytes allowed?
+// * As a heuristic, use less verbosity for equal nodes than unequal nodes.
+// TODO: Enforce unique outputs?
+// * Avoid Stringer methods if it results in same output?
+// * Print pointer address if outputs still equal?
+
+// numContextRecords is the number of surrounding equal records to print.
+const numContextRecords = 2
+
+type diffMode byte
+
+const (
+ diffUnknown diffMode = 0
+ diffIdentical diffMode = ' '
+ diffRemoved diffMode = '-'
+ diffInserted diffMode = '+'
+)
+
+type typeMode int
+
+const (
+ // emitType always prints the type.
+ emitType typeMode = iota
+ // elideType never prints the type.
+ elideType
+ // autoType prints the type only for composite kinds
+ // (i.e., structs, slices, arrays, and maps).
+ autoType
+)
+
+type formatOptions struct {
+ // DiffMode controls the output mode of FormatDiff.
+ //
+ // If diffUnknown, then produce a diff of the x and y values.
+ // If diffIdentical, then emit values as if they were equal.
+ // If diffRemoved, then only emit x values (ignoring y values).
+ // If diffInserted, then only emit y values (ignoring x values).
+ DiffMode diffMode
+
+ // TypeMode controls whether to print the type for the current node.
+ //
+ // As a general rule of thumb, we always print the type of the next node
+ // after an interface, and always elide the type of the next node after
+ // a slice or map node.
+ TypeMode typeMode
+
+ // formatValueOptions are options specific to printing reflect.Values.
+ formatValueOptions
+}
+
+func (opts formatOptions) WithDiffMode(d diffMode) formatOptions {
+ opts.DiffMode = d
+ return opts
+}
+func (opts formatOptions) WithTypeMode(t typeMode) formatOptions {
+ opts.TypeMode = t
+ return opts
+}
+
+// FormatDiff converts a valueNode tree into a textNode tree, where the later
+// is a textual representation of the differences detected in the former.
+func (opts formatOptions) FormatDiff(v *valueNode) textNode {
+ // TODO: Add specialized formatting for slices of primitives.
+
+ // For leaf nodes, format the value based on the reflect.Values alone.
+ if v.MaxDepth == 0 {
+ switch opts.DiffMode {
+ case diffUnknown, diffIdentical:
+ // Format Equal.
+ if v.NumDiff == 0 {
+ outx := opts.FormatValue(v.ValueX, visitedPointers{})
+ outy := opts.FormatValue(v.ValueY, visitedPointers{})
+ if v.NumIgnored > 0 && v.NumSame == 0 {
+ return textEllipsis
+ } else if outx.Len() < outy.Len() {
+ return outx
+ } else {
+ return outy
+ }
+ }
+
+ // Format unequal.
+ assert(opts.DiffMode == diffUnknown)
+ var list textList
+ outx := opts.WithTypeMode(elideType).FormatValue(v.ValueX, visitedPointers{})
+ outy := opts.WithTypeMode(elideType).FormatValue(v.ValueY, visitedPointers{})
+ if outx != nil {
+ list = append(list, textRecord{Diff: '-', Value: outx})
+ }
+ if outy != nil {
+ list = append(list, textRecord{Diff: '+', Value: outy})
+ }
+ return opts.WithTypeMode(emitType).FormatType(v.Type, list)
+ case diffRemoved:
+ return opts.FormatValue(v.ValueX, visitedPointers{})
+ case diffInserted:
+ return opts.FormatValue(v.ValueY, visitedPointers{})
+ default:
+ panic("invalid diff mode")
+ }
+ }
+
+ // Descend into the child value node.
+ if v.TransformerName != "" {
+ out := opts.WithTypeMode(emitType).FormatDiff(v.Value)
+ out = textWrap{"Inverse(" + v.TransformerName + ", ", out, ")"}
+ return opts.FormatType(v.Type, out)
+ } else {
+ switch k := v.Type.Kind(); k {
+ case reflect.Struct, reflect.Array, reflect.Slice, reflect.Map:
+ return opts.FormatType(v.Type, opts.formatDiffList(v.Records, k))
+ case reflect.Ptr:
+ return textWrap{"&", opts.FormatDiff(v.Value), ""}
+ case reflect.Interface:
+ return opts.WithTypeMode(emitType).FormatDiff(v.Value)
+ default:
+ panic(fmt.Sprintf("%v cannot have children", k))
+ }
+ }
+}
+
+func (opts formatOptions) formatDiffList(recs []reportRecord, k reflect.Kind) textNode {
+ // Derive record name based on the data structure kind.
+ var name string
+ var formatKey func(reflect.Value) string
+ switch k {
+ case reflect.Struct:
+ name = "field"
+ opts = opts.WithTypeMode(autoType)
+ formatKey = func(v reflect.Value) string { return v.String() }
+ case reflect.Slice, reflect.Array:
+ name = "element"
+ opts = opts.WithTypeMode(elideType)
+ formatKey = func(reflect.Value) string { return "" }
+ case reflect.Map:
+ name = "entry"
+ opts = opts.WithTypeMode(elideType)
+ formatKey = formatMapKey
+ }
+
+ // Handle unification.
+ switch opts.DiffMode {
+ case diffIdentical, diffRemoved, diffInserted:
+ var list textList
+ var deferredEllipsis bool // Add final "..." to indicate records were dropped
+ for _, r := range recs {
+ // Elide struct fields that are zero value.
+ if k == reflect.Struct {
+ var isZero bool
+ switch opts.DiffMode {
+ case diffIdentical:
+ isZero = value.IsZero(r.Value.ValueX) || value.IsZero(r.Value.ValueX)
+ case diffRemoved:
+ isZero = value.IsZero(r.Value.ValueX)
+ case diffInserted:
+ isZero = value.IsZero(r.Value.ValueY)
+ }
+ if isZero {
+ continue
+ }
+ }
+ // Elide ignored nodes.
+ if r.Value.NumIgnored > 0 && r.Value.NumSame+r.Value.NumDiff == 0 {
+ deferredEllipsis = !(k == reflect.Slice || k == reflect.Array)
+ if !deferredEllipsis {
+ list.AppendEllipsis(diffStats{})
+ }
+ continue
+ }
+ if out := opts.FormatDiff(r.Value); out != nil {
+ list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
+ }
+ }
+ if deferredEllipsis {
+ list.AppendEllipsis(diffStats{})
+ }
+ return textWrap{"{", list, "}"}
+ case diffUnknown:
+ default:
+ panic("invalid diff mode")
+ }
+
+ // Handle differencing.
+ var list textList
+ groups := coalesceAdjacentRecords(name, recs)
+ for i, ds := range groups {
+ // Handle equal records.
+ if ds.NumDiff() == 0 {
+ // Compute the number of leading and trailing records to print.
+ var numLo, numHi int
+ numEqual := ds.NumIgnored + ds.NumIdentical
+ for numLo < numContextRecords && numLo+numHi < numEqual && i != 0 {
+ if r := recs[numLo].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
+ break
+ }
+ numLo++
+ }
+ for numHi < numContextRecords && numLo+numHi < numEqual && i != len(groups)-1 {
+ if r := recs[numEqual-numHi-1].Value; r.NumIgnored > 0 && r.NumSame+r.NumDiff == 0 {
+ break
+ }
+ numHi++
+ }
+ if numEqual-(numLo+numHi) == 1 && ds.NumIgnored == 0 {
+ numHi++ // Avoid pointless coalescing of a single equal record
+ }
+
+ // Format the equal values.
+ for _, r := range recs[:numLo] {
+ out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
+ list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
+ }
+ if numEqual > numLo+numHi {
+ ds.NumIdentical -= numLo + numHi
+ list.AppendEllipsis(ds)
+ }
+ for _, r := range recs[numEqual-numHi : numEqual] {
+ out := opts.WithDiffMode(diffIdentical).FormatDiff(r.Value)
+ list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
+ }
+ recs = recs[numEqual:]
+ continue
+ }
+
+ // Handle unequal records.
+ for _, r := range recs[:ds.NumDiff()] {
+ switch {
+ // TODO: Add specialized formatting for slices of primitives.
+ case r.Value.NumChildren == r.Value.MaxDepth:
+ outx := opts.WithDiffMode(diffRemoved).FormatDiff(r.Value)
+ outy := opts.WithDiffMode(diffInserted).FormatDiff(r.Value)
+ if outx != nil {
+ list = append(list, textRecord{Diff: diffRemoved, Key: formatKey(r.Key), Value: outx})
+ }
+ if outy != nil {
+ list = append(list, textRecord{Diff: diffInserted, Key: formatKey(r.Key), Value: outy})
+ }
+ default:
+ out := opts.FormatDiff(r.Value)
+ list = append(list, textRecord{Key: formatKey(r.Key), Value: out})
+ }
+ }
+ recs = recs[ds.NumDiff():]
+ }
+ assert(len(recs) == 0)
+ return textWrap{"{", list, "}"}
+}
+
+// coalesceAdjacentRecords coalesces the list of records into groups of
+// adjacent equal, or unequal counts.
+func coalesceAdjacentRecords(name string, recs []reportRecord) (groups []diffStats) {
+ var prevCase int // Arbitrary index into which case last occurred
+ lastStats := func(i int) *diffStats {
+ if prevCase != i {
+ groups = append(groups, diffStats{Name: name})
+ prevCase = i
+ }
+ return &groups[len(groups)-1]
+ }
+ for _, r := range recs {
+ switch rv := r.Value; {
+ case rv.NumIgnored > 0 && rv.NumSame+rv.NumDiff == 0:
+ lastStats(1).NumIgnored++
+ case rv.NumDiff == 0:
+ lastStats(1).NumIdentical++
+ case rv.NumDiff > 0 && !rv.ValueY.IsValid():
+ lastStats(2).NumRemoved++
+ case rv.NumDiff > 0 && !rv.ValueX.IsValid():
+ lastStats(2).NumInserted++
+ default:
+ lastStats(2).NumModified++
+ }
+ }
+ return groups
+}
diff --git a/cmp/report_reflect.go b/cmp/report_reflect.go
new file mode 100644
index 0000000..5521c60
--- /dev/null
+++ b/cmp/report_reflect.go
@@ -0,0 +1,279 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmp
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "github.com/google/go-cmp/cmp/internal/flags"
+ "github.com/google/go-cmp/cmp/internal/value"
+)
+
+type formatValueOptions struct {
+ // AvoidStringer controls whether to avoid calling custom stringer
+ // methods like error.Error or fmt.Stringer.String.
+ AvoidStringer bool
+
+ // ShallowPointers controls whether to avoid descending into pointers.
+ // Useful when printing map keys, where pointer comparison is performed
+ // on the pointer address rather than the pointed-at value.
+ ShallowPointers bool
+
+ // PrintAddresses controls whether to print the address of all pointers,
+ // slice elements, and maps.
+ PrintAddresses bool
+}
+
+// FormatType prints the type as if it were wrapping s.
+// This may return s as-is depending on the current type and TypeMode mode.
+func (opts formatOptions) FormatType(t reflect.Type, s textNode) textNode {
+ // Check whether to emit the type or not.
+ switch opts.TypeMode {
+ case autoType:
+ switch t.Kind() {
+ case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map:
+ if s.Equal(textNil) {
+ return s
+ }
+ default:
+ return s
+ }
+ case elideType:
+ return s
+ }
+
+ // Determine the type label, applying special handling for unnamed types.
+ typeName := t.String()
+ if t.Name() == "" {
+ // According to Go grammar, certain type literals contain symbols that
+ // do not strongly bind to the next lexicographical token (e.g., *T).
+ switch t.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Ptr:
+ typeName = "(" + typeName + ")"
+ }
+ typeName = strings.Replace(typeName, "struct {", "struct{", -1)
+ typeName = strings.Replace(typeName, "interface {", "interface{", -1)
+ }
+
+ // Avoid wrap the value in parenthesis if unnecessary.
+ if s, ok := s.(textWrap); ok {
+ hasParens := strings.HasPrefix(s.Prefix, "(") && strings.HasSuffix(s.Suffix, ")")
+ hasBraces := strings.HasPrefix(s.Prefix, "{") && strings.HasSuffix(s.Suffix, "}")
+ if hasParens || hasBraces {
+ return textWrap{typeName, s, ""}
+ }
+ }
+ return textWrap{typeName + "(", s, ")"}
+}
+
+// FormatValue prints the reflect.Value, taking extra care to avoid descending
+// into pointers already in m. As pointers are visited, m is also updated.
+func (opts formatOptions) FormatValue(v reflect.Value, m visitedPointers) (out textNode) {
+ if !v.IsValid() {
+ return nil
+ }
+ t := v.Type()
+
+ // Check whether there is an Error or String method to call.
+ if !opts.AvoidStringer && v.CanInterface() {
+ // Avoid calling Error or String methods on nil receivers since many
+ // implementations crash when doing so.
+ if (t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface) || !v.IsNil() {
+ switch v := v.Interface().(type) {
+ case error:
+ return textLine("e" + formatString(v.Error()))
+ case fmt.Stringer:
+ return textLine("s" + formatString(v.String()))
+ }
+ }
+ }
+
+ // Check whether to explicitly wrap the result with the type.
+ var skipType bool
+ defer func() {
+ if !skipType {
+ out = opts.FormatType(t, out)
+ }
+ }()
+
+ var ptr string
+ switch t.Kind() {
+ case reflect.Bool:
+ return textLine(fmt.Sprint(v.Bool()))
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return textLine(fmt.Sprint(v.Int()))
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ // Unnamed uints are usually bytes or words, so use hexadecimal.
+ if t.PkgPath() == "" || t.Kind() == reflect.Uintptr {
+ return textLine(formatHex(v.Uint()))
+ }
+ return textLine(fmt.Sprint(v.Uint()))
+ case reflect.Float32, reflect.Float64:
+ return textLine(fmt.Sprint(v.Float()))
+ case reflect.Complex64, reflect.Complex128:
+ return textLine(fmt.Sprint(v.Complex()))
+ case reflect.String:
+ return textLine(formatString(v.String()))
+ case reflect.UnsafePointer, reflect.Chan, reflect.Func:
+ return textLine(formatPointer(v))
+ case reflect.Struct:
+ var list textList
+ for i := 0; i < v.NumField(); i++ {
+ vv := v.Field(i)
+ if value.IsZero(vv) {
+ continue // Elide fields with zero values
+ }
+ s := opts.WithTypeMode(autoType).FormatValue(vv, m)
+ list = append(list, textRecord{Key: t.Field(i).Name, Value: s})
+ }
+ return textWrap{"{", list, "}"}
+ case reflect.Slice:
+ if v.IsNil() {
+ return textNil
+ }
+ if opts.PrintAddresses {
+ ptr = formatPointer(v)
+ }
+ fallthrough
+ case reflect.Array:
+ var list textList
+ for i := 0; i < v.Len(); i++ {
+ vi := v.Index(i)
+ if vi.CanAddr() { // Check for cyclic elements
+ p := vi.Addr()
+ if m.Visit(p) {
+ var out textNode
+ out = textLine(formatPointer(p))
+ out = opts.WithTypeMode(emitType).FormatType(p.Type(), out)
+ out = textWrap{"*", out, ""}
+ list = append(list, textRecord{Value: out})
+ continue
+ }
+ }
+ s := opts.WithTypeMode(elideType).FormatValue(vi, m)
+ list = append(list, textRecord{Value: s})
+ }
+ return textWrap{ptr + "{", list, "}"}
+ case reflect.Map:
+ if v.IsNil() {
+ return textNil
+ }
+ if m.Visit(v) {
+ return textLine(formatPointer(v))
+ }
+
+ var list textList
+ for _, k := range value.SortKeys(v.MapKeys()) {
+ sk := formatMapKey(k)
+ sv := opts.WithTypeMode(elideType).FormatValue(v.MapIndex(k), m)
+ list = append(list, textRecord{Key: sk, Value: sv})
+ }
+ if opts.PrintAddresses {
+ ptr = formatPointer(v)
+ }
+ return textWrap{ptr + "{", list, "}"}
+ case reflect.Ptr:
+ if v.IsNil() {
+ return textNil
+ }
+ if m.Visit(v) || opts.ShallowPointers {
+ return textLine(formatPointer(v))
+ }
+ if opts.PrintAddresses {
+ ptr = formatPointer(v)
+ }
+ skipType = true // Let the underlying value print the type instead
+ return textWrap{"&" + ptr, opts.FormatValue(v.Elem(), m), ""}
+ case reflect.Interface:
+ if v.IsNil() {
+ return textNil
+ }
+ // Interfaces accept different concrete types,
+ // so configure the underlying value to explicitly print the type.
+ skipType = true // Print the concrete type instead
+ return opts.WithTypeMode(emitType).FormatValue(v.Elem(), m)
+ default:
+ panic(fmt.Sprintf("%v kind not handled", v.Kind()))
+ }
+}
+
+// formatMapKey formats v as if it were a map key.
+// The result is guaranteed to be a single line.
+func formatMapKey(v reflect.Value) string {
+ var opts formatOptions
+ opts.TypeMode = elideType
+ opts.AvoidStringer = true
+ opts.ShallowPointers = true
+ s := opts.FormatValue(v, visitedPointers{}).String()
+ return strings.TrimSpace(s)
+}
+
+// formatString prints s as a double-quoted or backtick-quoted string.
+func formatString(s string) string {
+ // Use quoted string if it the same length as a raw string literal.
+ // Otherwise, attempt to use the raw string form.
+ qs := strconv.Quote(s)
+ if len(qs) == 1+len(s)+1 {
+ return qs
+ }
+
+ // Disallow newlines to ensure output is a single line.
+ // Only allow printable runes for readability purposes.
+ rawInvalid := func(r rune) bool {
+ return r == '`' || r == '\n' || !(unicode.IsPrint(r) || r == '\t')
+ }
+ if strings.IndexFunc(s, rawInvalid) < 0 {
+ return "`" + s + "`"
+ }
+ return qs
+}
+
+// formatHex prints u as a hexadecimal integer in Go notation.
+func formatHex(u uint64) string {
+ var f string
+ switch {
+ case u <= 0xff:
+ f = "0x%02x"
+ case u <= 0xffff:
+ f = "0x%04x"
+ case u <= 0xffffff:
+ f = "0x%06x"
+ case u <= 0xffffffff:
+ f = "0x%08x"
+ case u <= 0xffffffffff:
+ f = "0x%010x"
+ case u <= 0xffffffffffff:
+ f = "0x%012x"
+ case u <= 0xffffffffffffff:
+ f = "0x%014x"
+ case u <= 0xffffffffffffffff:
+ f = "0x%016x"
+ }
+ return fmt.Sprintf(f, u)
+}
+
+// formatPointer prints the address of the pointer.
+func formatPointer(v reflect.Value) string {
+ p := v.Pointer()
+ if flags.Deterministic {
+ p = 0xdeadf00f // Only used for stable testing purposes
+ }
+ return fmt.Sprintf("⟪0x%x⟫", p)
+}
+
+type visitedPointers map[value.Pointer]struct{}
+
+// Visit inserts pointer v into the visited map and reports whether it had
+// already been visited before.
+func (m visitedPointers) Visit(v reflect.Value) bool {
+ p := value.PointerOf(v)
+ _, visited := m[p]
+ m[p] = struct{}{}
+ return visited
+}
diff --git a/cmp/report_text.go b/cmp/report_text.go
new file mode 100644
index 0000000..80605d0
--- /dev/null
+++ b/cmp/report_text.go
@@ -0,0 +1,382 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmp
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "strings"
+ "time"
+
+ "github.com/google/go-cmp/cmp/internal/flags"
+)
+
+var randBool = rand.New(rand.NewSource(time.Now().Unix())).Intn(2) == 0
+
+type indentMode int
+
+func (n indentMode) appendIndent(b []byte, d diffMode) []byte {
+ if flags.Deterministic || randBool {
+ // Use regular spaces (U+0020).
+ switch d {
+ case diffUnknown, diffIdentical:
+ b = append(b, " "...)
+ case diffRemoved:
+ b = append(b, "- "...)
+ case diffInserted:
+ b = append(b, "+ "...)
+ }
+ } else {
+ // Use non-breaking spaces (U+00a0).
+ switch d {
+ case diffUnknown, diffIdentical:
+ b = append(b, "  "...)
+ case diffRemoved:
+ b = append(b, "- "...)
+ case diffInserted:
+ b = append(b, "+ "...)
+ }
+ }
+ return repeatCount(n).appendChar(b, '\t')
+}
+
+type repeatCount int
+
+func (n repeatCount) appendChar(b []byte, c byte) []byte {
+ for ; n > 0; n-- {
+ b = append(b, c)
+ }
+ return b
+}
+
+// textNode is a simplified tree-based representation of structured text.
+// Possible node types are textWrap, textList, or textLine.
+type textNode interface {
+ // Len reports the length in bytes of a single-line version of the tree.
+ // Nested textRecord.Diff and textRecord.Comment fields are ignored.
+ Len() int
+ // Equal reports whether the two trees are structurally identical.
+ // Nested textRecord.Diff and textRecord.Comment fields are compared.
+ Equal(textNode) bool
+ // String returns the string representation of the text tree.
+ // It is not guaranteed that len(x.String()) == x.Len(),
+ // nor that x.String() == y.String() implies that x.Equal(y).
+ String() string
+
+ // formatCompactTo formats the contents of the tree as a single-line string
+ // to the provided buffer. Any nested textRecord.Diff and textRecord.Comment
+ // fields are ignored.
+ //
+ // However, not all nodes in the tree should be collapsed as a single-line.
+ // If a node can be collapsed as a single-line, it is replaced by a textLine
+ // node. Since the top-level node cannot replace itself, this also returns
+ // the current node itself.
+ //
+ // This does not mutate the receiver.
+ formatCompactTo([]byte, diffMode) ([]byte, textNode)
+ // formatExpandedTo formats the contents of the tree as a multi-line string
+ // to the provided buffer. In order for column alignment to operate well,
+ // formatCompactTo must be called before calling formatExpandedTo.
+ formatExpandedTo([]byte, diffMode, indentMode) []byte
+}
+
+// textWrap is a wrapper that concatenates a prefix and/or a suffix
+// to the underlying node.
+type textWrap struct {
+ Prefix string // e.g., "bytes.Buffer{"
+ Value textNode // textWrap | textList | textLine
+ Suffix string // e.g., "}"
+}
+
+func (s textWrap) Len() int {
+ return len(s.Prefix) + s.Value.Len() + len(s.Suffix)
+}
+func (s1 textWrap) Equal(s2 textNode) bool {
+ if s2, ok := s2.(textWrap); ok {
+ return s1.Prefix == s2.Prefix && s1.Value.Equal(s2.Value) && s1.Suffix == s2.Suffix
+ }
+ return false
+}
+func (s textWrap) String() string {
+ var d diffMode
+ var n indentMode
+ _, s2 := s.formatCompactTo(nil, d)
+ b := n.appendIndent(nil, d) // Leading indent
+ b = s2.formatExpandedTo(b, d, n) // Main body
+ b = append(b, '\n') // Trailing newline
+ return string(b)
+}
+func (s textWrap) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
+ n0 := len(b) // Original buffer length
+ b = append(b, s.Prefix...)
+ b, s.Value = s.Value.formatCompactTo(b, d)
+ b = append(b, s.Suffix...)
+ if _, ok := s.Value.(textLine); ok {
+ return b, textLine(b[n0:])
+ }
+ return b, s
+}
+func (s textWrap) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
+ b = append(b, s.Prefix...)
+ b = s.Value.formatExpandedTo(b, d, n)
+ b = append(b, s.Suffix...)
+ return b
+}
+
+// textList is a comma-separated list of textWrap or textLine nodes.
+// The list may be formatted as multi-lines or single-line at the discretion
+// of the textList.formatCompactTo method.
+type textList []textRecord
+type textRecord struct {
+ Diff diffMode // e.g., 0 or '-' or '+'
+ Key string // e.g., "MyField"
+ Value textNode // textWrap | textLine
+ Comment fmt.Stringer // e.g., "6 identical fields"
+}
+
+// AppendEllipsis appends a new ellipsis node to the list if none already
+// exists at the end. If cs is non-zero it coalesces the statistics with the
+// previous diffStats.
+func (s *textList) AppendEllipsis(ds diffStats) {
+ hasStats := ds != diffStats{}
+ if len(*s) == 0 || !(*s)[len(*s)-1].Value.Equal(textEllipsis) {
+ if hasStats {
+ *s = append(*s, textRecord{Value: textEllipsis, Comment: ds})
+ } else {
+ *s = append(*s, textRecord{Value: textEllipsis})
+ }
+ return
+ }
+ if hasStats {
+ (*s)[len(*s)-1].Comment = (*s)[len(*s)-1].Comment.(diffStats).Append(ds)
+ }
+}
+
+func (s textList) Len() (n int) {
+ for i, r := range s {
+ n += len(r.Key)
+ if r.Key != "" {
+ n += len(": ")
+ }
+ n += r.Value.Len()
+ if i < len(s)-1 {
+ n += len(", ")
+ }
+ }
+ return n
+}
+
+func (s1 textList) Equal(s2 textNode) bool {
+ if s2, ok := s2.(textList); ok {
+ if len(s1) != len(s2) {
+ return false
+ }
+ for i := range s1 {
+ r1, r2 := s1[i], s2[i]
+ if !(r1.Diff == r2.Diff && r1.Key == r2.Key && r1.Value.Equal(r2.Value) && r1.Comment == r2.Comment) {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
+func (s textList) String() string {
+ return textWrap{"{", s, "}"}.String()
+}
+
+func (s textList) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
+ s = append(textList(nil), s...) // Avoid mutating original
+
+ // Determine whether we can collapse this list as a single line.
+ n0 := len(b) // Original buffer length
+ var multiLine bool
+ for i, r := range s {
+ if r.Diff == diffInserted || r.Diff == diffRemoved {
+ multiLine = true
+ }
+ b = append(b, r.Key...)
+ if r.Key != "" {
+ b = append(b, ": "...)
+ }
+ b, s[i].Value = r.Value.formatCompactTo(b, d|r.Diff)
+ if _, ok := s[i].Value.(textLine); !ok {
+ multiLine = true
+ }
+ if r.Comment != nil {
+ multiLine = true
+ }
+ if i < len(s)-1 {
+ b = append(b, ", "...)
+ }
+ }
+ // Force multi-lined output when printing a removed/inserted node that
+ // is sufficiently long.
+ if (d == diffInserted || d == diffRemoved) && len(b[n0:]) > 80 {
+ multiLine = true
+ }
+ if !multiLine {
+ return b, textLine(b[n0:])
+ }
+ return b, s
+}
+
+func (s textList) formatExpandedTo(b []byte, d diffMode, n indentMode) []byte {
+ alignKeyLens := s.alignLens(
+ func(r textRecord) bool {
+ _, isLine := r.Value.(textLine)
+ return r.Key == "" || !isLine
+ },
+ func(r textRecord) int { return len(r.Key) },
+ )
+ alignValueLens := s.alignLens(
+ func(r textRecord) bool {
+ _, isLine := r.Value.(textLine)
+ return !isLine || r.Value.Equal(textEllipsis) || r.Comment == nil
+ },
+ func(r textRecord) int { return len(r.Value.(textLine)) },
+ )
+
+ // Format the list as a multi-lined output.
+ n++
+ for i, r := range s {
+ b = n.appendIndent(append(b, '\n'), d|r.Diff)
+ if r.Key != "" {
+ b = append(b, r.Key+": "...)
+ }
+ b = alignKeyLens[i].appendChar(b, ' ')
+
+ b = r.Value.formatExpandedTo(b, d|r.Diff, n)
+ if !r.Value.Equal(textEllipsis) {
+ b = append(b, ',')
+ }
+ b = alignValueLens[i].appendChar(b, ' ')
+
+ if r.Comment != nil {
+ b = append(b, " // "+r.Comment.String()...)
+ }
+ }
+ n--
+
+ return n.appendIndent(append(b, '\n'), d)
+}
+
+func (s textList) alignLens(
+ skipFunc func(textRecord) bool,
+ lenFunc func(textRecord) int,
+) []repeatCount {
+ var startIdx, endIdx, maxLen int
+ lens := make([]repeatCount, len(s))
+ for i, r := range s {
+ if skipFunc(r) {
+ for j := startIdx; j < endIdx && j < len(s); j++ {
+ lens[j] = repeatCount(maxLen - lenFunc(s[j]))
+ }
+ startIdx, endIdx, maxLen = i+1, i+1, 0
+ } else {
+ if maxLen < lenFunc(r) {
+ maxLen = lenFunc(r)
+ }
+ endIdx = i + 1
+ }
+ }
+ for j := startIdx; j < endIdx && j < len(s); j++ {
+ lens[j] = repeatCount(maxLen - lenFunc(s[j]))
+ }
+ return lens
+}
+
+// textLine is a single-line segment of text and is always a leaf node
+// in the textNode tree.
+type textLine []byte
+
+var (
+ textNil = textLine("nil")
+ textEllipsis = textLine("...")
+)
+
+func (s textLine) Len() int {
+ return len(s)
+}
+func (s1 textLine) Equal(s2 textNode) bool {
+ if s2, ok := s2.(textLine); ok {
+ return bytes.Equal([]byte(s1), []byte(s2))
+ }
+ return false
+}
+func (s textLine) String() string {
+ return string(s)
+}
+func (s textLine) formatCompactTo(b []byte, d diffMode) ([]byte, textNode) {
+ return append(b, s...), s
+}
+func (s textLine) formatExpandedTo(b []byte, _ diffMode, _ indentMode) []byte {
+ return append(b, s...)
+}
+
+type diffStats struct {
+ Name string
+ NumIgnored int
+ NumIdentical int
+ NumRemoved int
+ NumInserted int
+ NumModified int
+}
+
+func (s diffStats) NumDiff() int {
+ return s.NumRemoved + s.NumInserted + s.NumModified
+}
+
+func (s diffStats) Append(ds diffStats) diffStats {
+ assert(s.Name == ds.Name)
+ s.NumIgnored += ds.NumIgnored
+ s.NumIdentical += ds.NumIdentical
+ s.NumRemoved += ds.NumRemoved
+ s.NumInserted += ds.NumInserted
+ s.NumModified += ds.NumModified
+ return s
+}
+
+// String prints a humanly-readable summary of coalesced records.
+//
+// Example:
+// diffStats{Name: "Field", NumIgnored: 5}.String() => "5 ignored fields"
+func (s diffStats) String() string {
+ var ss []string
+ var sum int
+ labels := [...]string{"ignored", "identical", "removed", "inserted", "modified"}
+ counts := [...]int{s.NumIgnored, s.NumIdentical, s.NumRemoved, s.NumInserted, s.NumModified}
+ for i, n := range counts {
+ if n > 0 {
+ ss = append(ss, fmt.Sprintf("%d %v", n, labels[i]))
+ }
+ sum += n
+ }
+
+ // Pluralize the name (adjusting for some obscure English grammar rules).
+ name := s.Name
+ if sum > 1 {
+ name = name + "s"
+ if strings.HasSuffix(name, "ys") {
+ name = name[:len(name)-2] + "ies" // e.g., "entrys" => "entries"
+ }
+ }
+
+ // Format the list according to English grammar (with Oxford comma).
+ switch n := len(ss); n {
+ case 0:
+ return ""
+ case 1, 2:
+ return strings.Join(ss, " and ") + " " + name
+ default:
+ return strings.Join(ss[:n-1], ", ") + ", and " + ss[n-1] + " " + name
+ }
+}
+
+type commentString string
+
+func (s commentString) String() string { return string(s) }
diff --git a/cmp/report_value.go b/cmp/report_value.go
new file mode 100644
index 0000000..fcff486
--- /dev/null
+++ b/cmp/report_value.go
@@ -0,0 +1,120 @@
+// Copyright 2019, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package cmp
+
+import "reflect"
+
+// valueNode represents a single node within a report, which is a
+// structured representation of the value tree, containing information
+// regarding which nodes are equal or not.
+type valueNode struct {
+ parent *valueNode
+
+ Type reflect.Type
+ ValueX reflect.Value
+ ValueY reflect.Value
+
+ // NumSame is the number of leaf nodes that are equal.
+ // All descendants are equal only if NumDiff is 0.
+ NumSame int
+ // NumDiff is the number of leaf nodes that are not equal.
+ NumDiff int
+ // NumIgnored is the number of leaf nodes that are ignored.
+ NumIgnored int
+ // NumCompared is the number of leaf nodes that were compared
+ // using an Equal method or Comparer function.
+ NumCompared int
+ // NumTransformed is the number of non-leaf nodes that were transformed.
+ NumTransformed int
+ // NumChildren is the number of transitive descendants of this node.
+ // This counts from zero; thus, leaf nodes have no descendants.
+ NumChildren int
+ // MaxDepth is the maximum depth of the tree. This counts from zero;
+ // thus, leaf nodes have a depth of zero.
+ MaxDepth int
+
+ // Records is a list of struct fields, slice elements, or map entries.
+ Records []reportRecord // If populated, implies Value is not populated
+
+ // Value is the result of a transformation, pointer indirect, of
+ // type assertion.
+ Value *valueNode // If populated, implies Records is not populated
+
+ // TransformerName is the name of the transformer.
+ TransformerName string // If non-empty, implies Value is populated
+}
+type reportRecord struct {
+ Key reflect.Value // Invalid for slice element
+ Value *valueNode
+}
+
+func (parent *valueNode) PushStep(ps PathStep) (child *valueNode) {
+ vx, vy := ps.Values()
+ child = &valueNode{parent: parent, Type: ps.Type(), ValueX: vx, ValueY: vy}
+ switch s := ps.(type) {
+ case StructField:
+ assert(parent.Value == nil)
+ parent.Records = append(parent.Records, reportRecord{Key: reflect.ValueOf(s.Name()), Value: child})
+ case SliceIndex:
+ assert(parent.Value == nil)
+ parent.Records = append(parent.Records, reportRecord{Value: child})
+ case MapIndex:
+ assert(parent.Value == nil)
+ parent.Records = append(parent.Records, reportRecord{Key: s.Key(), Value: child})
+ case Indirect:
+ assert(parent.Value == nil && parent.Records == nil)
+ parent.Value = child
+ case TypeAssertion:
+ assert(parent.Value == nil && parent.Records == nil)
+ parent.Value = child
+ case Transform:
+ assert(parent.Value == nil && parent.Records == nil)
+ parent.Value = child
+ parent.TransformerName = s.Name()
+ parent.NumTransformed++
+ default:
+ assert(parent == nil) // Must be the root step
+ }
+ return child
+}
+
+func (r *valueNode) Report(f reportFlags) {
+ assert(r.MaxDepth == 0) // May only be called on leaf nodes
+
+ if f&reportEqual > 0 {
+ r.NumSame++
+ }
+ if f&reportUnequal > 0 {
+ r.NumDiff++
+ }
+ if f&reportIgnored > 0 {
+ r.NumIgnored++
+ }
+ assert(r.NumSame+r.NumDiff+r.NumIgnored == 1)
+
+ if f&reportByMethod > 0 {
+ r.NumCompared++
+ }
+ if f&reportByFunc > 0 {
+ r.NumCompared++
+ }
+ assert(r.NumCompared <= 1)
+}
+
+func (child *valueNode) PopStep() (parent *valueNode) {
+ parent = child.parent
+ if parent != nil {
+ parent.NumSame += child.NumSame
+ parent.NumDiff += child.NumDiff
+ parent.NumIgnored += child.NumIgnored
+ parent.NumCompared += child.NumCompared
+ parent.NumTransformed += child.NumTransformed
+ parent.NumChildren += child.NumChildren + 1
+ if parent.MaxDepth < child.MaxDepth+1 {
+ parent.MaxDepth = child.MaxDepth + 1
+ }
+ }
+ return parent
+}