Skip to content

Commit 62d9872

Browse files
authored
Merge pull request #27 from mroth/fuzz
Implement fuzz testing
2 parents 44586fa + 89fb685 commit 62d9872

File tree

4 files changed

+145
-5
lines changed

4 files changed

+145
-5
lines changed

Diff for: fuzz_test.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
package weightedrand
5+
6+
import (
7+
"encoding/binary"
8+
"errors"
9+
"fmt"
10+
"reflect"
11+
"testing"
12+
)
13+
14+
// Fuzz testing does not support slices as a corpus type in go1.18, thus we
15+
// write a bunch of boilerplate here to allow us to encode []uint64 as []byte
16+
// for kicks.
17+
18+
func bEncodeSlice(xs []uint64) []byte {
19+
bs := make([]byte, len(xs)*8)
20+
for i, x := range xs {
21+
n := i * 8
22+
binary.LittleEndian.PutUint64(bs[n:], x)
23+
}
24+
return bs
25+
}
26+
27+
func bDecodeSlice(bs []byte) []uint64 {
28+
n := len(bs) / 8
29+
xs := make([]uint64, 0, n)
30+
for i := 0; i < n; i++ {
31+
x := binary.LittleEndian.Uint64(bs[8*i:])
32+
xs = append(xs, x)
33+
}
34+
return xs
35+
}
36+
37+
// test our own encoder to make sure we didn't introduce errors.
38+
func Test_bEncodeSlice(t *testing.T) {
39+
var testcases = [][]uint64{
40+
{},
41+
{1},
42+
{42},
43+
{912346},
44+
{1, 2},
45+
{1, 1, 1},
46+
{1, 2, 3},
47+
{1, 1000000},
48+
{1, 2, 3, 4, 5, 6, 7, 8, 9},
49+
}
50+
for _, tc := range testcases {
51+
t.Run(fmt.Sprintf("%v", tc), func(t *testing.T) {
52+
before := tc
53+
encoded := bEncodeSlice(before)
54+
if want, got := len(before)*8, len(encoded); want != got {
55+
t.Errorf("encoded length not as expected: want %d got %d", want, got)
56+
}
57+
decoded := bDecodeSlice(encoded)
58+
if !reflect.DeepEqual(before, decoded) {
59+
t.Errorf("want %v got %v", before, decoded)
60+
}
61+
})
62+
}
63+
}
64+
65+
func FuzzNewChooser(f *testing.F) {
66+
var fuzzcases = [][]uint64{
67+
{},
68+
{0},
69+
{1},
70+
{1, 1},
71+
{1, 2, 3},
72+
{0, 1, 2},
73+
}
74+
for _, tc := range fuzzcases {
75+
f.Add(bEncodeSlice(tc))
76+
}
77+
78+
f.Fuzz(func(t *testing.T, encodedWeights []byte) {
79+
weights := bDecodeSlice(encodedWeights)
80+
const sentinel = 1
81+
82+
cs := make([]Choice[int, uint64], 0, len(weights))
83+
for _, w := range weights {
84+
cs = append(cs, Choice[int, uint64]{Item: sentinel, Weight: w})
85+
}
86+
87+
// fuzz for error or panic on NewChooser
88+
c, err := NewChooser(cs...)
89+
if err != nil && !errors.Is(err, errNoValidChoices) && !errors.Is(err, errWeightOverflow) {
90+
t.Fatal(err)
91+
}
92+
93+
if err == nil {
94+
result := c.Pick() // fuzz for panic on Panic
95+
if result != sentinel { // fuzz for returned value unexpected (just use same non-zero sentinel value for all choices)
96+
t.Fatalf("expected %v got %v", sentinel, result)
97+
}
98+
}
99+
})
100+
}

Diff for: testdata/fuzz/FuzzNewChooser/a547669aeb7ca0ca

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("0000000\x8f00000000")

Diff for: weightedrand.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,16 @@ func NewChooser[T any, W integer](choices ...Choice[T, W]) (*Chooser[T, W], erro
4848
totals := make([]int, len(choices))
4949
runningTotal := 0
5050
for i, c := range choices {
51-
weight := int(c.Weight)
52-
if weight < 0 {
51+
if c.Weight < 0 {
5352
continue // ignore negative weights, can never be picked
5453
}
5554

55+
// case of single ~uint64 or similar value that exceeds maxInt on its own
56+
if uint64(c.Weight) >= maxInt {
57+
return nil, errWeightOverflow
58+
}
59+
60+
weight := int(c.Weight) // convert weight to int for internal counter usage
5661
if (maxInt - runningTotal) <= weight {
5762
return nil, errWeightOverflow
5863
}
@@ -68,8 +73,9 @@ func NewChooser[T any, W integer](choices ...Choice[T, W]) (*Chooser[T, W], erro
6873
}
6974

7075
const (
71-
intSize = 32 << (^uint(0) >> 63) // cf. strconv.IntSize
72-
maxInt = 1<<(intSize-1) - 1
76+
intSize = 32 << (^uint(0) >> 63) // cf. strconv.IntSize
77+
maxInt = 1<<(intSize-1) - 1
78+
maxUint64 = 1<<64 - 1
7379
)
7480

7581
// Possible errors returned by NewChooser, preventing the creation of a Chooser

Diff for: weightedrand_test.go

+33-1
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,42 @@ func TestNewChooser(t *testing.T) {
8181
}
8282
for _, tt := range tests {
8383
t.Run(tt.name, func(t *testing.T) {
84-
_, err := NewChooser(tt.cs...)
84+
c, err := NewChooser(tt.cs...)
8585
if err != tt.wantErr {
8686
t.Errorf("NewChooser() error = %v, wantErr %v", err, tt.wantErr)
8787
}
88+
89+
if err == nil { // run a few Picks to make sure there are no panics
90+
for i := 0; i < 10; i++ {
91+
_ = c.Pick()
92+
}
93+
}
94+
})
95+
}
96+
97+
u64tests := []struct {
98+
name string
99+
cs []Choice[rune, uint64]
100+
wantErr error
101+
}{
102+
{
103+
name: "weight overflow from single uint64 exceeding system maxInt",
104+
cs: []Choice[rune, uint64]{{Item: 'a', Weight: maxInt + 1}},
105+
wantErr: errWeightOverflow,
106+
},
107+
}
108+
for _, tt := range u64tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
c, err := NewChooser(tt.cs...)
111+
if err != tt.wantErr {
112+
t.Errorf("NewChooser() error = %v, wantErr %v", err, tt.wantErr)
113+
}
114+
115+
if err == nil { // run a few Picks to make sure there are no panics
116+
for i := 0; i < 10; i++ {
117+
_ = c.Pick()
118+
}
119+
}
88120
})
89121
}
90122
}

0 commit comments

Comments
 (0)