Skip to content

Commit 7ff1365

Browse files
committed
feat: support fuzz data by Descriptor
1 parent 405ebed commit 7ff1365

File tree

2 files changed

+385
-0
lines changed

2 files changed

+385
-0
lines changed

thrift/test_util.go

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package thrift
1919
import (
2020
"context"
2121
"fmt"
22+
"math/rand"
23+
"time"
2224

2325
"github.com/cloudwego/dynamicgo/internal/util_test"
2426
)
@@ -55,3 +57,297 @@ func FnRequest(fn *FunctionDescriptor) *TypeDescriptor {
5557
// let-it-fail: it panic when something is nil
5658
return fn.Request().Struct().Fields()[0].Type()
5759
}
60+
61+
type FuzzDataOptions struct {
62+
// MaxDepth is the max depth of the generated data
63+
MaxDepth int
64+
// MaxWidth is the max width (map/list/string length) of the generated data
65+
MaxWidth int
66+
// DefaultFieldsRatio is the default ratio of fields to be filled in a struct
67+
DefaultFieldsRatio float64
68+
// OptionalFieldsRatio is the ratio of optional fields to be filled in a struct
69+
OptionalFieldsRatio float64
70+
}
71+
72+
// MakeFuzzData generates random data based on the given TypeDescriptor
73+
// It returns a map[string]interface{} like structure for complex types
74+
func MakeFuzzData(reqDesc *TypeDescriptor, opts FuzzDataOptions) (interface{}, error) {
75+
// Set default options
76+
if opts.MaxDepth <= 0 {
77+
opts.MaxDepth = 3
78+
}
79+
if opts.MaxWidth <= 0 {
80+
opts.MaxWidth = 1
81+
}
82+
if opts.DefaultFieldsRatio <= 0 {
83+
opts.DefaultFieldsRatio = 0.8
84+
}
85+
if opts.OptionalFieldsRatio <= 0 {
86+
opts.OptionalFieldsRatio = 0.5
87+
}
88+
89+
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
90+
return makeFuzzValue(reqDesc, opts, rng, 0)
91+
}
92+
93+
func makeFuzzValue(desc *TypeDescriptor, opts FuzzDataOptions, rng *rand.Rand, depth int) (interface{}, error) {
94+
if desc == nil {
95+
return nil, nil
96+
}
97+
98+
// Check depth limit
99+
if depth >= opts.MaxDepth {
100+
return getDefaultValue(desc.Type()), nil
101+
}
102+
103+
switch desc.Type() {
104+
case BOOL:
105+
return rng.Intn(2) == 1, nil
106+
107+
case BYTE: // I08 is the same as BYTE
108+
return int8(rng.Intn(256) - 128), nil
109+
110+
case I16:
111+
return int16(rng.Intn(65536) - 32768), nil
112+
113+
case I32:
114+
return rng.Int31(), nil
115+
116+
case I64:
117+
return rng.Int63(), nil
118+
119+
case DOUBLE:
120+
return rng.Float64() * 1000, nil
121+
122+
case STRING:
123+
return generateRandomString(rng, opts.MaxWidth), nil
124+
125+
case LIST:
126+
return makeFuzzList(desc, opts, rng, depth)
127+
128+
case SET:
129+
return makeFuzzList(desc, opts, rng, depth)
130+
131+
case MAP:
132+
return makeFuzzMap(desc, opts, rng, depth)
133+
134+
case STRUCT:
135+
return makeFuzzStruct(desc, opts, rng, depth)
136+
137+
default:
138+
return nil, nil
139+
}
140+
}
141+
142+
func makeFuzzList(desc *TypeDescriptor, opts FuzzDataOptions, rng *rand.Rand, depth int) (interface{}, error) {
143+
elemDesc := desc.Elem()
144+
if elemDesc == nil {
145+
return []interface{}{}, nil
146+
}
147+
148+
size := rng.Intn(opts.MaxWidth)
149+
list := make([]interface{}, size)
150+
151+
for i := 0; i < size; i++ {
152+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
153+
if err != nil {
154+
return nil, err
155+
}
156+
list[i] = val
157+
}
158+
159+
return list, nil
160+
}
161+
162+
func makeFuzzMap(desc *TypeDescriptor, opts FuzzDataOptions, rng *rand.Rand, depth int) (interface{}, error) {
163+
keyDesc := desc.Key()
164+
elemDesc := desc.Elem()
165+
166+
if keyDesc == nil || elemDesc == nil {
167+
return make(map[string]interface{}), nil
168+
}
169+
170+
size := rng.Intn(opts.MaxWidth)
171+
172+
switch keyDesc.Type() {
173+
case STRING:
174+
result := make(map[string]interface{}, size)
175+
for i := 0; i < size; i++ {
176+
key := generateRandomString(rng, 8)
177+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
178+
if err != nil {
179+
return nil, err
180+
}
181+
result[key] = val
182+
}
183+
return result, nil
184+
case I32:
185+
result := make(map[int32]interface{}, size)
186+
for i := 0; i < size; i++ {
187+
key := int32(i)
188+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
189+
if err != nil {
190+
return nil, err
191+
}
192+
result[key] = val
193+
}
194+
return result, nil
195+
case I64:
196+
result := make(map[int64]interface{}, size)
197+
for i := 0; i < size; i++ {
198+
key := int64(i)
199+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
200+
if err != nil {
201+
return nil, err
202+
}
203+
result[key] = val
204+
}
205+
return result, nil
206+
case I16:
207+
result := make(map[int16]interface{}, size)
208+
for i := 0; i < size; i++ {
209+
key := int16(i)
210+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
211+
if err != nil {
212+
return nil, err
213+
}
214+
result[key] = val
215+
}
216+
return result, nil
217+
case BYTE: // I08
218+
result := make(map[int8]interface{}, size)
219+
for i := 0; i < size; i++ {
220+
key := int8(i)
221+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
222+
if err != nil {
223+
return nil, err
224+
}
225+
result[key] = val
226+
}
227+
return result, nil
228+
case BOOL:
229+
result := make(map[bool]interface{}, 2)
230+
// ensure we can have up to two entries: true/false
231+
for i := 0; i < size && i < 2; i++ {
232+
key := (i%2 == 0)
233+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
234+
if err != nil {
235+
return nil, err
236+
}
237+
result[key] = val
238+
}
239+
return result, nil
240+
case DOUBLE:
241+
result := make(map[float64]interface{}, size)
242+
for i := 0; i < size; i++ {
243+
key := rng.Float64()
244+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
245+
if err != nil {
246+
return nil, err
247+
}
248+
result[key] = val
249+
}
250+
return result, nil
251+
default:
252+
// Fallback to string key if unsupported key type
253+
result := make(map[string]interface{}, size)
254+
for i := 0; i < size; i++ {
255+
key := fmt.Sprintf("key_%d_%s", i, generateRandomString(rng, 5))
256+
val, err := makeFuzzValue(elemDesc, opts, rng, depth+1)
257+
if err != nil {
258+
return nil, err
259+
}
260+
result[key] = val
261+
}
262+
return result, nil
263+
}
264+
}
265+
266+
func makeFuzzStruct(desc *TypeDescriptor, opts FuzzDataOptions, rng *rand.Rand, depth int) (interface{}, error) {
267+
if desc.Struct() == nil {
268+
return make(map[string]interface{}), nil
269+
}
270+
271+
result := make(map[string]interface{})
272+
fields := desc.Struct().Fields()
273+
274+
for _, field := range fields {
275+
if field == nil {
276+
continue
277+
}
278+
279+
// Decide whether to include this field based on requireness
280+
shouldInclude := shouldIncludeField(field, opts, rng)
281+
if !shouldInclude {
282+
continue
283+
}
284+
285+
fieldDesc := field.Type()
286+
if fieldDesc == nil {
287+
continue
288+
}
289+
290+
val, err := makeFuzzValue(fieldDesc, opts, rng, depth+1)
291+
if err != nil {
292+
return nil, err
293+
}
294+
295+
result[field.Name()] = val
296+
}
297+
298+
return result, nil
299+
}
300+
301+
func shouldIncludeField(field *FieldDescriptor, opts FuzzDataOptions, rng *rand.Rand) bool {
302+
req := field.Required()
303+
304+
// Always include required fields
305+
if req == RequiredRequireness {
306+
return true
307+
}
308+
309+
// Include default fields with high probability
310+
if req == DefaultRequireness {
311+
return rng.Float64() < opts.DefaultFieldsRatio
312+
}
313+
314+
// Include optional fields with lower probability
315+
return rng.Float64() < opts.OptionalFieldsRatio
316+
}
317+
318+
func generateRandomString(rng *rand.Rand, maxLen int) string {
319+
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
320+
length := rng.Intn(maxLen) + 1
321+
result := make([]byte, length)
322+
for i := 0; i < length; i++ {
323+
result[i] = charset[rng.Intn(len(charset))]
324+
}
325+
return string(result)
326+
}
327+
328+
func getDefaultValue(typ Type) interface{} {
329+
switch typ {
330+
case BOOL:
331+
return false
332+
case BYTE: // I08 is the same as BYTE
333+
return int8(0)
334+
case I16:
335+
return int16(0)
336+
case I32:
337+
return int32(0)
338+
case I64:
339+
return int64(0)
340+
case DOUBLE:
341+
return float64(0)
342+
case STRING:
343+
return ""
344+
case LIST, SET:
345+
return []interface{}{}
346+
case MAP:
347+
return make(map[string]interface{})
348+
case STRUCT:
349+
return make(map[string]interface{})
350+
default:
351+
return nil
352+
}
353+
}

thrift/test_util_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package thrift
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestMakeFuzzData_MapInt64Key(t *testing.T) {
8+
// Build a MAP descriptor: key=I64, elem=STRING
9+
keyDesc := &TypeDescriptor{typ: I64}
10+
elemDesc := &TypeDescriptor{typ: STRING}
11+
desc := &TypeDescriptor{typ: MAP, key: keyDesc, elem: elemDesc}
12+
13+
opts := FuzzDataOptions{MaxDepth: 2, MaxWidth: 2}
14+
15+
data, err := MakeFuzzData(desc, opts)
16+
if err != nil {
17+
t.Fatalf("MakeFuzzData error: %v", err)
18+
}
19+
20+
// Expect map[int64]interface{}
21+
if _, ok := data.(map[int64]interface{}); !ok {
22+
t.Fatalf("expected map[int64]interface{}, got %T", data)
23+
}
24+
}
25+
26+
// TestMakeFuzzData_ComplexIDL builds a descriptor from an IDL string containing
27+
// struct/list/map/int/string/float/bool, then validates the generated fuzz data types.
28+
func TestMakeFuzzData_ComplexIDL(t *testing.T) {
29+
content := `
30+
namespace go test
31+
32+
struct Inner {
33+
1: required i32 a
34+
}
35+
36+
struct Complex {
37+
1: required bool b
38+
2: required i32 i
39+
3: required double f
40+
4: required string s
41+
5: required list<i64> l
42+
6: required map<i32, string> m
43+
7: required Inner inner
44+
}
45+
46+
service S {
47+
void ExampleMethod(1: Complex req)
48+
}
49+
`
50+
51+
fn, err := GetDescFromContent(content, "ExampleMethod", &Options{})
52+
if err != nil {
53+
t.Fatalf("failed to build descriptor from content: %v", err)
54+
}
55+
56+
reqDesc := FnRequest(fn)
57+
data, err := MakeFuzzData(reqDesc, FuzzDataOptions{MaxDepth: 5, MaxWidth: 3, DefaultFieldsRatio: 1.0, OptionalFieldsRatio: 1.0})
58+
if err != nil {
59+
t.Fatalf("MakeFuzzData error: %v", err)
60+
}
61+
62+
root, ok := data.(map[string]interface{})
63+
if !ok {
64+
t.Fatalf("expected root map[string]interface{}, got %T", data)
65+
}
66+
67+
// Validate presence and types of each required field
68+
if _, ok := root["b"].(bool); !ok {
69+
t.Fatalf("field b: expected bool, got %T", root["b"])
70+
}
71+
if _, ok := root["i"].(int32); !ok {
72+
t.Fatalf("field i: expected int32, got %T", root["i"])
73+
}
74+
if _, ok := root["f"].(float64); !ok {
75+
t.Fatalf("field f: expected float64, got %T", root["f"])
76+
}
77+
if _, ok := root["s"].(string); !ok {
78+
t.Fatalf("field s: expected string, got %T", root["s"])
79+
}
80+
if _, ok := root["l"].([]interface{}); !ok {
81+
t.Fatalf("field l: expected []interface{}, got %T", root["l"])
82+
}
83+
if _, ok := root["m"].(map[int32]interface{}); !ok {
84+
t.Fatalf("field m: expected map[int32]interface{}, got %T", root["m"])
85+
}
86+
if _, ok := root["inner"].(map[string]interface{}); !ok {
87+
t.Fatalf("field inner: expected map[string]interface{}, got %T", root["inner"])
88+
}
89+
}

0 commit comments

Comments
 (0)