Skip to content

Commit b69f05d

Browse files
committed
add fuzz test for SelectBuilder.Clone
1 parent 22d937c commit b69f05d

File tree

1 file changed

+114
-0
lines changed

1 file changed

+114
-0
lines changed

select_fuzz_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math/rand"
66
"reflect"
77
"slices"
8+
"sync"
89
"testing"
910
)
1011

@@ -382,3 +383,116 @@ func generateArgumentForType(argType reflect.Type, data []byte) reflect.Value {
382383
return reflect.Zero(argType)
383384
}
384385
}
386+
387+
// FuzzSelectClone fuzzes SelectBuilder.Clone behavior under concurrent usage
388+
// and ensures cloned instances are independent and safe to mutate.
389+
func FuzzSelectClone(f *testing.F) {
390+
f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) {
391+
if len(data) == 0 {
392+
return
393+
}
394+
395+
methodList, methodNames := getSelectBuilderMethods()
396+
397+
r := rand.New(rand.NewSource(seed))
398+
r.Shuffle(len(methodNames), func(i, j int) {
399+
methodNames[i], methodNames[j] = methodNames[j], methodNames[i]
400+
})
401+
402+
// Build a base template SelectBuilder via fuzzed method chains.
403+
base := NewSelectBuilder()
404+
baseState := &fuzzState{
405+
data: data,
406+
dataIndex: 0,
407+
callchainRepresentation: "NewSelectBuilder()",
408+
currentBuilder: reflect.ValueOf(base),
409+
usedMethods: make(map[string]bool),
410+
}
411+
412+
maxChains := numberOfChainedFunction
413+
if maxChains > 10 {
414+
maxChains = 10
415+
}
416+
executeMethodChain(methodList, methodNames, baseState, maxChains, t)
417+
418+
baseSQLBefore, baseArgsBefore := base.Build()
419+
420+
// Clone concurrently and mutate clones with fuzzed chains.
421+
cloneCount := int(r.Uint32()%4) + 1 // 1..4 clones
422+
var wg sync.WaitGroup
423+
wg.Add(cloneCount)
424+
start := make(chan struct{})
425+
426+
type result struct {
427+
sql string
428+
args []interface{}
429+
}
430+
results := make(chan result, cloneCount)
431+
432+
for i := 0; i < cloneCount; i++ {
433+
// Use different offsets into the same fuzz data for variety.
434+
offset := 0
435+
if len(data) > 0 {
436+
offset = (i * 17) % len(data)
437+
}
438+
go func(off int) {
439+
defer wg.Done()
440+
<-start // start all goroutines roughly at the same time
441+
442+
c := base.Clone()
443+
st := &fuzzState{
444+
data: data,
445+
dataIndex: off,
446+
callchainRepresentation: "Clone()",
447+
currentBuilder: reflect.ValueOf(c),
448+
usedMethods: make(map[string]bool),
449+
}
450+
executeMethodChain(methodList, methodNames, st, maxChains, t)
451+
finalizeBuild(st) // ensure no panic on Build
452+
s, a := c.Build()
453+
results <- result{sql: s, args: a}
454+
}(offset)
455+
}
456+
457+
close(start)
458+
wg.Wait()
459+
close(results)
460+
461+
// Ensure base builder stays unchanged after concurrent cloning/mutation of clones.
462+
baseSQLAfter, baseArgsAfter := base.Build()
463+
if baseSQLBefore != baseSQLAfter || !reflect.DeepEqual(baseArgsBefore, baseArgsAfter) {
464+
t.Fatalf("base builder mutated by clones:\n before: %s %v\n after: %s %v", baseSQLBefore, baseArgsBefore, baseSQLAfter, baseArgsAfter)
465+
}
466+
467+
// Independence check: mutating one clone does not affect another clone.
468+
cloneA := base.Clone()
469+
sA1, aA1 := cloneA.Build()
470+
471+
done := make(chan struct{})
472+
go func() {
473+
defer close(done)
474+
c2 := base.Clone()
475+
// Apply a deterministic small change; should not affect cloneA.
476+
c2.OrderBy("id").Desc().Limit(1).Offset(0)
477+
_, _ = c2.Build()
478+
}()
479+
480+
sA2, aA2 := cloneA.Build()
481+
if sA1 != sA2 || !reflect.DeepEqual(aA1, aA2) {
482+
t.Fatalf("cloneA changed after mutating another clone")
483+
}
484+
<-done
485+
486+
// Further independence: modifying cloneA should not affect the base.
487+
cloneA.Limit(3).Asc()
488+
_ = cloneA.String()
489+
baseSQLFinal, baseArgsFinal := base.Build()
490+
if baseSQLFinal != baseSQLAfter || !reflect.DeepEqual(baseArgsFinal, baseArgsAfter) {
491+
t.Fatalf("base changed after modifying a clone")
492+
}
493+
494+
// Drain results to ensure all builds completed; mainly to use the values and avoid lints.
495+
for range results {
496+
}
497+
})
498+
}

0 commit comments

Comments
 (0)