Skip to content
This repository has been archived by the owner on Jan 10, 2024. It is now read-only.

Commit

Permalink
Add customizable string generators
Browse files Browse the repository at this point in the history
Add UnicodeRange and UnicodeRanges types, to generate strings with a more controllable set of characters. Use is optional.
  • Loading branch information
kwongtailau committed Jul 14, 2020
1 parent 35f2754 commit 18d1906
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 11 deletions.
40 changes: 40 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"math/rand"
"strings"

"github.com/google/gofuzz"
)
Expand Down Expand Up @@ -223,3 +224,42 @@ func ExampleEnum() {
}
// Output:
}

func ExampleCustomString() {
a2z := "abcdefghijklmnopqrstuvwxyz"
a2z0to9 := "abcdefghijklmnopqrstuvwxyz0123456789"

// example for generating custom string within one unicode range.
var A string
unicodeRange := fuzz.UnicodeRange{'a', 'z'}

f := fuzz.New().Funcs(unicodeRange.CustomStringFuzzFunc())
f.Fuzz(&A)

for i := range A {
if !strings.ContainsRune(a2z, rune(A[i])) {
fmt.Printf("A[%d]: %v is not in range of a-z.\n", i, A[i])
}
}
fmt.Println("Got a string, each character is selected from a-z.")

// example for generating custom string within multiple unicode range.
var B string
unicodeRanges := fuzz.UnicodeRanges{
{'a', 'z'},
{'0', '9'}, // You can also use 0x0030 as 0, 0x0039 as 9.
}
ff := fuzz.New().Funcs(unicodeRanges.CustomStringFuzzFunc())
ff.Fuzz(&B)

for i := range B {
if !strings.ContainsRune(a2z0to9, rune(B[i])) {
fmt.Printf("A[%d]: %v is not in range list [ a-z, 0-9 ].\n", i, string(B[i]))
}
}
fmt.Println("Got a string, each character is selected from a-z, 0-9.")

// Output:
// Got a string, each character is selected from a-z.
// Got a string, each character is selected from a-z, 0-9.
}
86 changes: 76 additions & 10 deletions fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ func (fc *fuzzerContext) doFuzz(v reflect.Value, flags uint64) {
fn(v, fc.fuzzer.r)
return
}

switch v.Kind() {
case reflect.Map:
if fc.fuzzer.genShouldFill() {
Expand Down Expand Up @@ -503,35 +504,100 @@ type int63nPicker interface {
Int63n(int64) int64
}

type charRange struct {
first, last rune
// UnicodeRange describes a sequential range of unicode characters.
// Last must be numerically greater than First.
type UnicodeRange struct {
First, Last rune
}

// UnicodeRanges describes an arbitrary number of sequential ranges of unicode characters.
// To be useful, each range must have at least one character (First <= Last) and
// there must be at least one range.
type UnicodeRanges []UnicodeRange

// choose returns a random unicode character from the given range, using the
// given randomness source.
func (cr charRange) choose(r int63nPicker) rune {
count := int64(cr.last - cr.first + 1)
return cr.first + rune(r.Int63n(count))
func (ur UnicodeRange) choose(r int63nPicker) rune {
count := int64(ur.Last - ur.First + 1)
return ur.First + rune(r.Int63n(count))
}

// CustomStringFuzzFunc constructs a FuzzFunc which produces random strings.
// Each character is selected from the range ur. If there are no characters
// in the range (cr.Last < cr.First), this will panic.
func (ur UnicodeRange) CustomStringFuzzFunc() func(s *string, c Continue) {
ur.check()
return func(s *string, c Continue) {
*s = ur.randString(c.Rand)
}
}

// check is a function that used to check whether the first of ur(UnicodeRange)
// is greater than the last one.
func (ur UnicodeRange) check() {
if ur.Last < ur.First {
panic("The last encoding must be greater than the first one.")
}
}

// randString of UnicodeRange makes a random string up to 20 characters long.
// Each character is selected form ur(UnicodeRange).
func (ur UnicodeRange) randString(r *rand.Rand) string {
n := r.Intn(20)
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteRune(ur.choose(r))
}
return sb.String()
}

var unicodeRanges = []charRange{
// defaultUnicodeRanges sets a default unicode range when user do not set
// CustomStringFuzzFunc() but wants fuzz string.
var defaultUnicodeRanges = UnicodeRanges{
{' ', '~'}, // ASCII characters
{'\u00a0', '\u02af'}, // Multi-byte encoded characters
{'\u4e00', '\u9fff'}, // Common CJK (even longer encodings)
}

// randString makes a random string up to 20 characters long. The returned string
// may include a variety of (valid) UTF-8 encodings.
func randString(r *rand.Rand) string {
// CustomStringFuzzFunc constructs a FuzzFunc which produces random strings.
// Each character is selected from one of the ranges of ur(UnicodeRanges).
// Each range has an equal probability of being chosen. If there are no ranges,
// or a selected range has no characters (.Last < .First), this will panic.
// Do not modify any of the ranges in ur after calling this function.
func (ur UnicodeRanges) CustomStringFuzzFunc() func(s *string, c Continue) {
// Check unicode ranges slice is empty.
if len(ur) == 0 {
panic("UnicodeRanges is empty.")
}
// if not empty, each range should be checked.
for i := range ur {
ur[i].check()
}
return func(s *string, c Continue) {
*s = ur.randString(c.Rand)
}
}

// randString of UnicodeRanges makes a random string up to 20 characters long.
// Each character is selected form one of the ranges of ur(UnicodeRanges),
// and each range has an equal probability of being chosen.
func (ur UnicodeRanges) randString(r *rand.Rand) string {
n := r.Intn(20)
sb := strings.Builder{}
sb.Grow(n)
for i := 0; i < n; i++ {
sb.WriteRune(unicodeRanges[r.Intn(len(unicodeRanges))].choose(r))
sb.WriteRune(ur[r.Intn(len(ur))].choose(r))
}
return sb.String()
}

// randString makes a random string up to 20 characters long. The returned string
// may include a variety of (valid) UTF-8 encodings.
func randString(r *rand.Rand) string {
return defaultUnicodeRanges.randString(r)
}

// randUint64 makes random 64 bit numbers.
// Weirdly, rand doesn't have a function that gives you 64 random bits.
func randUint64(r *rand.Rand) uint64 {
Expand Down
69 changes: 68 additions & 1 deletion fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"math/rand"
"reflect"
"regexp"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -538,7 +539,7 @@ func (c customInt63) Int63n(n int64) int64 {
}

func Test_charRange_choose(t *testing.T) {
lowercaseLetters := charRange{'a', 'z'}
lowercaseLetters := UnicodeRange{'a', 'z'}

t.Run("Picks first", func(t *testing.T) {
r := customInt63{mode: modeFirst}
Expand All @@ -557,6 +558,49 @@ func Test_charRange_choose(t *testing.T) {
})
}

func Test_UnicodeRange_CustomStringFuzzFunc(t *testing.T) {
a2z := "abcdefghijklmnopqrstuvwxyz"

unicodeRange := UnicodeRange{'a', 'z'}
f := New().Funcs(unicodeRange.CustomStringFuzzFunc())
var myString string
f.Fuzz(&myString)

t.Run("Picks a-z string", func(t *testing.T) {
for i := range myString {
if !strings.ContainsRune(a2z, rune(myString[i])) {
t.Errorf("Expected a-z, got %v", string(myString[i]))
}
}
})
}

func Test_UnicodeRange_Check(t *testing.T) {
unicodeRange := UnicodeRange{'a', 'z'}

unicodeRange.check()
}

func Test_UnicodeRanges_CustomStringFuzzFunc(t *testing.T) {
a2z0to9 := "abcdefghijklmnopqrstuvwxyz0123456789"

unicodeRanges := UnicodeRanges{
{'a', 'z'},
{'0', '9'},
}
f := New().Funcs(unicodeRanges.CustomStringFuzzFunc())
var myString string
f.Fuzz(&myString)

t.Run("Picks a-z0-9 string", func(t *testing.T) {
for i := range myString {
if !strings.ContainsRune(a2z0to9, rune(myString[i])) {
t.Errorf("Expected a-z0-9, got %v", string(myString[i]))
}
}
})
}

func TestNewFromGoFuzz(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -585,3 +629,26 @@ func BenchmarkRandString(b *testing.B) {
randString(rs)
}
}

func BenchmarkUnicodeRangeRandString(b *testing.B) {
unicodeRange := UnicodeRange{'a', 'z'}

rs := rand.New(rand.NewSource(123))

for i := 0; i < b.N; i++ {
unicodeRange.randString(rs)
}
}

func BenchmarkUnicodeRangesRandString(b *testing.B) {
unicodeRanges := UnicodeRanges{
{'a', 'z'},
{'0', '9'},
}

rs := rand.New(rand.NewSource(123))

for i := 0; i < b.N; i++ {
unicodeRanges.randString(rs)
}
}

0 comments on commit 18d1906

Please sign in to comment.