Adds config option DisableNilValues, which specifies whether or not to include

nil fields. This is useful in tests that rely on "golden" test data: Often one
adds a new, optional field to a struct, and it's onerous to update all test
data to reflect this field, which would now be nil everywhere.
This commit is contained in:
Alexander Staubo 2016-11-21 13:19:51 -05:00
parent 74bd53c438
commit 51a089712b
3 changed files with 101 additions and 16 deletions

View File

@ -67,6 +67,12 @@ type ConfigState struct {
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisableNilValues specifies whether or not to include nil fields. This is
// useful in tests that rely on "golden" test data: Often one adds a new,
// optional field to a struct, and it's onerous to update all test data
// to reflect this field, which would now be nil everywhere.
DisableNilValues bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool

View File

@ -234,10 +234,16 @@ func (d *dumpState) dumpSlice(v reflect.Value) {
}
// Recursively call dump for each item.
l := list{dumpState: d}
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
d.writeComma(i < (numEntries - 1))
entryVal := d.unpackValue(v.Index(i))
if !d.filterValue(entryVal) {
continue
}
l.beginEntry()
d.dump(entryVal)
}
l.end()
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
@ -378,18 +384,23 @@ func (d *dumpState) dump(v reflect.Value) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
sortValues(keys, d.cs)
}
for i, key := range keys {
l := list{dumpState: d}
for _, key := range keys {
mapValue := d.unpackValue(v.MapIndex(key))
if !d.filterValue(mapValue) {
continue
}
l.beginEntry()
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.MapIndex(key)))
d.writeComma(i < (numEntries - 1))
d.dump(mapValue)
}
l.end()
}
d.depth--
d.indent()
@ -404,15 +415,20 @@ func (d *dumpState) dump(v reflect.Value) {
} else {
vt := v.Type()
numFields := v.NumField()
l := list{dumpState: d, indented: true}
for i := 0; i < numFields; i++ {
d.indent()
fieldValue := d.unpackValue(v.Field(i))
if !d.filterValue(fieldValue) {
continue
}
l.beginEntry()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.Field(i)))
d.writeComma(i < (numFields - 1))
d.dump(fieldValue)
}
l.end()
}
d.depth--
d.indent()
@ -436,9 +452,9 @@ func (d *dumpState) dump(v reflect.Value) {
}
}
// writeComma emits a comma if hasMoreElements is true, or if trailing commas
// are always enabled.
func (d *dumpState) writeComma(hasMoreElements bool) {
// writeDelimiter emits a comma if hasMoreElements is true, or if trailing
// commas are always enabled.
func (d *dumpState) writeDelimiter(hasMoreElements bool) {
if hasMoreElements || d.cs.AlwaysIncludeTrailingComma {
d.w.Write(commaNewlineBytes)
} else {
@ -446,15 +462,57 @@ func (d *dumpState) writeComma(hasMoreElements bool) {
}
}
// filterValue returns true if a value should be dumped.
func (d *dumpState) filterValue(value reflect.Value) bool {
if !d.cs.DisableNilValues {
return true
}
return !isNil(value)
}
// list is a small helper to write lists and ensure that each item is
// correctly delimited, and that nothing is emitted if no entires were added.
type list struct {
count int
indented bool
*dumpState
}
func (l *list) beginEntry() {
if l.count > 0 {
l.writeDelimiter(true)
}
l.count++
if l.indented {
l.indent()
}
}
func (l *list) end() {
if l.count > 0 {
l.writeDelimiter(false)
}
}
func isNil(v reflect.Value) bool {
switch v.Kind() {
case reflect.Interface, reflect.Map, reflect.Slice, reflect.Ptr:
return v.IsNil()
}
return false
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
for _, arg := range a {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
if !cs.DisableNilValues {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
}
continue
}

View File

@ -133,6 +133,7 @@ func initSpewTests() {
scsNoPtrAddr := &spew.ConfigState{DisablePointerAddresses: true}
scsNoCap := &spew.ConfigState{DisableCapacities: true}
scsTrailingComma := &spew.ConfigState{Indent: " ", AlwaysIncludeTrailingComma: true}
scsNoNils := &spew.ConfigState{Indent: " ", DisableNilValues: true}
// Variables for tests on types which implement Stringer interface with and
// without a pointer receiver.
@ -144,6 +145,17 @@ func initSpewTests() {
}
tptr := &ptrTester{s: &struct{}{}}
type testIntf interface {
TestyTesty()
}
type nilTester struct {
s *struct{}
slice []interface{}
m map[string]interface{}
intf testIntf
}
// depthTester is used to test max depth handling for structs, array, slices
// and maps.
type depthTester struct {
@ -226,6 +238,15 @@ func initSpewTests() {
" (string) (len=3) \"one\": (int) 1,\n" +
" },\n" +
"}\n"},
{scsNoNils, fCSSdump, "", nilTester{}, "(spew_test.nilTester) {\n}\n"},
{scsNoNils, fCSSdump, "", nilTester{
slice: []interface{}{nil, "foo"},
},
"(spew_test.nilTester) {\n" +
" slice: ([]interface {}) (len=2 cap=2) {\n" +
" (string) (len=3) \"foo\"\n" +
" }\n" +
"}\n"},
}
}