Skip to content

Commit a77bd23

Browse files
authored
fix: rendering time diff (#27)
* fix: rendering time values * added sortability for time.Time 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 * doc: fixed a few hiccups in README Signed-off-by: Frederic BIDON <[email protected]> --------- Signed-off-by: Frederic BIDON <[email protected]>
1 parent c889903 commit a77bd23

File tree

11 files changed

+602
-32
lines changed

11 files changed

+602
-32
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Becomes:
7979
```go
8080
import (
8181
"testing"
82-
"github.com/go-openapi/testify/v2"
82+
"github.com/go-openapi/testify/v2/require"
8383
)
8484
...
8585

docs/doc-site/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ To use this package in your projects:
8686
```go
8787
import (
8888
"testing"
89-
"github.com/go-openapi/testify/v2"
89+
"github.com/go-openapi/testify/v2/require"
9090
)
9191
...
9292

internal/spew/common.go

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"reflect"
2424
"sort"
2525
"strconv"
26+
"time"
2627
)
2728

2829
// Some constants in the form of bytes to avoid string overhead. This mirrors
@@ -98,47 +99,100 @@ func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool)
9899
v = unsafeReflectValue(v)
99100
}
100101

102+
defer catchPanic(w, v)
103+
handled, continued := handleErrorOrStringer(cs, w, v)
104+
if handled {
105+
// short-circuit: if we can handle directly without trying to convert to a pointer receiver, we're done.
106+
// This allows avoiding to call unsafeReflectValue() or retrieving the value's address when not necessary.
107+
return true
108+
}
109+
if continued {
110+
// printed, and wants to return now to continue digging
111+
return false
112+
}
113+
101114
// Choose whether or not to do error and Stringer interface lookups against
102115
// the base type or a pointer to the base type depending on settings.
116+
//
103117
// Technically calling one of these methods with a pointer receiver can
104-
// mutate the value, however, types which choose to satisify an error or
118+
// mutate the value, however, types which choose to satisfy an error or
105119
// Stringer interface with a pointer receiver should not be mutating their
106120
// state inside these interface methods.
107121
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
122+
// since this is unsafe, there are a few edge cases where it doesn't work well
108123
v = unsafeReflectValue(v)
109124
}
125+
110126
if v.CanAddr() {
111127
v = v.Addr()
112128
}
113129

130+
handled, _ = handleErrorOrStringer(cs, w, v)
131+
132+
return handled
133+
}
134+
135+
// handleErrorOrString is only called when v.CanInterface().
136+
//
137+
// NOTE: we should prove that unsafReflectValue doesn't alter this property.
138+
func handleErrorOrStringer(cs *ConfigState, w io.Writer, v reflect.Value) (handled, continued bool) {
114139
// Is it an error or Stringer?
115140
switch iface := v.Interface().(type) {
116141
case error:
117-
defer catchPanic(w, v)
118142
if cs.ContinueOnMethod {
119143
w.Write(openParenBytes)
120144
w.Write([]byte(iface.Error()))
121145
w.Write(closeParenBytes)
122146
w.Write(spaceBytes)
123-
return false
147+
return false, true
124148
}
125149

126150
w.Write([]byte(iface.Error()))
127-
return true
151+
return true, false
128152

129153
case fmt.Stringer:
130-
defer catchPanic(w, v)
131154
if cs.ContinueOnMethod {
132155
w.Write(openParenBytes)
133156
w.Write([]byte(iface.String()))
134157
w.Write(closeParenBytes)
135158
w.Write(spaceBytes)
136-
return false
159+
return false, true
137160
}
161+
138162
w.Write([]byte(iface.String()))
139-
return true
163+
return true, false
164+
165+
default:
166+
// is it convertible to time.Time (or *time.Time)?
167+
converted, ok := isConvertibleToTime(v)
168+
if !ok {
169+
// can't handle this value
170+
return false, false
171+
}
172+
173+
// rendering time.Time
174+
if !converted.CanInterface() {
175+
return false, false // safeguard: should never be the case
176+
}
177+
178+
timeIface := converted.Interface()
179+
stringer, ok := timeIface.(fmt.Stringer)
180+
if !ok {
181+
return false, false // safeguard: should never be the case
182+
}
183+
184+
if cs.ContinueOnMethod {
185+
_, _ = w.Write(openParenBytes)
186+
_, _ = w.Write([]byte(stringer.String()))
187+
_, _ = w.Write(closeParenBytes)
188+
_, _ = w.Write(spaceBytes)
189+
return false, true
190+
}
191+
192+
_, _ = w.Write([]byte(stringer.String()))
193+
194+
return true, false
140195
}
141-
return false
142196
}
143197

144198
// printBool outputs a boolean value as true or false to Writer w.
@@ -226,12 +280,21 @@ type valuesSorter struct {
226280
// newValuesSorter initializes a valuesSorter instance, which holds a set of
227281
// surrogate keys on which the data should be sorted. It uses flags in
228282
// ConfigState to decide if and how to populate those surrogate keys.
283+
//
284+
// Values that are convertible to a time.Time are compared using [time.Time.Compare()].
285+
// This is safer than sorting their string representation, because the [time.Location]
286+
// may differ.
287+
//
288+
// NOTE: if all values are not of the same underlying type,
289+
// comparison will resort to the default reflect.Value.String() representation.
229290
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
230291
vs := &valuesSorter{values: values, cs: cs}
231-
if canSortSimply(vs.values[0].Kind()) {
292+
v0 := vs.values[0]
293+
if canSortSimply(v0.Kind()) || isTime(v0) {
232294
return vs
233295
}
234-
if !cs.DisableMethods {
296+
297+
if !vs.cs.DisableMethods {
235298
vs.strings = make([]string, len(values))
236299
for i := range vs.values {
237300
b := bytes.Buffer{}
@@ -242,12 +305,14 @@ func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
242305
vs.strings[i] = b.String()
243306
}
244307
}
308+
245309
if vs.strings == nil && cs.SpewKeys {
246310
vs.strings = make([]string, len(values))
247311
for i := range vs.values {
248312
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
249313
}
250314
}
315+
251316
return vs
252317
}
253318

@@ -291,6 +356,15 @@ func (s *valuesSorter) Swap(i, j int) {
291356
}
292357
}
293358

359+
// Less returns whether the value at index i should sort before the
360+
// value at index j. It is part of the sort.Interface implementation.
361+
func (s *valuesSorter) Less(i, j int) bool {
362+
if s.strings == nil {
363+
return valueSortLess(s.values[i], s.values[j])
364+
}
365+
return s.strings[i] < s.strings[j]
366+
}
367+
294368
// valueSortLess returns whether the first value should sort before the second
295369
// value. It is used by valueSorter.Less as part of the sort.Interface
296370
// implementation.
@@ -327,17 +401,64 @@ func valueSortLess(a, b reflect.Value) bool {
327401
}
328402
fallthrough
329403
default:
330-
return a.String() < b.String()
404+
isTimeA := isTime(a)
405+
isTimeB := isTime(b)
406+
if !isTimeA || !isTimeB {
407+
return a.String() < b.String()
408+
}
409+
410+
return timeLess(a, b)
331411
}
332412
}
333413

334-
// Less returns whether the value at index i should sort before the
335-
// value at index j. It is part of the sort.Interface implementation.
336-
func (s *valuesSorter) Less(i, j int) bool {
337-
if s.strings == nil {
338-
return valueSortLess(s.values[i], s.values[j])
414+
// timeLess compare two values that convert to a time.Time.
415+
//
416+
// This handles nil values gracefully.
417+
func timeLess(a, b reflect.Value) bool {
418+
convertedA, okForA := isConvertibleToTime(a)
419+
convertedB, okForB := isConvertibleToTime(b)
420+
if !okForA || !okForB {
421+
// types can convert, but not values: this means we have at least one nil
422+
if !okForA { // a =< b because nil =< (any value)
423+
return true
424+
}
425+
426+
return false // b > a
339427
}
340-
return s.strings[i] < s.strings[j]
428+
429+
// time comparison: we need to retrieve the time.Time values from a chain of pointers
430+
for convertedA.Kind() == reflect.Pointer && convertedA.IsValid() {
431+
convertedA = reflect.Indirect(convertedA)
432+
}
433+
for convertedB.Kind() == reflect.Pointer && convertedB.IsValid() {
434+
convertedB = reflect.Indirect(convertedB)
435+
}
436+
437+
// handle the case when some values are nil, after resolving pointers
438+
switch {
439+
case !convertedA.IsValid(): // nil <= (any value)
440+
return true
441+
case !convertedB.IsValid(): // (valid) > nil
442+
return false
443+
}
444+
445+
okIfaceA := convertedA.CanInterface()
446+
okIfaceB := convertedB.CanInterface()
447+
448+
if !okIfaceA || !okIfaceB {
449+
// defensive safeguard (should never get there, since we have successfully converted)
450+
return a.String() < b.String()
451+
}
452+
453+
tA, okTimeA := convertedA.Interface().(time.Time)
454+
tB, okTimeB := convertedB.Interface().(time.Time)
455+
456+
if !okTimeA || !okTimeB {
457+
// defensive safeguard (should never get there, since we have successfully indirected and converted)
458+
return a.String() < b.String()
459+
}
460+
461+
return tA.Compare(tB) <= 0
341462
}
342463

343464
// sortValues is a sort function that handles both native types and any type that

0 commit comments

Comments
 (0)