Skip to content

Commit 70fe1c5

Browse files
committed
fix: rendering time values
This is inspired by stretchr#1829, but we proceed differently, not checking for a string type but for type convertibility to a time instead. Added more tests to check how embedded types, pointers etc actually render and don't cause panic. From the original issues: * fixes stretchr#1078 * fixes stretchr#1079 Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 9dae2c7 commit 70fe1c5

File tree

8 files changed

+372
-19
lines changed

8 files changed

+372
-19
lines changed

internal/spew/common.go

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,47 +98,99 @@ func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool)
9898
v = unsafeReflectValue(v)
9999
}
100100

101+
defer catchPanic(w, v)
102+
handled, continued := handleErrorOrStringer(cs, w, v)
103+
if handled {
104+
// short-circuit: if we can handle directly without trying to convert to a pointer receiver, we're done.
105+
// This allows avoiding to call unsafeReflectValue() or retrieving the value's address when not necessary.
106+
return true
107+
}
108+
if continued {
109+
// printed, and wants to return now to continue digging
110+
return false
111+
}
112+
101113
// Choose whether or not to do error and Stringer interface lookups against
102114
// the base type or a pointer to the base type depending on settings.
115+
//
103116
// Technically calling one of these methods with a pointer receiver can
104-
// mutate the value, however, types which choose to satisify an error or
117+
// mutate the value, however, types which choose to satisfy an error or
105118
// Stringer interface with a pointer receiver should not be mutating their
106119
// state inside these interface methods.
107120
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
121+
// since this is unsafe, there are a few edge cases where it doesn't work well
108122
v = unsafeReflectValue(v)
109123
}
124+
110125
if v.CanAddr() {
111126
v = v.Addr()
112127
}
113128

129+
handled, _ = handleErrorOrStringer(cs, w, v)
130+
131+
return handled
132+
}
133+
134+
// handleErrorOrString is only called when v.CanInterface().
135+
//
136+
// NOTE: we should prove that unsafReflectValue doesn't alter this property.
137+
func handleErrorOrStringer(cs *ConfigState, w io.Writer, v reflect.Value) (handled, continued bool) {
114138
// Is it an error or Stringer?
115139
switch iface := v.Interface().(type) {
116140
case error:
117-
defer catchPanic(w, v)
118141
if cs.ContinueOnMethod {
119142
w.Write(openParenBytes)
120143
w.Write([]byte(iface.Error()))
121144
w.Write(closeParenBytes)
122145
w.Write(spaceBytes)
123-
return false
146+
return false, true
124147
}
125148

126149
w.Write([]byte(iface.Error()))
127-
return true
150+
return true, false
128151

129152
case fmt.Stringer:
130-
defer catchPanic(w, v)
131153
if cs.ContinueOnMethod {
132154
w.Write(openParenBytes)
133155
w.Write([]byte(iface.String()))
134156
w.Write(closeParenBytes)
135157
w.Write(spaceBytes)
136-
return false
158+
return false, true
137159
}
160+
138161
w.Write([]byte(iface.String()))
139-
return true
162+
return true, false
163+
164+
default:
165+
// is it convertible to time.Time (or *time.Time)?
166+
converted, ok := isConvertibleToTime(v)
167+
if !ok {
168+
// can't handle this value
169+
return false, false
170+
}
171+
172+
if !converted.CanInterface() {
173+
return false, false // safeguard
174+
}
175+
176+
timeIface := converted.Interface()
177+
stringer, ok := timeIface.(fmt.Stringer)
178+
if !ok {
179+
return false, false // safeguard
180+
}
181+
182+
if cs.ContinueOnMethod {
183+
_, _ = w.Write(openParenBytes)
184+
_, _ = w.Write([]byte(stringer.String()))
185+
_, _ = w.Write(closeParenBytes)
186+
_, _ = w.Write(spaceBytes)
187+
return false, true
188+
}
189+
190+
_, _ = w.Write([]byte(stringer.String()))
191+
192+
return true, false
140193
}
141-
return false
142194
}
143195

144196
// printBool outputs a boolean value as true or false to Writer w.
@@ -242,12 +294,14 @@ func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
242294
vs.strings[i] = b.String()
243295
}
244296
}
297+
245298
if vs.strings == nil && cs.SpewKeys {
246299
vs.strings = make([]string, len(values))
247300
for i := range vs.values {
248301
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
249302
}
250303
}
304+
251305
return vs
252306
}
253307

internal/spew/config.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ type ConfigState struct {
5353
// invoked for types that implement them.
5454
DisableMethods bool
5555

56+
// EnableTimeStringer specifies whether to invoke the Stringer interface on
57+
// time.Time values even when DisableMethods is true. This is useful to get
58+
// human-readable output for time.Time values while keeping method calls
59+
// disabled for other types.
60+
EnableTimeStringer bool
61+
5662
// DisablePointerMethods specifies whether or not to check for and invoke
5763
// error and Stringer interfaces on types which only accept a pointer
5864
// receiver when the current type is not a pointer.
@@ -102,7 +108,10 @@ type ConfigState struct {
102108

103109
// Config is the active configuration of the top-level functions.
104110
// The configuration can be changed by modifying the contents of spew.Config.
105-
var Config = ConfigState{Indent: " "}
111+
var Config = ConfigState{ //nolint:gochecknoglobals // this is global configuration and we leave it for backward-compatibility
112+
Indent: " ",
113+
EnableTimeStringer: true,
114+
}
106115

107116
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
108117
// passed with a Formatter interface returned by c.NewFormatter. It returns

internal/spew/dump.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ func (d *dumpState) dump(v reflect.Value) {
303303

304304
// Call Stringer/error interfaces if they exist and the handle methods flag
305305
// is enabled
306-
if !d.cs.DisableMethods {
306+
if !d.cs.DisableMethods || (d.cs.EnableTimeStringer && isTime(v)) {
307307
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
308308
if handled := handleMethods(d.cs, d.w, v); handled {
309309
return

internal/spew/dump_test.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import (
6666
"fmt"
6767
"strconv"
6868
"testing"
69+
"time"
6970
"unsafe"
7071

7172
"github.com/go-openapi/testify/v2/internal/spew"
@@ -957,6 +958,91 @@ func addErrorDumpTests() {
957958
addDumpTest(nv, "(*"+vt+")(<nil>)\n")
958959
}
959960

961+
type (
962+
embeddedTime struct {
963+
time.Time
964+
}
965+
embeddedTimePtr struct { // this type is an example where the unsafeReflectValue does not work well
966+
*time.Time
967+
}
968+
redeclaredTime time.Time
969+
redeclaredTimePtr *time.Time
970+
aliasedTime = time.Time
971+
)
972+
973+
func addTimeDumpTests() {
974+
ts := time.Date(2006, time.January, 2, 15, 4, 5, 999999999, time.UTC)
975+
alias := aliasedTime(ts) //nolint:unconvert // we want to prove here that aliased types don't matter
976+
tsAddr := fmt.Sprintf("%p", &ts)
977+
es := embeddedTime{
978+
Time: ts,
979+
}
980+
ps := embeddedTimePtr{
981+
Time: &ts,
982+
}
983+
var tptr *time.Time
984+
panick := embeddedTimePtr{
985+
Time: nil,
986+
}
987+
rtptr := redeclaredTimePtr(&ts)
988+
ppts := ptr(ptr(ts))
989+
ppAddr := fmt.Sprintf("%p", ppts)
990+
ppIAddr := fmt.Sprintf("%p", *ppts)
991+
992+
addDumpTest(
993+
// simple time.Time
994+
ts,
995+
"(time.Time) 2006-01-02 15:04:05.999999999 +0000 UTC\n",
996+
)
997+
addDumpTest(
998+
// aliases are ignored at runtime
999+
alias,
1000+
"(time.Time) 2006-01-02 15:04:05.999999999 +0000 UTC\n",
1001+
)
1002+
addDumpTest(
1003+
// pointer to time.Time
1004+
&ts,
1005+
"(*time.Time)("+tsAddr+")(2006-01-02 15:04:05.999999999 +0000 UTC)\n",
1006+
)
1007+
addDumpTest(
1008+
// struct with embedded time.Time
1009+
es,
1010+
"(spew_test.embeddedTime) 2006-01-02 15:04:05.999999999 +0000 UTC\n",
1011+
)
1012+
addDumpTest(
1013+
// struct with embedded pointer to time.Time
1014+
ps,
1015+
"(spew_test.embeddedTimePtr) 2006-01-02 15:04:05.999999999 +0000 UTC\n",
1016+
)
1017+
addDumpTest(
1018+
// nil time.Time
1019+
tptr,
1020+
"(*time.Time)(<nil>)\n",
1021+
)
1022+
addDumpTest(
1023+
// **time.Time
1024+
ppts,
1025+
"(**time.Time)("+ppAddr+"->"+ppIAddr+")(2006-01-02 15:04:05.999999999 +0000 UTC)\n",
1026+
)
1027+
addDumpTest(
1028+
panick, // this is a stringer, but the inner member that implements String() string is nil
1029+
"(spew_test.embeddedTimePtr) (PANIC=runtime error: invalid memory address or nil pointer dereference){\n Time: (*time.Time)(<nil>)\n}\n",
1030+
)
1031+
addDumpTest(
1032+
// redeclared type convertible to time.Time
1033+
redeclaredTime(ts),
1034+
"(spew_test.redeclaredTime) 2006-01-02 15:04:05.999999999 +0000 UTC\n",
1035+
)
1036+
addDumpTest(
1037+
// redeclared type convertible to *time.Time
1038+
//
1039+
// NOTE: the information about the original (redeclared) type is lost. This is due to
1040+
// how displaying pointer type information is displayed (i.e. using v.Elem().Type()).
1041+
rtptr,
1042+
"(*time.Time)("+tsAddr+")(2006-01-02 15:04:05.999999999 +0000 UTC)\n",
1043+
)
1044+
}
1045+
9601046
// TestDump executes all of the tests described by dumpTests.
9611047
func TestDump(t *testing.T) {
9621048
// Setup tests.
@@ -979,14 +1065,15 @@ func TestDump(t *testing.T) {
9791065
addPanicDumpTests()
9801066
addErrorDumpTests()
9811067
addCgoDumpTests()
1068+
addTimeDumpTests()
9821069

9831070
t.Logf("Running %d tests", len(dumpTests))
9841071
for i, test := range dumpTests {
9851072
buf := new(bytes.Buffer)
9861073
spew.Fdump(buf, test.in)
9871074
s := buf.String()
9881075
if testFailed(s, test.wants) {
989-
t.Errorf("Dump #%d\n got: %s %s", i, s, stringizeWants(test.wants))
1076+
t.Errorf("Dump #%d\n got: %s %s", i, s, stringizeWants(test.wants))
9901077
continue
9911078
}
9921079
}
@@ -1041,3 +1128,8 @@ func TestDumpSortedKeys(t *testing.T) {
10411128
}
10421129

10431130
}
1131+
1132+
func ptr[T any](value T) *T {
1133+
v := value
1134+
return &v
1135+
}

internal/spew/format.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (f *formatState) format(v reflect.Value) {
222222

223223
// Call Stringer/error interfaces if they exist and the handle methods
224224
// flag is enabled.
225-
if !f.cs.DisableMethods {
225+
if !f.cs.DisableMethods || (f.cs.EnableTimeStringer && isTime(v)) { // we consider the case when we want times printed out
226226
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
227227
if handled := handleMethods(f.cs, f.fs, v); handled {
228228
return

0 commit comments

Comments
 (0)