Files
sqlbuilder/select_fuzz_test.go

499 lines
15 KiB
Go

package sqlbuilder
import (
"fmt"
"math/rand"
"reflect"
"slices"
"sync"
"testing"
)
type fuzzState struct {
data []byte
dataIndex int
callchainRepresentation string
currentBuilder reflect.Value
usedMethods map[string]bool
}
func (fs *fuzzState) consumeData(size int) []byte {
if len(fs.data) <= fs.dataIndex+size {
return []byte{}
}
result := make([]byte, size)
copy(result, fs.data[fs.dataIndex:fs.dataIndex+size])
fs.dataIndex += size
return result
}
func (fs *fuzzState) updateCallchain(method string, args []reflect.Value) {
fs.callchainRepresentation += "." + method + "("
for i, arg := range args {
if i > 0 {
fs.callchainRepresentation += ", "
}
fs.callchainRepresentation += fmt.Sprintf("%q", arg)
}
fs.callchainRepresentation += ")"
}
func getSelectBuilderMethods() (map[string]reflect.Type, []string) {
sb := NewSelectBuilder()
sbType := reflect.TypeOf(sb)
// Skip methods that are likely to cause issues or don't return builders
skipMethods := []string{
"Build", "String", "BuildWithFlavor", "Flavor",
"NumCol", "NumValue", "NumAssignment", "TableNames", "Var",
}
methodList := make(map[string]reflect.Type)
methodNames := make([]string, 0, sbType.NumMethod())
for i := 0; i < sbType.NumMethod(); i++ {
method := sbType.Method(i)
if slices.Contains(skipMethods, method.Name) {
continue
}
methodList[method.Name] = method.Type
methodNames = append(methodNames, method.Name)
}
return methodList, methodNames
}
func generateMethodArgs(methodType reflect.Type, state *fuzzState) ([]reflect.Value, bool) {
numArgs := methodType.NumIn() - 1 // Skip receiver
isVariadic := methodType.IsVariadic()
if isVariadic {
return generateVariadicArgs(methodType, numArgs, state)
}
return generateFixedArgs(methodType, numArgs, state)
}
func generateFixedArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) {
args := make([]reflect.Value, numArgs)
for i := 0; i < numArgs; i++ {
argType := methodType.In(i + 1) // Skip receiver
argData := state.consumeData(16)
args[i] = generateArgumentForType(argType, argData)
if !args[i].IsValid() {
return nil, false
}
// Additional type compatibility check for complex types
if argType.Kind() == reflect.Ptr && args[i].Kind() == reflect.Ptr {
if argType != args[i].Type() {
return nil, false
}
}
}
return args, true
}
func generateVariadicArgs(methodType reflect.Type, numArgs int, state *fuzzState) ([]reflect.Value, bool) {
numFixedArgs := numArgs - 1 // Last parameter is the variadic slice
// Generate fixed arguments first
args := make([]reflect.Value, numFixedArgs)
for i := 0; i < numFixedArgs; i++ {
argType := methodType.In(i + 1) // Skip receiver
argData := state.consumeData(16)
args[i] = generateArgumentForType(argType, argData)
if !args[i].IsValid() {
return nil, false
}
}
// Generate variadic arguments (0-3 arguments to keep it reasonable)
if numFixedArgs < numArgs {
variadicType := methodType.In(numArgs).Elem() // Get the element type of the slice
numVariadicArgs := 0
if len(state.data) > state.dataIndex {
// 0-3 variadic args, keep the number small while still exercising multiple values
// TODO: Possible fuzz improvement to allow for more variadic args. Not sure it's worth it.
numVariadicArgs = int(state.data[state.dataIndex] % 4)
state.dataIndex++
}
for j := 0; j < numVariadicArgs; j++ {
argData := state.consumeData(16)
varArg := generateArgumentForType(variadicType, argData)
if !varArg.IsValid() {
return nil, false
}
args = append(args, varArg)
}
}
return args, true
}
func tryCallMethod(methodName string, methodType reflect.Type, state *fuzzState, t *testing.T) bool {
// Check if method exists on current builder
callableMethod := state.currentBuilder.MethodByName(methodName)
if !callableMethod.IsValid() {
return false
}
// Generate arguments
args, canCall := generateMethodArgs(methodType, state)
if !canCall {
return false
}
// Update call chain representation and log it
state.updateCallchain(methodName, args)
t.Log("callchain:", state.callchainRepresentation)
// Mark this method as used
state.usedMethods[methodName] = true
// Call method and capture result for chaining
result := callableMethod.Call(args)
// Only chain if method returns the same builder type (SelectBuilder)
if len(result) > 0 && result[0].IsValid() {
resultType := result[0].Type()
if resultType.Kind() == reflect.Ptr &&
resultType.String() == "*sqlbuilder.SelectBuilder" &&
!result[0].IsNil() {
state.currentBuilder = result[0]
}
}
return true
}
func executeMethodChain(methodList map[string]reflect.Type, methodNames []string, state *fuzzState, maxChains uint8, t *testing.T) {
for nbFunc := uint8(0); nbFunc < maxChains; nbFunc++ {
methodCalled := false
// Try to find a method we haven't used yet to create more varied chains
for _, method := range methodNames {
// Skip methods we've already used to create more variety
if state.usedMethods[method] && len(state.usedMethods) < len(methodNames) {
continue
}
if tryCallMethod(method, methodList[method], state, t) {
methodCalled = true
break // Move to next chain iteration
}
}
// If no method could be called, break the chain
if !methodCalled {
break
}
}
}
func finalizeBuild(state *fuzzState) {
// Always try to build the final SQL to ensure it doesn't panic
if state.currentBuilder.IsValid() {
buildMethod := state.currentBuilder.MethodByName("Build")
if buildMethod.IsValid() {
buildMethod.Call([]reflect.Value{})
}
}
}
func FuzzSelect(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) {
if len(data) == 0 {
return
}
// Get all available methods for SelectBuilder
methodList, methodNames := getSelectBuilderMethods()
// Randomize method order deterministically based on seed
r := rand.New(rand.NewSource(seed))
r.Shuffle(len(methodNames), func(i, j int) {
methodNames[i], methodNames[j] = methodNames[j], methodNames[i]
})
// Initialize fuzzing state
state := &fuzzState{
data: data,
dataIndex: 0,
callchainRepresentation: "NewSelectBuilder()",
currentBuilder: reflect.ValueOf(NewSelectBuilder()),
usedMethods: make(map[string]bool),
}
// Limit the number of chained functions to prevent infinite loops
maxChains := numberOfChainedFunction
if maxChains > 10 {
maxChains = 10
}
// Execute method chain
executeMethodChain(methodList, methodNames, state, maxChains, t)
t.Logf("Final callchain: %s", state.callchainRepresentation)
// Try to build the final result
finalizeBuild(state)
})
}
// generateArgumentForType generates a reflect.Value for the given type based on the provided data.
// It will consume the data slice to create a value of the specified type.
// It handles specific custom types like JoinOption and Flavor, and Go will consider them disntinct types than their aliases.
func generateArgumentForType(argType reflect.Type, data []byte) reflect.Value {
switch argType.Kind() {
case reflect.String:
// Handle specific custom string types first
if argType.String() == "sqlbuilder.JoinOption" {
joinOptions := []JoinOption{
FullJoin, FullOuterJoin, InnerJoin,
LeftJoin, LeftOuterJoin, RightJoin, RightOuterJoin,
}
if len(data) > 0 {
return reflect.ValueOf(joinOptions[int(data[0])%len(joinOptions)])
}
return reflect.ValueOf(InnerJoin)
}
// Use remaining data as string
return reflect.ValueOf(string(data))
case reflect.Int:
// Handle specific custom int types first
if argType.String() == "sqlbuilder.Flavor" {
return reflect.ValueOf(DefaultFlavor)
}
if len(data) > 0 {
return reflect.ValueOf(int(data[0]))
}
return reflect.ValueOf(0)
case reflect.Bool:
if len(data) > 0 {
return reflect.ValueOf(data[0]%2 == 0)
}
return reflect.ValueOf(false)
case reflect.Int8:
if len(data) > 0 {
return reflect.ValueOf(int8(data[0]))
}
return reflect.ValueOf(int8(0))
case reflect.Int16:
if len(data) >= 2 {
return reflect.ValueOf(int16(data[0])<<8 | int16(data[1]))
}
return reflect.ValueOf(int16(0))
case reflect.Int32:
if len(data) >= 4 {
return reflect.ValueOf(int32(data[0])<<24 | int32(data[1])<<16 | int32(data[2])<<8 | int32(data[3]))
}
return reflect.ValueOf(int32(0))
case reflect.Int64:
if len(data) >= 8 {
val := int64(data[0])<<56 | int64(data[1])<<48 | int64(data[2])<<40 | int64(data[3])<<32 |
int64(data[4])<<24 | int64(data[5])<<16 | int64(data[6])<<8 | int64(data[7])
return reflect.ValueOf(val)
}
return reflect.ValueOf(int64(0))
case reflect.Uint:
if len(data) > 0 {
return reflect.ValueOf(uint(data[0]))
}
return reflect.ValueOf(uint(0))
case reflect.Uint8:
if len(data) > 0 {
return reflect.ValueOf(uint8(data[0]))
}
return reflect.ValueOf(uint8(0))
case reflect.Uint16:
if len(data) >= 2 {
return reflect.ValueOf(uint16(data[0])<<8 | uint16(data[1]))
}
return reflect.ValueOf(uint16(0))
case reflect.Uint32:
if len(data) >= 4 {
return reflect.ValueOf(uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]))
}
return reflect.ValueOf(uint32(0))
case reflect.Uint64:
if len(data) >= 8 {
val := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 |
uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7])
return reflect.ValueOf(val)
}
return reflect.ValueOf(uint64(0))
case reflect.Float32:
if len(data) >= 4 {
bits := uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3])
return reflect.ValueOf(float32(bits))
}
return reflect.ValueOf(float32(0))
case reflect.Float64:
if len(data) >= 8 {
bits := uint64(data[0])<<56 | uint64(data[1])<<48 | uint64(data[2])<<40 | uint64(data[3])<<32 |
uint64(data[4])<<24 | uint64(data[5])<<16 | uint64(data[6])<<8 | uint64(data[7])
return reflect.ValueOf(float64(bits))
}
return reflect.ValueOf(float64(0))
case reflect.Slice:
if argType.Elem().Kind() == reflect.String {
return reflect.ValueOf([]string{string(data)})
}
if argType.Elem().Kind() == reflect.Interface {
return reflect.ValueOf([]interface{}{string(data)})
}
return reflect.ValueOf([]interface{}{string(data)})
case reflect.Ptr:
// Handle pointer types by creating a pointer to the underlying type
// Handle specific pointer types
if argType == reflect.TypeOf((*WhereClause)(nil)) {
return reflect.ValueOf(NewWhereClause())
}
if argType == reflect.TypeOf((*SelectBuilder)(nil)) {
return reflect.ValueOf(NewSelectBuilder())
}
if argType == reflect.TypeOf((*Args)(nil)) {
return reflect.ValueOf(&Args{})
}
if argType == reflect.TypeOf((*CTEBuilder)(nil)) {
return reflect.ValueOf(DefaultFlavor.NewCTEBuilder())
}
if argType == reflect.TypeOf((*InsertBuilder)(nil)) {
return reflect.ValueOf(DefaultFlavor.NewInsertBuilder())
}
if argType == reflect.TypeOf((*UpdateBuilder)(nil)) {
return reflect.ValueOf(DefaultFlavor.NewUpdateBuilder())
}
if argType == reflect.TypeOf((*DeleteBuilder)(nil)) {
return reflect.ValueOf(DefaultFlavor.NewDeleteBuilder())
}
// For other pointer types, create a pointer to the underlying type
str := string(data)
return reflect.ValueOf(&str)
case reflect.Interface:
// Handle specific interface types
if argType.String() == "sqlbuilder.Builder" {
// Create a simple SelectBuilder for Builder interface
return reflect.ValueOf(NewSelectBuilder())
}
return reflect.ValueOf(string(data))
default:
// For other types, use zero value
return reflect.Zero(argType)
}
}
// FuzzSelectClone fuzzes SelectBuilder.Clone behavior under concurrent usage
// and ensures cloned instances are independent and safe to mutate.
func FuzzSelectClone(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte, seed int64, numberOfChainedFunction uint8) {
if len(data) == 0 {
return
}
methodList, methodNames := getSelectBuilderMethods()
r := rand.New(rand.NewSource(seed))
r.Shuffle(len(methodNames), func(i, j int) {
methodNames[i], methodNames[j] = methodNames[j], methodNames[i]
})
// Build a base template SelectBuilder via fuzzed method chains.
base := NewSelectBuilder()
baseState := &fuzzState{
data: data,
dataIndex: 0,
callchainRepresentation: "NewSelectBuilder()",
currentBuilder: reflect.ValueOf(base),
usedMethods: make(map[string]bool),
}
maxChains := numberOfChainedFunction
if maxChains > 10 {
maxChains = 10
}
executeMethodChain(methodList, methodNames, baseState, maxChains, t)
baseSQLBefore, baseArgsBefore := base.Build()
// Clone concurrently and mutate clones with fuzzed chains.
cloneCount := int(r.Uint32()%4) + 1 // 1..4 clones
var wg sync.WaitGroup
wg.Add(cloneCount)
start := make(chan struct{})
type result struct {
sql string
args []interface{}
}
results := make(chan result, cloneCount)
for i := 0; i < cloneCount; i++ {
// Use different offsets into the same fuzz data for variety.
offset := 0
if len(data) > 0 {
offset = (i * 17) % len(data)
}
go func(off int) {
defer wg.Done()
<-start // start all goroutines roughly at the same time
c := base.Clone()
st := &fuzzState{
data: data,
dataIndex: off,
callchainRepresentation: "Clone()",
currentBuilder: reflect.ValueOf(c),
usedMethods: make(map[string]bool),
}
executeMethodChain(methodList, methodNames, st, maxChains, t)
finalizeBuild(st) // ensure no panic on Build
s, a := c.Build()
results <- result{sql: s, args: a}
}(offset)
}
close(start)
wg.Wait()
close(results)
// Ensure base builder stays unchanged after concurrent cloning/mutation of clones.
baseSQLAfter, baseArgsAfter := base.Build()
if baseSQLBefore != baseSQLAfter || !reflect.DeepEqual(baseArgsBefore, baseArgsAfter) {
t.Fatalf("base builder mutated by clones:\n before: %s %v\n after: %s %v", baseSQLBefore, baseArgsBefore, baseSQLAfter, baseArgsAfter)
}
// Independence check: mutating one clone does not affect another clone.
cloneA := base.Clone()
sA1, aA1 := cloneA.Build()
done := make(chan struct{})
go func() {
defer close(done)
c2 := base.Clone()
// Apply a deterministic small change; should not affect cloneA.
c2.OrderBy("id").Desc().Limit(1).Offset(0)
_, _ = c2.Build()
}()
sA2, aA2 := cloneA.Build()
if sA1 != sA2 || !reflect.DeepEqual(aA1, aA2) {
t.Fatalf("cloneA changed after mutating another clone")
}
<-done
// Further independence: modifying cloneA should not affect the base.
cloneA.Limit(3).Asc()
_ = cloneA.String()
baseSQLFinal, baseArgsFinal := base.Build()
if baseSQLFinal != baseSQLAfter || !reflect.DeepEqual(baseArgsFinal, baseArgsAfter) {
t.Fatalf("base changed after modifying a clone")
}
// Drain results to ensure all builds completed; mainly to use the values and avoid lints.
for range results {
}
})
}