[!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")
		}
	})
}
