diff options
Diffstat (limited to 'cmp')
-rw-r--r-- | cmp/compare.go | 21 | ||||
-rw-r--r-- | cmp/compare_test.go | 1201 | ||||
-rw-r--r-- | cmp/example_test.go | 201 | ||||
-rw-r--r-- | cmp/internal/flags/flags.go | 9 | ||||
-rw-r--r-- | cmp/internal/flags/toolchain_legacy.go | 10 | ||||
-rw-r--r-- | cmp/internal/flags/toolchain_recent.go | 10 | ||||
-rw-r--r-- | cmp/internal/value/format.go | 280 | ||||
-rw-r--r-- | cmp/internal/value/format_test.go | 93 | ||||
-rw-r--r-- | cmp/internal/value/zero.go | 45 | ||||
-rw-r--r-- | cmp/internal/value/zero_test.go | 45 | ||||
-rw-r--r-- | cmp/path.go | 9 | ||||
-rw-r--r-- | cmp/report.go | 71 | ||||
-rw-r--r-- | cmp/report_compare.go | 290 | ||||
-rw-r--r-- | cmp/report_reflect.go | 279 | ||||
-rw-r--r-- | cmp/report_text.go | 382 | ||||
-rw-r--r-- | cmp/report_value.go | 120 |
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 +} |