github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/stdlibs/testing/fuzz.gno (about) 1 package testing 2 3 import ( 4 "math" 5 "strings" 6 "time" 7 ) 8 9 type Fuzzer interface { 10 InsertDeleteMutate(p float64) Fuzzer 11 Mutate() Fuzzer 12 String() string 13 } 14 15 type StringFuzzer struct { 16 Value string 17 f *F 18 } 19 20 func NewStringFuzzer(value string) *StringFuzzer { 21 return &StringFuzzer{Value: value} 22 } 23 24 // Mutate changes a StringFuzzer's value by replacing a random character 25 // with a random ASCII character. 26 func (sf *StringFuzzer) Mutate() Fuzzer { 27 runes := []rune(sf.Value) 28 if len(runes) == 0 { 29 return sf 30 } 31 32 index := randRange(0, len(runes)-1) 33 runes[index] = randomASCIIChar() 34 35 return NewStringFuzzer(string(runes)) 36 } 37 38 func (sf *StringFuzzer) InsertDeleteMutate(p float64) Fuzzer { 39 value := InsertDelete(sf.Value, p) 40 return NewStringFuzzer(value) 41 } 42 43 func (sf *StringFuzzer) Fuzz() string { 44 if GenerateRandomBool(0.2) { 45 return InsertDelete(sf.Value, 0.1) 46 } 47 48 rs := []rune(sf.Value) 49 lrs := len(rs) 50 51 if lrs == 0 { 52 return sf.Value 53 } 54 55 index := randRange(0, lrs-1) 56 rs[index] = randomASCIIChar() 57 58 return string(rs) 59 } 60 61 func (sf *StringFuzzer) String() string { 62 return sf.Value 63 } 64 65 func randomASCIIChar() rune { 66 r := int(randRange(32, 126)) 67 68 return rune(r) 69 } 70 71 // Individual represents a single individual in the population. 72 type Individual struct { 73 Fuzzer Fuzzer 74 Fitness int 75 } 76 77 func NewIndividual(fuzzer Fuzzer) *Individual { 78 return &Individual{Fuzzer: fuzzer} 79 } 80 81 func (ind *Individual) calculateFitness() { 82 ind.Fitness = len(ind.Fuzzer.String()) 83 } 84 85 // Selection selects individuals from the population based on their fitness. 86 // 87 // Use roulette wheel selection to select individuals from the population. 88 // ref: https://en.wikipedia.org/wiki/Fitness_proportionate_selection 89 func Selection(population []*Individual) []*Individual { 90 totalFitness := calculateTotalFitness(population) 91 selected := make([]*Individual, len(population)) 92 93 for i := range selected { 94 selected[i] = selectIndividual(population, totalFitness) 95 } 96 97 return selected 98 } 99 100 func calculateTotalFitness(population []*Individual) int { 101 totalFitness := 0 102 103 for _, ind := range population { 104 totalFitness += ind.Fitness 105 } 106 107 return totalFitness 108 } 109 110 func selectIndividual(population []*Individual, totalFitness int) *Individual { 111 pick := randRange(0, totalFitness-1) 112 sum := 0 113 114 for _, ind := range population { 115 sum += ind.Fitness 116 if uint64(sum) > uint64(pick) { 117 return ind 118 } 119 } 120 121 return nil 122 } 123 124 // Crossover takes two parents and creates two children by combining their genetic material. 125 // 126 // The pivot point is chosen randomly from the length of the shortest parent. after the pivot point selected, 127 // the genetic material of the two parents is swapped to create the two children. 128 func Crossover(parent1, parent2 *Individual) (*Individual, *Individual) { 129 p1Runes := []rune(parent1.Fuzzer.String()) 130 p2Runes := []rune(parent2.Fuzzer.String()) 131 132 p1Len := len(p1Runes) 133 p2Len := len(p2Runes) 134 135 point := 0 136 if p1Len >= p2Len { 137 point = int(randRange(0, p2Len-1)) 138 } else { 139 point = int(randRange(0, p1Len-1)) 140 } 141 142 child1 := append(append([]rune{}, p1Runes[:point]...), p2Runes[point:]...) 143 child2 := append(append([]rune{}, p2Runes[:point]...), p1Runes[point:]...) 144 145 updatedIdv1 := NewIndividual(NewStringFuzzer(string(child1))) 146 updatedIdv2 := NewIndividual(NewStringFuzzer(string(child2))) 147 148 return updatedIdv1, updatedIdv2 149 } 150 151 func (ind *Individual) Mutate() { 152 ind.Fuzzer = ind.Fuzzer.Mutate() 153 } 154 155 // InsertDelete randomly inserts or deletes a character from a string. 156 func InsertDelete(s string, p float64) string { 157 rr := []rune(s) 158 l := len(rr) 159 160 // Insert 161 if GenerateRandomBool(p) { 162 pos := randRange(0, l-1) 163 rr = append(rr, 0) 164 165 copy(rr[pos+1:], rr[pos:]) 166 167 char := randomASCIIChar() 168 rr[pos] = char 169 } else { 170 if l == 0 { 171 return s 172 } 173 174 pos := randRange(0, l-1) 175 rr = append(rr[:pos], rr[pos+1:]...) 176 } 177 178 return string(rr) 179 } 180 181 type F struct { 182 corpus []string 183 failed bool // Indicates whether the fuzzing has encountered a failure. 184 msgs []string // Stores log messages for reporting. 185 iters int // Number of iterations to run the fuzzing process. TODO: CLI flag to set this. 186 } 187 188 // Runner is a type for the target function to fuzz. 189 type Runner func(*T, ...interface{}) 190 191 // Fuzz applies the fuzzing process to the target function. 192 func (f *F) Fuzz(run Runner, iter int) { 193 f.evolve(iter) 194 195 for _, input := range f.corpus { 196 args := make([]interface{}, len(f.corpus)) 197 for i := range args { 198 args[i] = input 199 } 200 201 run(nil, args...) 202 } 203 } 204 205 // Add adds test values to initialize the corpus. 206 func (f *F) Add(values ...interface{}) []Fuzzer { 207 fuzzers := make([]Fuzzer, len(values)) 208 209 for i, v := range values { 210 str, ok := v.(string) 211 if !ok { 212 continue 213 } 214 f.corpus = append(f.corpus, str) 215 fuzzers[i] = &StringFuzzer{Value: str} 216 } 217 218 return fuzzers 219 } 220 221 func (f *F) evolve(generations int) { 222 population := make([]*Individual, len(f.corpus)) 223 for i, c := range f.corpus { 224 population[i] = &Individual{Fuzzer: &StringFuzzer{Value: c, f: f}} 225 } 226 227 for _, ind := range population { 228 ind.calculateFitness() 229 } 230 231 for gen := 0; gen < generations; gen++ { 232 population = Selection(population) 233 newPopulation := make([]*Individual, 0, len(population)) 234 235 for i := 0; i < len(population); i += 2 { 236 if i+1 < len(population) { 237 child1, child2 := Crossover(population[i], population[i+1]) 238 newPopulation = append(newPopulation, child1, child2) 239 continue 240 } 241 242 newPopulation = append(newPopulation, population[i]) 243 } 244 245 var ( 246 bestFitness int 247 bestIndividual string 248 ) 249 250 for _, ind := range newPopulation { 251 if GenerateRandomBool(0.2) { 252 ind.Mutate() 253 } 254 255 if GenerateRandomBool(0.1) { 256 ind.Fuzzer = ind.Fuzzer.InsertDeleteMutate(0.3) 257 } 258 259 ind.calculateFitness() 260 261 if ind.Fitness > bestFitness { 262 bestFitness = ind.Fitness 263 bestIndividual = ind.Fuzzer.String() 264 } 265 } 266 267 population = newPopulation 268 } 269 270 f.corpus = make([]string, len(population)) 271 for i, ind := range population { 272 f.corpus[i] = ind.Fuzzer.String() 273 } 274 } 275 276 // Fail marks the function as having failed bur continue execution. 277 func (f *F) Fail() { 278 f.failed = true 279 } 280 281 // Fatal is equivalent to Log followed by FailNow. 282 // It logs the message and marks the fuzzing as failed. 283 func (f *F) Fatal(args ...interface{}) { 284 var sb strings.Builder 285 286 for _, arg := range args { 287 sb.WriteString(arg.(string)) 288 } 289 290 f.msgs = append(f.msgs, sb.String()) 291 f.Fail() 292 }