diff --git a/spew/config.go b/spew/config.go index a2eab61..d766dfb 100644 --- a/spew/config.go +++ b/spew/config.go @@ -73,6 +73,13 @@ type ConfigState struct { // NOTE: This flag does not have any effect if method invocation is disabled // via the DisableMethods or DisablePointerMethods options. ContinueOnMethod bool + + // 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, + // other types will be sort according to the reflect.Value.String() output + // which guarantees display stability. + SortKeys bool } // Config is the active configuration of the top-level functions. diff --git a/spew/dump.go b/spew/dump.go index 216c5b3..771b473 100644 --- a/spew/dump.go +++ b/spew/dump.go @@ -24,6 +24,7 @@ import ( "os" "reflect" "regexp" + "sort" "strconv" "strings" ) @@ -241,6 +242,46 @@ func (d *dumpState) dumpSlice(v reflect.Value) { } } +type valuesSorter struct { + values []reflect.Value +} + +func (s *valuesSorter) Len() int { + return len(s.values) +} + +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] +} + +func (s *valuesSorter) Less(i, j int) bool { + switch s.values[i].Kind() { + case reflect.Bool: + return !s.values[i].Bool() && s.values[j].Bool() + case reflect.Float32, reflect.Float64: + return s.values[i].Float() < s.values[j].Float() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return s.values[i].Int() < s.values[j].Int() + case reflect.String: + return s.values[i].String() < s.values[j].String() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return s.values[i].Uint() < s.values[j].Uint() + case reflect.Uintptr: + return s.values[i].UnsafeAddr() < s.values[j].UnsafeAddr() + } + return s.values[i].String() < s.values[j].String() +} + +// Generic sort function for native types: int, uint, bool, string and uintptr. +// Other inputs are sort according to their Value.String() value to ensure +// display stability. +func SortValues(values []reflect.Value) { + if len(values) == 0 { + return + } + sort.Sort(&valuesSorter{values}) +} + // dump is the main workhorse for dumping a value. It uses the passed reflect // value to figure out what kind of object we are dealing with and formats it // appropriately. It is a recursive function, however circular data structures @@ -349,6 +390,9 @@ func (d *dumpState) dump(v reflect.Value) { } else { numEntries := v.Len() keys := v.MapKeys() + if d.cs.SortKeys { + SortValues(keys) + } for i, key := range keys { d.dump(d.unpackValue(key)) d.w.Write(colonSpaceBytes) diff --git a/spew/dump_test.go b/spew/dump_test.go index 31b7b6b..354c71c 100644 --- a/spew/dump_test.go +++ b/spew/dump_test.go @@ -65,6 +65,7 @@ import ( "bytes" "fmt" "github.com/davecgh/go-spew/spew" + "reflect" "testing" "unsafe" ) @@ -896,3 +897,44 @@ func TestDump(t *testing.T) { } } } + +func TestSortValues(t *testing.T) { + v := reflect.ValueOf + + a := v("a") + b := v("b") + c := v("c") + tests := []struct { + input []reflect.Value + expected []reflect.Value + }{ + {[]reflect.Value{v(2), v(1), v(3)}, + []reflect.Value{v(1), v(2), v(3)}}, + {[]reflect.Value{v(2.), v(1.), v(3.)}, + []reflect.Value{v(1.), v(2.), v(3.)}}, + {[]reflect.Value{v(false), v(true), v(false)}, + []reflect.Value{v(false), v(false), v(true)}}, + {[]reflect.Value{b, a, c}, + []reflect.Value{a, b, c}}, + } + for _, test := range tests { + spew.SortValues(test.input) + if !reflect.DeepEqual(test.input, test.expected) { + t.Errorf("Sort mismatch:\n %v != %v", test.input, test.expected) + } + } +} + +func TestDumpSortedKeys(t *testing.T) { + cfg := spew.ConfigState{SortKeys: true} + s := cfg.Sdump(map[int]string{1: "1", 3: "3", 2: "2"}) + expected := `(map[int]string) { +(int) 1: (string) "1", +(int) 2: (string) "2", +(int) 3: (string) "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 70785fa..fc57fda 100644 --- a/spew/format.go +++ b/spew/format.go @@ -302,6 +302,9 @@ func (f *formatState) format(v reflect.Value) { f.fs.Write(maxShortBytes) } else { keys := v.MapKeys() + if f.cs.SortKeys { + SortValues(keys) + } for i, key := range keys { if i > 0 { f.fs.Write(spaceBytes) diff --git a/spew/format_test.go b/spew/format_test.go index b151513..80c5ef9 100644 --- a/spew/format_test.go +++ b/spew/format_test.go @@ -1472,3 +1472,12 @@ func TestFormatter(t *testing.T) { } } } + +func TestPrintSortedKeys(t *testing.T) { + cfg := spew.ConfigState{SortKeys: true} + s := cfg.Sprint(map[int]string{1: "1", 3: "3", 2: "2"}) + expected := "map[1:1 2:2 3:3]" + if s != expected { + t.Errorf("Sorted keys mismatch:\n %v %v", s, expected) + } +}