Skip to content

Commit 9a8080e

Browse files
committed
fix(spew): fixed more panic & hang cases in the internalized spew
* fixed case of circular reference that hangs * fixed unrecoverable panic when dumping a map with a circular reference test(integrationtest): added new module for integration tests This new module takes a systematic approach to explore edge cases in spew by producing random scenarios with uncommon data types. This approach uncovered 2 bugs in spew (fixed above)... and one in the fmt standard library. This new module leverages the rapid library to build generators and exercises spew.Sdump. The "property based" tests produced with rapid are generalized in a fuzz test. Signed-off-by: Frederic BIDON <[email protected]>
1 parent 991556d commit 9a8080e

File tree

16 files changed

+2035
-36
lines changed

16 files changed

+2035
-36
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ linters:
3030
max-complexity: 22
3131
gocyclo:
3232
min-complexity: 22
33+
gocognit:
34+
min-complexity: 35
3335
exhaustive:
3436
default-signifies-exhaustive: true
3537
default-case-required: true

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use (
22
.
33
./codegen
44
./enable/yaml
5+
./internal/testintegration
56
)
67

78
go 1.24.0

internal/assertions/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// whereas the publicly exposed API (in packages assert and require)
99
// is generated.
1010
//
11+
// For convenience, assertion functions are classified by domain.
12+
// The entire API can be searched by domain at https://go-openapi.github.io/testify/api.
1113
// # Domains
1214
//
1315
// - boolean: asserting boolean values

internal/spew/dump.go

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,12 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
9797
cycleFound := false
9898
indirects := 0
9999
ve := v
100-
for ve.Kind() == reflect.Ptr {
100+
for ve.Kind() == reflect.Pointer {
101101
if ve.IsNil() {
102102
nilFound = true
103103
break
104104
}
105+
105106
indirects++
106107
addr := ve.Pointer()
107108
pointerChain = append(pointerChain, addr)
@@ -114,11 +115,27 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
114115

115116
ve = ve.Elem()
116117
if ve.Kind() == reflect.Interface {
117-
if ve.IsNil() {
118+
if ve.IsNil() { // interface with nil value
118119
nilFound = true
119120
break
120121
}
121122
ve = ve.Elem()
123+
if ve.Kind() == reflect.Pointer {
124+
if ve.IsNil() {
125+
nilFound = true
126+
break
127+
}
128+
129+
// case of interface containing a pointer that cycles to the same depth level.
130+
// If we have a cycle at the same level, if some break the loop now
131+
addr = ve.Pointer()
132+
if pd, ok := d.pointers[addr]; ok && pd <= d.depth {
133+
cycleFound = true
134+
indirects--
135+
break
136+
}
137+
d.pointers[addr] = d.depth
138+
}
122139
}
123140
}
124141

@@ -156,6 +173,64 @@ func (d *dumpState) dumpPtr(v reflect.Value) {
156173
d.w.Write(closeParenBytes)
157174
}
158175

176+
func (d *dumpState) dumpMap(v reflect.Value) {
177+
// Remove pointers at or below the current depth from map used to detect
178+
// circular refs.
179+
for k, depth := range d.pointers {
180+
if depth >= d.depth {
181+
delete(d.pointers, k)
182+
}
183+
}
184+
185+
// Keep list of all dereferenced pointers to show later.
186+
cycleFound := false
187+
188+
// nil maps should be indicated as different than empty maps
189+
if v.IsNil() {
190+
_, _ = d.w.Write(nilAngleBytes)
191+
return
192+
}
193+
194+
// maps like pointers may present circular references
195+
addr := v.Pointer()
196+
if pd, ok := d.pointers[addr]; ok && pd <= d.depth {
197+
cycleFound = true
198+
}
199+
d.pointers[addr] = d.depth
200+
201+
_, _ = d.w.Write(openBraceNewlineBytes)
202+
d.depth++
203+
204+
switch {
205+
case d.cs.MaxDepth != 0 && d.depth > d.cs.MaxDepth:
206+
d.indent()
207+
_, _ = d.w.Write(maxNewlineBytes)
208+
case cycleFound:
209+
_, _ = d.w.Write(circularBytes)
210+
default:
211+
numEntries := v.Len()
212+
keys := v.MapKeys()
213+
if d.cs.SortKeys {
214+
sortValues(keys, d.cs)
215+
}
216+
for i, key := range keys {
217+
d.dump(d.unpackValue(key))
218+
_, _ = d.w.Write(colonSpaceBytes)
219+
d.ignoreNextIndent = true
220+
d.dump(d.unpackValue(v.MapIndex(key)))
221+
if i < (numEntries - 1) {
222+
_, _ = d.w.Write(commaNewlineBytes)
223+
} else {
224+
_, _ = d.w.Write(newlineBytes)
225+
}
226+
}
227+
}
228+
229+
d.depth--
230+
d.indent()
231+
_, _ = d.w.Write(closeBraceBytes)
232+
}
233+
159234
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
160235
// reflection) arrays and slices are dumped in hexdump -C fashion.
161236
func (d *dumpState) dumpSlice(v reflect.Value) {
@@ -257,7 +332,7 @@ func (d *dumpState) dump(v reflect.Value) {
257332
}
258333

259334
// Handle pointers specially.
260-
if kind == reflect.Ptr {
335+
if kind == reflect.Pointer {
261336
d.indent()
262337
d.dumpPtr(v)
263338
return
@@ -367,43 +442,12 @@ func (d *dumpState) dump(v reflect.Value) {
367442
d.w.Write(nilAngleBytes)
368443
}
369444

370-
case reflect.Ptr:
445+
case reflect.Pointer:
371446
// Do nothing. We should never get here since pointers have already
372447
// been handled above.
373448

374449
case reflect.Map:
375-
// nil maps should be indicated as different than empty maps
376-
if v.IsNil() {
377-
d.w.Write(nilAngleBytes)
378-
break
379-
}
380-
381-
d.w.Write(openBraceNewlineBytes)
382-
d.depth++
383-
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
384-
d.indent()
385-
d.w.Write(maxNewlineBytes)
386-
} else {
387-
numEntries := v.Len()
388-
keys := v.MapKeys()
389-
if d.cs.SortKeys {
390-
sortValues(keys, d.cs)
391-
}
392-
for i, key := range keys {
393-
d.dump(d.unpackValue(key))
394-
d.w.Write(colonSpaceBytes)
395-
d.ignoreNextIndent = true
396-
d.dump(d.unpackValue(v.MapIndex(key)))
397-
if i < (numEntries - 1) {
398-
d.w.Write(commaNewlineBytes)
399-
} else {
400-
d.w.Write(newlineBytes)
401-
}
402-
}
403-
}
404-
d.depth--
405-
d.indent()
406-
d.w.Write(closeBraceBytes)
450+
d.dumpMap(v)
407451

408452
case reflect.Struct:
409453
d.w.Write(openBraceNewlineBytes)

internal/spew/edgecases_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package spew
2+
3+
import (
4+
"io"
5+
"iter"
6+
"slices"
7+
"sync"
8+
"testing"
9+
)
10+
11+
func TestEdgeCases(t *testing.T) {
12+
t.Parallel()
13+
cfg := Config
14+
output := io.Discard
15+
16+
for tt := range edgeCases() {
17+
t.Run(tt.name, func(t *testing.T) {
18+
t.Parallel()
19+
20+
defer func() {
21+
if r := recover(); r != nil {
22+
t.Errorf("fdump panicked: %v\nWith value type: %T\nValue: %#v", r, tt.value, tt.value)
23+
24+
return
25+
}
26+
}()
27+
28+
fdump(&cfg, output, tt.value)
29+
})
30+
}
31+
}
32+
33+
type edgeCase struct {
34+
name string
35+
value any
36+
}
37+
38+
func edgeCases() iter.Seq[edgeCase] {
39+
type withLock struct {
40+
sync.Mutex
41+
}
42+
type withLockPtr struct {
43+
*sync.Mutex
44+
}
45+
var iface any = "x"
46+
str := "y"
47+
var ifaceToPtr any = &str
48+
ifacePtr := &iface
49+
ifacePtrPtr := &ifaceToPtr
50+
var ifaceCircular any
51+
ifaceCircular = &ifaceCircular
52+
53+
// Map with circular value (map contains itself)
54+
m := map[string]any{
55+
"key1": 1,
56+
"key2": "val",
57+
}
58+
m["circular"] = m // Map contains itself as a value
59+
60+
return slices.Values([]edgeCase{
61+
{
62+
name: "self-referencing map",
63+
value: m,
64+
},
65+
{
66+
name: "sync.Mutex",
67+
value: withLock{},
68+
},
69+
{
70+
name: "*sync.Mutex",
71+
value: withLockPtr{
72+
Mutex: &sync.Mutex{},
73+
},
74+
},
75+
{
76+
name: "pointer to interface",
77+
value: ifacePtr,
78+
},
79+
{
80+
name: "pointer to interface to pointer",
81+
value: ifacePtrPtr,
82+
},
83+
{
84+
name: "pointer to pointer to interface to pointer",
85+
value: &ifaceToPtr,
86+
},
87+
{
88+
// case that used to hang
89+
name: "pointer to interface with circular reference",
90+
value: &ifaceCircular,
91+
},
92+
})
93+
}

0 commit comments

Comments
 (0)