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  }