diff options
author | Joe Tsai <joetsai@digital-static.net> | 2017-07-20 15:59:54 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-20 15:59:54 -0700 |
commit | 18107e6c56edb2d51f965f7d68e59404f0daee54 (patch) | |
tree | 40c1cf2b0a678b8cc4ab021370f080fc5315ceb2 /cmp | |
parent | f9054c6a605e1bb9318d701929105b0fd8a7cac8 (diff) | |
download | go-cmp-18107e6c56edb2d51f965f7d68e59404f0daee54.tar.gz |
Move general reflect logic to internal/value (#15)
This internal package has the API:
Format(reflect.Value, useStringer bool) string
SortKeys(vs []reflect.Value) []reflect.Value
While moving Format and SortKeys, we also tweak the formatting logic
to print the type on named primitive types.
Diffstat (limited to 'cmp')
-rw-r--r-- | cmp/compare.go | 3 | ||||
-rw-r--r-- | cmp/compare_test.go | 14 | ||||
-rw-r--r-- | cmp/internal/value/format.go | 258 | ||||
-rw-r--r-- | cmp/internal/value/format_test.go | 91 | ||||
-rw-r--r-- | cmp/internal/value/sort.go | 111 | ||||
-rw-r--r-- | cmp/internal/value/sort_test.go (renamed from cmp/reporter_test.go) | 109 | ||||
-rw-r--r-- | cmp/reporter.go | 339 |
7 files changed, 492 insertions, 433 deletions
diff --git a/cmp/compare.go b/cmp/compare.go index 2294579..4fff3fd 100644 --- a/cmp/compare.go +++ b/cmp/compare.go @@ -31,6 +31,7 @@ import ( "reflect" "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/value" ) // BUG: Maps with keys containing NaN values cannot be properly compared due to @@ -442,7 +443,7 @@ func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { step := &mapIndex{pathStep: pathStep{t.Elem()}} s.curPath.push(step) defer s.curPath.pop() - for _, k := range sortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { step.key = k vvx := vx.MapIndex(k) vvy := vy.MapIndex(k) diff --git a/cmp/compare_test.go b/cmp/compare_test.go index 11c30c2..d7d80d5 100644 --- a/cmp/compare_test.go +++ b/cmp/compare_test.go @@ -1438,8 +1438,8 @@ func project1Tests() []test { -: "southbay2" +: "southbay" *{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.State: - -: 6 - +: 5 + -: testprotos.Goat_States(6) + +: testprotos.Goat_States(5) {teststructs.Eagle}.Slaps[0].Immutable.MildSlap: -: false +: true @@ -1652,8 +1652,8 @@ func project3Tests() []test { -: &teststructs.MockTable{state: []string{"a", "c"}} +: &teststructs.MockTable{state: []string{"a", "b", "c"}} {teststructs.Dirt}.Discord: - -: 554 - +: 500 + -: teststructs.DiscordState(554) + +: teststructs.DiscordState(500) λ({teststructs.Dirt}.Proto): -: "blah" +: "proto" @@ -1751,10 +1751,10 @@ func project4Tests() []test { -: 0x04 +: 0x03 {teststructs.Cartel}.poisons[0].poisonType: - -: 1 - +: 5 + -: testprotos.PoisonType(1) + +: testprotos.PoisonType(5) {teststructs.Cartel}.poisons[1->?]: - -: &teststructs.Poison{poisonType: 2, manufactuer: "acme2"} + -: &teststructs.Poison{poisonType: testprotos.PoisonType(2), manufactuer: "acme2"} +: <non-existent>`, }} } diff --git a/cmp/internal/value/format.go b/cmp/internal/value/format.go new file mode 100644 index 0000000..e26395d --- /dev/null +++ b/cmp/internal/value/format.go @@ -0,0 +1,258 @@ +// 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 ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +// formatFakePointers controls whether to substitute pointer addresses with nil. +// This is used for deterministic testing. +var formatFakePointers = false + +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, useStringer bool) string { + return formatAny(v, formatConfig{useStringer, true, true, !formatFakePointers}, nil) +} + +type formatConfig struct { + useStringer bool // Should the String method be used if available? + printType bool // Should we print the type before the value? + followPointers bool // Should we recursively follow pointers? + realPointers bool // Should we print the real address of pointers? +} + +func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) 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) { + if v.Kind() == reflect.Ptr && v.IsNil() { + return "<nil>" + } + return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String()) + } + + 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(), fmt.Sprintf("%q", v), 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 visited[v.Pointer()] || !conf.followPointers { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + return "&" + formatAny(v.Elem(), conf, visited) + case reflect.Interface: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "<nil>" + } + return formatAny(v.Elem(), conf, visited) + case reflect.Slice: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "<nil>" + } + if visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + fallthrough + case reflect.Array: + var ss []string + subConf := conf + subConf.printType = v.Type().Elem().Kind() == reflect.Interface + for i := 0; i < v.Len(); i++ { + s := formatAny(v.Index(i), subConf, visited) + ss = append(ss, s) + } + 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 visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + + var ss []string + subConf := conf + subConf.printType = v.Type().Elem().Kind() == reflect.Interface + for _, k := range SortKeys(v.MapKeys()) { + sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited) + sv := formatAny(v.MapIndex(k), subConf, visited) + 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 && isExported(name) + s := formatAny(vv, subConf, visited) + 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 formatPrimitive(t reflect.Type, v interface{}, conf formatConfig) string { + if conf.printType && 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) +} + +// insertPointer insert p into m, allocating m if necessary. +func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool { + if m == nil { + m = make(map[uintptr]bool) + } + m[p] = true + return m +} + +// 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 +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/cmp/internal/value/format_test.go b/cmp/internal/value/format_test.go new file mode 100644 index 0000000..c44884f --- /dev/null +++ b/cmp/internal/value/format_test.go @@ -0,0 +1,91 @@ +// 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{0x00: \"hello\"}", + }, { + in: func() interface{} { + var a = []interface{}{nil} + a[0] = a + return a + }(), + want: "[]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)}", + }} + + formatFakePointers = true + defer func() { formatFakePointers = false }() + for i, tt := range tests { + got := Format(reflect.ValueOf(tt.in), true) + if got != tt.want { + t.Errorf("test %d, pretty print:\ngot %q\nwant %q", i, got, tt.want) + } + } +} diff --git a/cmp/internal/value/sort.go b/cmp/internal/value/sort.go new file mode 100644 index 0000000..da00896 --- /dev/null +++ b/cmp/internal/value/sort.go @@ -0,0 +1,111 @@ +// 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 ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKey sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.Sort(valueSorter(vs)) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if v.Interface() != vs2[len(vs2)-1].Interface() { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. +type valueSorter []reflect.Value + +func (vs valueSorter) Len() int { return len(vs) } +func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) } +func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return x.Uint() < y.Uint() + case reflect.Float32, reflect.Float64: + fx, fy := x.Float(), y.Float() + return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) + case reflect.Complex64, reflect.Complex128: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/cmp/reporter_test.go b/cmp/internal/value/sort_test.go index 7310077..d3f254b 100644 --- a/cmp/reporter_test.go +++ b/cmp/internal/value/sort_test.go @@ -2,92 +2,16 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE.md file. -package cmp +package value_test import ( - "bytes" - "io" "math" "reflect" "testing" -) - -func TestFormatAny(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[cmp.key]string{{}: \"hello\"}", - }, { - in: map[key]string{{a: 5, b: "key", c: make(chan bool)}: "hello"}, - want: "map[cmp.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{0x00: \"hello\"}", - }, { - in: func() interface{} { - var a = []interface{}{nil} - a[0] = a - return a - }(), - want: "[]interface {}{([]interface {})(0x00)}", - }, { - in: func() interface{} { - type A *A - var a A - a = &a - return a - }(), - want: "&(cmp.A)(0x00)", - }, { - in: func() interface{} { - type A map[*A]A - a := make(A) - a[&a] = a - return a - }(), - want: "cmp.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 { - got := formatAny(reflect.ValueOf(tt.in), formatConfig{true, true, true, false}, nil) - if got != tt.want { - t.Errorf("test %d, pretty print:\ngot %q\nwant %q", i, got, tt.want) - } - } -} + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/value" +) func TestSortKeys(t *testing.T) { type ( @@ -101,14 +25,14 @@ func TestSortKeys(t *testing.T) { EmptyStruct struct{} ) - opts := []Option{ - Comparer(func(x, y float64) bool { + opts := []cmp.Option{ + cmp.Comparer(func(x, y float64) bool { if math.IsNaN(x) && math.IsNaN(y) { return true } return x == y }), - Comparer(func(x, y complex128) bool { + cmp.Comparer(func(x, y complex128) bool { rx, ix, ry, iy := real(x), imag(x), real(y), imag(y) if math.IsNaN(rx) && math.IsNaN(ry) { rx, ry = 0, 0 @@ -118,11 +42,11 @@ func TestSortKeys(t *testing.T) { } return rx == ry && ix == iy }), - Comparer(func(x, y chan bool) bool { return true }), - Comparer(func(x, y chan int) bool { return true }), - Comparer(func(x, y chan float64) bool { return true }), - Comparer(func(x, y chan interface{}) bool { return true }), - Comparer(func(x, y *int) bool { return true }), + cmp.Comparer(func(x, y chan bool) bool { return true }), + cmp.Comparer(func(x, y chan int) bool { return true }), + cmp.Comparer(func(x, y chan float64) bool { return true }), + cmp.Comparer(func(x, y chan interface{}) bool { return true }), + cmp.Comparer(func(x, y *int) bool { return true }), } tests := []struct { @@ -188,7 +112,8 @@ func TestSortKeys(t *testing.T) { [2]int{2, 3}, [2]int{2, 4}, [2]int{4, 0}, MyArray([2]int{2, 4}), make(chan bool), make(chan bool), make(chan int), make(chan interface{}), new(int), new(int), - MyString("abc"), MyString("abcd"), MyString("abcde"), "abc", "abcd", "abcde", "bar", "foo", + "abc", "abcd", "abcde", "bar", "foo", + MyString("abc"), MyString("abcd"), MyString("abcde"), EmptyStruct{}, MyStruct{"alpha", [2]int{3, 3}, make(chan float64)}, MyStruct{"bravo", [2]int{2, 3}, make(chan float64)}, @@ -217,11 +142,11 @@ func TestSortKeys(t *testing.T) { for i, tt := range tests { keys := append(reflect.ValueOf(tt.in).MapKeys(), reflect.ValueOf(tt.in).MapKeys()...) var got []interface{} - for _, k := range sortKeys(keys) { + for _, k := range value.SortKeys(keys) { got = append(got, k.Interface()) } - if !Equal(got, tt.want, opts...) { - t.Errorf("test %d, output mismatch:\ngot %#v\nwant %#v", i, got, tt.want) + if d := cmp.Diff(tt.want, got, opts...); d != "" { + t.Errorf("test %d, output mismatch (-want +got):\n%s", i, d) } } } diff --git a/cmp/reporter.go b/cmp/reporter.go index 3f3454b..faebc05 100644 --- a/cmp/reporter.go +++ b/cmp/reporter.go @@ -6,14 +6,11 @@ package cmp import ( "fmt" - "math" "reflect" - "sort" "strings" -) -// TODO: Can we leave the interface for a reporter here in the cmp package -// and somehow extract the implementation of defaultReporter into cmp/report? + "github.com/google/go-cmp/cmp/internal/value" +) type defaultReporter struct { Option @@ -34,12 +31,12 @@ func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { const maxLines = 256 r.ndiffs++ if r.nbytes < maxBytes && r.nlines < maxLines { - sx := prettyPrint(x, true) - sy := prettyPrint(y, true) + sx := value.Format(x, true) + sy := value.Format(y, true) if sx == sy { // Stringer is not helpful, so rely on more exact formatting. - sx = prettyPrint(x, false) - sy = prettyPrint(y, false) + sx = value.Format(x, false) + sy = value.Format(y, false) } s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) r.diffs = append(r.diffs, s) @@ -55,327 +52,3 @@ func (r *defaultReporter) String() string { } return fmt.Sprintf("%s... %d more differences ...", s, len(r.diffs)-r.ndiffs) } - -var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() - -func prettyPrint(v reflect.Value, useStringer bool) string { - return formatAny(v, formatConfig{useStringer, true, true, true}, nil) -} - -type formatConfig struct { - useStringer bool // Should the String method be used if available? - printType bool // Should we print the type before the value? - followPointers bool // Should we recursively follow pointers? - realPointers bool // Should we print the real address of pointers? -} - -// formatAny prints the value v in a pretty formatted manner. -// 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 formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) 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) { - if v.Kind() == reflect.Ptr && v.IsNil() { - return "<nil>" - } - return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String()) - } - - switch v.Kind() { - case reflect.Bool: - return fmt.Sprint(v.Bool()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return fmt.Sprint(v.Int()) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { - return formatHex(v.Uint()) // Unnamed uints are usually bytes or words - } - return fmt.Sprint(v.Uint()) // Named uints are usually enumerations - case reflect.Float32, reflect.Float64: - return fmt.Sprint(v.Float()) - case reflect.Complex64, reflect.Complex128: - return fmt.Sprint(v.Complex()) - case reflect.String: - return fmt.Sprintf("%q", v) - 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 visited[v.Pointer()] || !conf.followPointers { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - return "&" + formatAny(v.Elem(), conf, visited) - case reflect.Interface: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "<nil>" - } - return formatAny(v.Elem(), conf, visited) - case reflect.Slice: - if v.IsNil() { - if conf.printType { - return fmt.Sprintf("%v(nil)", v.Type()) - } - return "<nil>" - } - if visited[v.Pointer()] { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - fallthrough - case reflect.Array: - var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface - for i := 0; i < v.Len(); i++ { - s := formatAny(v.Index(i), subConf, visited) - ss = append(ss, s) - } - 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 visited[v.Pointer()] { - return formatPointer(v, conf) - } - visited = insertPointer(visited, v.Pointer()) - - var ss []string - subConf := conf - subConf.printType = v.Type().Elem().Kind() == reflect.Interface - for _, k := range sortKeys(v.MapKeys()) { - sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited) - sv := formatAny(v.MapIndex(k), subConf, visited) - 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 && isExported(name) - s := formatAny(vv, subConf, visited) - 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 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) -} - -// insertPointer insert p into m, allocating m if necessary. -func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool { - if m == nil { - m = make(map[uintptr]bool) - } - m[p] = true - return m -} - -// 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 -} - -// isLess is a generic function for sorting arbitrary map keys. -// The inputs must be of the same type and must be comparable. -func isLess(x, y reflect.Value) bool { - switch x.Type().Kind() { - case reflect.Bool: - return !x.Bool() && y.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return x.Int() < y.Int() - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return x.Uint() < y.Uint() - case reflect.Float32, reflect.Float64: - fx, fy := x.Float(), y.Float() - return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) - case reflect.Complex64, reflect.Complex128: - cx, cy := x.Complex(), y.Complex() - rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) - if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { - return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) - } - return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) - case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: - return x.Pointer() < y.Pointer() - case reflect.String: - return x.String() < y.String() - case reflect.Array: - for i := 0; i < x.Len(); i++ { - if isLess(x.Index(i), y.Index(i)) { - return true - } - if isLess(y.Index(i), x.Index(i)) { - return false - } - } - return false - case reflect.Struct: - for i := 0; i < x.NumField(); i++ { - if isLess(x.Field(i), y.Field(i)) { - return true - } - if isLess(y.Field(i), x.Field(i)) { - return false - } - } - return false - case reflect.Interface: - vx, vy := x.Elem(), y.Elem() - if !vx.IsValid() || !vy.IsValid() { - return !vx.IsValid() && vy.IsValid() - } - tx, ty := vx.Type(), vy.Type() - if tx == ty { - return isLess(x.Elem(), y.Elem()) - } - if tx.Kind() != ty.Kind() { - return vx.Kind() < vy.Kind() - } - if tx.String() != ty.String() { - return tx.String() < ty.String() - } - if tx.PkgPath() != ty.PkgPath() { - return tx.PkgPath() < ty.PkgPath() - } - // This can happen in rare situations, so we fallback to just comparing - // the unique pointer for a reflect.Type. This guarantees deterministic - // ordering within a program, but it is obviously not stable. - return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() - default: - // Must be Func, Map, or Slice; which are not comparable. - panic(fmt.Sprintf("%T is not comparable", x.Type())) - } -} - -// sortKey sorts a list of map keys, deduplicating keys if necessary. -func sortKeys(vs []reflect.Value) []reflect.Value { - if len(vs) == 0 { - return vs - } - - // Sort the map keys. - sort.Sort(valueSorter(vs)) - - // Deduplicate keys (fails for NaNs). - vs2 := vs[:1] - for _, v := range vs[1:] { - if v.Interface() != vs2[len(vs2)-1].Interface() { - vs2 = append(vs2, v) - } - } - return vs2 -} - -// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. -type valueSorter []reflect.Value - -func (vs valueSorter) Len() int { return len(vs) } -func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) } -func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } |