| [!fuzz] skip | |
| # Test basic fuzzing mutator behavior. | |
| # | |
| # fuzz_test.go has two fuzz targets (FuzzA, FuzzB) which both add a seed value. | |
| # Each fuzz function writes the input to a log file. The coordinator and worker | |
| # use separate log files. check_logs.go verifies that the coordinator only | |
| # tests seed values and the worker tests mutated values on the fuzz target. | |
| [short] skip | |
| env GOCACHE=$WORK/cache | |
| go test -fuzz=FuzzA -fuzztime=100x -parallel=1 -log=fuzz | |
| go run check_logs.go fuzz fuzz.worker | |
| # TODO(b/181800488): remove -parallel=1, here and below. For now, when a | |
| # crash is found, all workers keep running, wasting resources and reducing | |
| # the number of executions available to the minimizer, increasing flakiness. | |
| # Test that the mutator is good enough to find several unique mutations. | |
| ! go test -fuzz=FuzzMutator -parallel=1 -fuzztime=100x mutator_test.go | |
| ! stdout '^ok' | |
| stdout FAIL | |
| stdout 'mutator found enough unique mutations' | |
| -- go.mod -- | |
| module m | |
| go 1.16 | |
| -- fuzz_test.go -- | |
| package fuzz_test | |
| import ( | |
| "flag" | |
| "fmt" | |
| "os" | |
| "testing" | |
| ) | |
| var ( | |
| logPath = flag.String("log", "", "path to log file") | |
| logFile *os.File | |
| ) | |
| func TestMain(m *testing.M) { | |
| flag.Parse() | |
| var err error | |
| logFile, err = os.OpenFile(*logPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) | |
| if os.IsExist(err) { | |
| *logPath += ".worker" | |
| logFile, err = os.OpenFile(*logPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) | |
| } | |
| if err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| os.Exit(m.Run()) | |
| } | |
| func FuzzA(f *testing.F) { | |
| f.Add([]byte("seed")) | |
| f.Fuzz(func(t *testing.T, b []byte) { | |
| fmt.Fprintf(logFile, "FuzzA %q\n", b) | |
| }) | |
| } | |
| func FuzzB(f *testing.F) { | |
| f.Add([]byte("seed")) | |
| f.Fuzz(func(t *testing.T, b []byte) { | |
| fmt.Fprintf(logFile, "FuzzB %q\n", b) | |
| }) | |
| } | |
| -- check_logs.go -- | |
| // +build ignore | |
| package main | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "os" | |
| "strings" | |
| ) | |
| func main() { | |
| coordPath, workerPath := os.Args[1], os.Args[2] | |
| coordLog, err := os.Open(coordPath) | |
| if err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| defer coordLog.Close() | |
| if err := checkCoordLog(coordLog); err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| workerLog, err := os.Open(workerPath) | |
| if err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| defer workerLog.Close() | |
| if err := checkWorkerLog(workerLog); err != nil { | |
| fmt.Fprintln(os.Stderr, err) | |
| os.Exit(1) | |
| } | |
| } | |
| func checkCoordLog(r io.Reader) error { | |
| b, err := io.ReadAll(r) | |
| if err != nil { | |
| return err | |
| } | |
| if string(bytes.TrimSpace(b)) != `FuzzB "seed"` { | |
| return fmt.Errorf("coordinator: did not test FuzzB seed") | |
| } | |
| return nil | |
| } | |
| func checkWorkerLog(r io.Reader) error { | |
| scan := bufio.NewScanner(r) | |
| var sawAMutant bool | |
| for scan.Scan() { | |
| line := scan.Text() | |
| if !strings.HasPrefix(line, "FuzzA ") { | |
| return fmt.Errorf("worker: tested something other than target: %s", line) | |
| } | |
| if strings.TrimPrefix(line, "FuzzA ") != `"seed"` { | |
| sawAMutant = true | |
| } | |
| } | |
| if err := scan.Err(); err != nil && err != bufio.ErrTooLong { | |
| return err | |
| } | |
| if !sawAMutant { | |
| return fmt.Errorf("worker: did not test any mutants") | |
| } | |
| return nil | |
| } | |
| -- mutator_test.go -- | |
| package fuzz_test | |
| import ( | |
| "testing" | |
| ) | |
| // TODO(katiehockman): re-work this test once we have a better fuzzing engine | |
| // (ie. more mutations, and compiler instrumentation) | |
| func FuzzMutator(f *testing.F) { | |
| // TODO(katiehockman): simplify this once we can dedupe crashes (e.g. | |
| // replace map with calls to panic, and simply count the number of crashes | |
| // that were added to testdata) | |
| crashes := make(map[string]bool) | |
| // No seed corpus initiated | |
| f.Fuzz(func(t *testing.T, b []byte) { | |
| crashes[string(b)] = true | |
| if len(crashes) >= 10 { | |
| panic("mutator found enough unique mutations") | |
| } | |
| }) | |
| } | |