|
5 | 5 | "math/rand" |
6 | 6 | "reflect" |
7 | 7 | "slices" |
| 8 | + "sync" |
8 | 9 | "testing" |
9 | 10 | ) |
10 | 11 |
|
@@ -382,3 +383,116 @@ func generateArgumentForType(argType reflect.Type, data []byte) reflect.Value { |
382 | 383 | return reflect.Zero(argType) |
383 | 384 | } |
384 | 385 | } |
| 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