summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--spew/common.go75
-rw-r--r--spew/common_test.go139
-rw-r--r--spew/config.go12
-rw-r--r--spew/doc.go12
-rw-r--r--spew/dump.go2
-rw-r--r--spew/dump_test.go37
-rw-r--r--spew/format.go2
-rw-r--r--spew/format_test.go49
-rw-r--r--spew/internal_test.go4
10 files changed, 304 insertions, 41 deletions
diff --git a/README.md b/README.md
index c84c4ad..7a5960f 100644
--- a/README.md
+++ b/README.md
@@ -132,9 +132,16 @@ options. See the ConfigState documentation for more details.
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
- are supported with other types sorted according to the
- reflect.Value.String() output which guarantees display stability.
- Natural map order is used by default.
+ and types which implement error or Stringer interfaces are supported,
+ with other types sorted according to the reflect.Value.String() output
+ which guarantees display stability. Natural map order is used by
+ default.
+
+* SpewKeys
+ SpewKeys specifies that, as a last resort attempt, map keys should be
+ spewed to strings and sorted by those strings. This is only considered
+ if SortKeys is true.
+
```
## License
diff --git a/spew/common.go b/spew/common.go
index 81fac0b..8252cd3 100644
--- a/spew/common.go
+++ b/spew/common.go
@@ -17,6 +17,7 @@
package spew
import (
+ "bytes"
"fmt"
"io"
"reflect"
@@ -325,7 +326,61 @@ func printHexPtr(w io.Writer, p uintptr) {
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
- values []reflect.Value
+ values []reflect.Value
+ strings []string // either nil or same len and values
+ cs *ConfigState
+}
+
+// newValuesSorter initializes a valuesSorter instance, which holds a set of
+// surrogate keys on which the data should be sorted. It uses flags in
+// ConfigState to decide if and how to populate those surrogate keys.
+func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
+ vs := &valuesSorter{values: values, cs: cs}
+ if canSortSimply(vs.values[0].Kind()) {
+ return vs
+ }
+ if !cs.DisableMethods {
+ vs.strings = make([]string, len(values))
+ for i := range vs.values {
+ b := bytes.Buffer{}
+ if !handleMethods(cs, &b, vs.values[i]) {
+ vs.strings = nil
+ break
+ }
+ vs.strings[i] = b.String()
+ }
+ }
+ if vs.strings == nil && cs.SpewKeys {
+ vs.strings = make([]string, len(values))
+ for i := range vs.values {
+ vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
+ }
+ }
+ return vs
+}
+
+// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
+// directly, or whether it should be considered for sorting by surrogate keys
+// (if the ConfigState allows it).
+func canSortSimply(kind reflect.Kind) bool {
+ // This switch parallels valueSortLess, except for the default case.
+ switch kind {
+ case reflect.Bool:
+ return true
+ case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
+ return true
+ case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
+ return true
+ case reflect.Float32, reflect.Float64:
+ return true
+ case reflect.String:
+ return true
+ case reflect.Uintptr:
+ return true
+ case reflect.Array:
+ return true
+ }
+ return false
}
// Len returns the number of values in the slice. It is part of the
@@ -338,6 +393,9 @@ func (s *valuesSorter) Len() int {
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
+ if s.strings != nil {
+ s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
+ }
}
// valueSortLess returns whether the first value should sort before the second
@@ -375,15 +433,18 @@ func valueSortLess(a, b reflect.Value) bool {
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
- return valueSortLess(s.values[i], s.values[j])
+ if s.strings == nil {
+ return valueSortLess(s.values[i], s.values[j])
+ }
+ return s.strings[i] < s.strings[j]
}
-// sortValues is a generic sort function for native types: int, uint, bool,
-// string and uintptr. Other inputs are sorted according to their
-// Value.String() value to ensure display stability.
-func sortValues(values []reflect.Value) {
+// sortValues is a sort function that handles both native types and any type that
+// can be converted to error or Stringer. Other inputs are sorted according to
+// their Value.String() value to ensure display stability.
+func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
- sort.Sort(&valuesSorter{values})
+ sort.Sort(newValuesSorter(values, cs))
}
diff --git a/spew/common_test.go b/spew/common_test.go
index 8e741cd..39b7525 100644
--- a/spew/common_test.go
+++ b/spew/common_test.go
@@ -18,9 +18,10 @@ package spew_test
import (
"fmt"
- "github.com/davecgh/go-spew/spew"
"reflect"
"testing"
+
+ "github.com/davecgh/go-spew/spew"
)
// custom type to test Stinger interface on non-pointer receiver.
@@ -113,9 +114,24 @@ func testFailed(result string, wants []string) bool {
return true
}
-// TestSortValues ensures the sort functionality for relect.Value based sorting
-// works as intended.
-func TestSortValues(t *testing.T) {
+type sortableStruct struct {
+ x int
+}
+
+func (ss sortableStruct) String() string {
+ return fmt.Sprintf("ss.%d", ss.x)
+}
+
+type unsortableStruct struct {
+ x int
+}
+
+type sortTestCase struct {
+ input []reflect.Value
+ expected []reflect.Value
+}
+
+func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) {
getInterfaces := func(values []reflect.Value) []interface{} {
interfaces := []interface{}{}
for _, v := range values {
@@ -124,6 +140,23 @@ func TestSortValues(t *testing.T) {
return interfaces
}
+ for _, test := range tests {
+ spew.SortValues(test.input, cs)
+ // reflect.DeepEqual cannot really make sense of reflect.Value,
+ // probably because of all the pointer tricks. For instance,
+ // v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
+ // instead.
+ input := getInterfaces(test.input)
+ expected := getInterfaces(test.expected)
+ if !reflect.DeepEqual(input, expected) {
+ t.Errorf("Sort mismatch:\n %v != %v", input, expected)
+ }
+ }
+}
+
+// TestSortValues ensures the sort functionality for relect.Value based sorting
+// works as intended.
+func TestSortValues(t *testing.T) {
v := reflect.ValueOf
a := v("a")
@@ -132,10 +165,7 @@ func TestSortValues(t *testing.T) {
embedA := v(embed{"a"})
embedB := v(embed{"b"})
embedC := v(embed{"c"})
- tests := []struct {
- input []reflect.Value
- expected []reflect.Value
- }{
+ tests := []sortTestCase{
// No values.
{
[]reflect.Value{},
@@ -176,22 +206,93 @@ func TestSortValues(t *testing.T) {
[]reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))},
[]reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))},
},
+ // SortableStructs.
+ {
+ // Note: not sorted - DisableMethods is set.
+ []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
+ []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
+ },
+ // UnsortableStructs.
+ {
+ // Note: not sorted - SpewKeys is false.
+ []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
+ []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
+ },
// Invalid.
{
[]reflect.Value{embedB, embedA, embedC},
[]reflect.Value{embedB, embedA, embedC},
},
}
- for _, test := range tests {
- spew.SortValues(test.input)
- // reflect.DeepEqual cannot really make sense of reflect.Value,
- // probably because of all the pointer tricks. For instance,
- // v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
- // instead.
- input := getInterfaces(test.input)
- expected := getInterfaces(test.expected)
- if !reflect.DeepEqual(input, expected) {
- t.Errorf("Sort mismatch:\n %v != %v", input, expected)
- }
+ cs := spew.ConfigState{DisableMethods: true, SpewKeys: false}
+ helpTestSortValues(tests, &cs, t)
+}
+
+// TestSortValuesWithMethods ensures the sort functionality for relect.Value
+// based sorting works as intended when using string methods.
+func TestSortValuesWithMethods(t *testing.T) {
+ v := reflect.ValueOf
+
+ a := v("a")
+ b := v("b")
+ c := v("c")
+ tests := []sortTestCase{
+ // Ints.
+ {
+ []reflect.Value{v(2), v(1), v(3)},
+ []reflect.Value{v(1), v(2), v(3)},
+ },
+ // Strings.
+ {
+ []reflect.Value{b, a, c},
+ []reflect.Value{a, b, c},
+ },
+ // SortableStructs.
+ {
+ []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
+ []reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
+ },
+ // UnsortableStructs.
+ {
+ // Note: not sorted - SpewKeys is false.
+ []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
+ []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
+ },
+ }
+ cs := spew.ConfigState{DisableMethods: false, SpewKeys: false}
+ helpTestSortValues(tests, &cs, t)
+}
+
+// TestSortValuesWithSpew ensures the sort functionality for relect.Value
+// based sorting works as intended when using spew to stringify keys.
+func TestSortValuesWithSpew(t *testing.T) {
+ v := reflect.ValueOf
+
+ a := v("a")
+ b := v("b")
+ c := v("c")
+ tests := []sortTestCase{
+ // Ints.
+ {
+ []reflect.Value{v(2), v(1), v(3)},
+ []reflect.Value{v(1), v(2), v(3)},
+ },
+ // Strings.
+ {
+ []reflect.Value{b, a, c},
+ []reflect.Value{a, b, c},
+ },
+ // SortableStructs.
+ {
+ []reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
+ []reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
+ },
+ // UnsortableStructs.
+ {
+ []reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
+ []reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})},
+ },
}
+ cs := spew.ConfigState{DisableMethods: true, SpewKeys: true}
+ helpTestSortValues(tests, &cs, t)
}
diff --git a/spew/config.go b/spew/config.go
index e516675..9e21b38 100644
--- a/spew/config.go
+++ b/spew/config.go
@@ -76,10 +76,16 @@ type ConfigState struct {
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
- // native types (bool, int, uint, floats, uintptr and string) are supported
- // with other types sorted according to the reflect.Value.String() output
- // which guarantees display stability.
+ // native types (bool, int, uint, floats, uintptr and string) and types
+ // that support the error or Stringer interfaces (if methods are
+ // enabled) are supported, with other types sorted according to the
+ // reflect.Value.String() output which guarantees display stability.
SortKeys bool
+
+ // SpewKeys specifies that, as a last resort attempt, map keys should
+ // be spewed to strings and sorted by those strings. This is only
+ // considered if SortKeys is true.
+ SpewKeys bool
}
// Config is the active configuration of the top-level functions.
diff --git a/spew/doc.go b/spew/doc.go
index a0d73ac..5be0c40 100644
--- a/spew/doc.go
+++ b/spew/doc.go
@@ -99,9 +99,15 @@ The following configuration options are available:
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
- are supported with other types sorted according to the
- reflect.Value.String() output which guarantees display stability.
- Natural map order is used by default.
+ and types which implement error or Stringer interfaces are
+ supported with other types sorted according to the
+ reflect.Value.String() output which guarantees display
+ stability. Natural map order is used by default.
+
+ * SpewKeys
+ Specifies that, as a last resort attempt, map keys should be
+ spewed to strings and sorted by those strings. This is only
+ considered if SortKeys is true.
Dump Usage
diff --git a/spew/dump.go b/spew/dump.go
index 983d23f..5783145 100644
--- a/spew/dump.go
+++ b/spew/dump.go
@@ -382,7 +382,7 @@ func (d *dumpState) dump(v reflect.Value) {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
- sortValues(keys)
+ sortValues(keys, d.cs)
}
for i, key := range keys {
d.dump(d.unpackValue(key))
diff --git a/spew/dump_test.go b/spew/dump_test.go
index 9e0e65f..3dd9089 100644
--- a/spew/dump_test.go
+++ b/spew/dump_test.go
@@ -64,9 +64,10 @@ package spew_test
import (
"bytes"
"fmt"
- "github.com/davecgh/go-spew/spew"
"testing"
"unsafe"
+
+ "github.com/davecgh/go-spew/spew"
)
// dumpTest is used to describe a test to be perfomed against the Dump method.
@@ -983,4 +984,38 @@ func TestDumpSortedKeys(t *testing.T) {
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
+
+ s = cfg.Sdump(map[stringer]int{"1": 1, "3": 3, "2": 2})
+ expected = `(map[spew_test.stringer]int) (len=3) {
+(spew_test.stringer) (len=1) stringer 1: (int) 1,
+(spew_test.stringer) (len=1) stringer 2: (int) 2,
+(spew_test.stringer) (len=1) stringer 3: (int) 3
+}
+`
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sdump(map[pstringer]int{pstringer("1"): 1, pstringer("3"): 3, pstringer("2"): 2})
+ expected = `(map[spew_test.pstringer]int) (len=3) {
+(spew_test.pstringer) (len=1) stringer 1: (int) 1,
+(spew_test.pstringer) (len=1) stringer 2: (int) 2,
+(spew_test.pstringer) (len=1) stringer 3: (int) 3
+}
+`
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sdump(map[customError]int{customError(1): 1, customError(3): 3, customError(2): 2})
+ expected = `(map[spew_test.customError]int) (len=3) {
+(spew_test.customError) error: 1: (int) 1,
+(spew_test.customError) error: 2: (int) 2,
+(spew_test.customError) error: 3: (int) 3
+}
+`
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
}
diff --git a/spew/format.go b/spew/format.go
index cc152ae..ecf3b80 100644
--- a/spew/format.go
+++ b/spew/format.go
@@ -309,7 +309,7 @@ func (f *formatState) format(v reflect.Value) {
} else {
keys := v.MapKeys()
if f.cs.SortKeys {
- sortValues(keys)
+ sortValues(keys, f.cs)
}
for i, key := range keys {
if i > 0 {
diff --git a/spew/format_test.go b/spew/format_test.go
index 4dd0ac2..b0f9761 100644
--- a/spew/format_test.go
+++ b/spew/format_test.go
@@ -69,9 +69,10 @@ package spew_test
import (
"bytes"
"fmt"
- "github.com/davecgh/go-spew/spew"
"testing"
"unsafe"
+
+ "github.com/davecgh/go-spew/spew"
)
// formatterTest is used to describe a test to be perfomed against NewFormatter.
@@ -1478,6 +1479,22 @@ func TestFormatter(t *testing.T) {
}
}
+type testStruct struct {
+ x int
+}
+
+func (ts testStruct) String() string {
+ return fmt.Sprintf("ts.%d", ts.x)
+}
+
+type testStructP struct {
+ x int
+}
+
+func (ts *testStructP) String() string {
+ return fmt.Sprintf("ts.%d", ts.x)
+}
+
func TestPrintSortedKeys(t *testing.T) {
cfg := spew.ConfigState{SortKeys: true}
s := cfg.Sprint(map[int]string{1: "1", 3: "3", 2: "2"})
@@ -1485,4 +1502,34 @@ func TestPrintSortedKeys(t *testing.T) {
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
+
+ s = cfg.Sprint(map[stringer]int{"1": 1, "3": 3, "2": 2})
+ expected = "map[stringer 1:1 stringer 2:2 stringer 3:3]"
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sprint(map[pstringer]int{pstringer("1"): 1, pstringer("3"): 3, pstringer("2"): 2})
+ expected = "map[stringer 1:1 stringer 2:2 stringer 3:3]"
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sprint(map[testStruct]int{testStruct{1}: 1, testStruct{3}: 3, testStruct{2}: 2})
+ expected = "map[ts.1:1 ts.2:2 ts.3:3]"
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sprint(map[testStructP]int{testStructP{1}: 1, testStructP{3}: 3, testStructP{2}: 2})
+ expected = "map[ts.1:1 ts.2:2 ts.3:3]"
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
+
+ s = cfg.Sprint(map[customError]int{customError(1): 1, customError(3): 3, customError(2): 2})
+ expected = "map[error: 1:1 error: 2:2 error: 3:3]"
+ if s != expected {
+ t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
+ }
}
diff --git a/spew/internal_test.go b/spew/internal_test.go
index 10dc0b1..b583bfd 100644
--- a/spew/internal_test.go
+++ b/spew/internal_test.go
@@ -151,6 +151,6 @@ func TestAddedReflectValue(t *testing.T) {
// SortValues makes the internal sortValues function available to the test
// package.
-func SortValues(values []reflect.Value) {
- sortValues(values)
+func SortValues(values []reflect.Value, cs *ConfigState) {
+ sortValues(values, cs)
}