| | |
| | |
| | |
| |
|
| | package testing |
| |
|
| | import ( |
| | "context" |
| | "errors" |
| | "flag" |
| | "fmt" |
| | "io" |
| | "os" |
| | "path/filepath" |
| | "reflect" |
| | "runtime" |
| | "strings" |
| | "time" |
| | ) |
| |
|
| | func initFuzzFlags() { |
| | matchFuzz = flag.String("test.fuzz", "", "run the fuzz test matching `regexp`") |
| | flag.Var(&fuzzDuration, "test.fuzztime", "time to spend fuzzing; default is to run indefinitely") |
| | flag.Var(&minimizeDuration, "test.fuzzminimizetime", "time to spend minimizing a value after finding a failing input") |
| |
|
| | fuzzCacheDir = flag.String("test.fuzzcachedir", "", "directory where interesting fuzzing inputs are stored (for use only by cmd/go)") |
| | isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values (for use only by cmd/go)") |
| | } |
| |
|
| | var ( |
| | matchFuzz *string |
| | fuzzDuration durationOrCountFlag |
| | minimizeDuration = durationOrCountFlag{d: 60 * time.Second, allowZero: true} |
| | fuzzCacheDir *string |
| | isFuzzWorker *bool |
| |
|
| | |
| | |
| | corpusDir = "testdata/fuzz" |
| | ) |
| |
|
| | |
| | |
| | |
| | const fuzzWorkerExitCode = 70 |
| |
|
| | |
| | |
| | type InternalFuzzTarget struct { |
| | Name string |
| | Fn func(f *F) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | type F struct { |
| | common |
| | fstate *fuzzState |
| | tstate *testState |
| |
|
| | |
| | |
| | inFuzzFn bool |
| |
|
| | |
| | |
| | corpus []corpusEntry |
| |
|
| | result fuzzResult |
| | fuzzCalled bool |
| | } |
| |
|
| | var _ TB = (*F)(nil) |
| |
|
| | |
| | |
| | |
| | type corpusEntry = struct { |
| | Parent string |
| | Path string |
| | Data []byte |
| | Values []any |
| | Generation int |
| | IsSeed bool |
| | } |
| |
|
| | |
| | |
| | |
| | func (f *F) Helper() { |
| | if f.inFuzzFn { |
| | panic("testing: f.Helper was called inside the fuzz target, use t.Helper instead") |
| | } |
| |
|
| | |
| | |
| | |
| | f.mu.Lock() |
| | defer f.mu.Unlock() |
| | if f.helperPCs == nil { |
| | f.helperPCs = make(map[uintptr]struct{}) |
| | } |
| | |
| | var pc [1]uintptr |
| | n := runtime.Callers(2, pc[:]) |
| | if n == 0 { |
| | panic("testing: zero callers found") |
| | } |
| | if _, found := f.helperPCs[pc[0]]; !found { |
| | f.helperPCs[pc[0]] = struct{}{} |
| | f.helperNames = nil |
| | } |
| | } |
| |
|
| | |
| | func (f *F) Fail() { |
| | |
| | |
| | if f.inFuzzFn { |
| | panic("testing: f.Fail was called inside the fuzz target, use t.Fail instead") |
| | } |
| | f.common.Helper() |
| | f.common.Fail() |
| | } |
| |
|
| | |
| | func (f *F) Skipped() bool { |
| | |
| | |
| | if f.inFuzzFn { |
| | panic("testing: f.Skipped was called inside the fuzz target, use t.Skipped instead") |
| | } |
| | f.common.Helper() |
| | return f.common.Skipped() |
| | } |
| |
|
| | |
| | |
| | |
| | func (f *F) Add(args ...any) { |
| | var values []any |
| | for i := range args { |
| | if t := reflect.TypeOf(args[i]); !supportedTypes[t] { |
| | panic(fmt.Sprintf("testing: unsupported type to Add %v", t)) |
| | } |
| | values = append(values, args[i]) |
| | } |
| | f.corpus = append(f.corpus, corpusEntry{Values: values, IsSeed: true, Path: fmt.Sprintf("seed#%d", len(f.corpus))}) |
| | } |
| |
|
| | |
| | var supportedTypes = map[reflect.Type]bool{ |
| | reflect.TypeFor[[]byte](): true, |
| | reflect.TypeFor[string](): true, |
| | reflect.TypeFor[bool](): true, |
| | reflect.TypeFor[byte](): true, |
| | reflect.TypeFor[rune](): true, |
| | reflect.TypeFor[float32](): true, |
| | reflect.TypeFor[float64](): true, |
| | reflect.TypeFor[int](): true, |
| | reflect.TypeFor[int8](): true, |
| | reflect.TypeFor[int16](): true, |
| | reflect.TypeFor[int32](): true, |
| | reflect.TypeFor[int64](): true, |
| | reflect.TypeFor[uint](): true, |
| | reflect.TypeFor[uint8](): true, |
| | reflect.TypeFor[uint16](): true, |
| | reflect.TypeFor[uint32](): true, |
| | reflect.TypeFor[uint64](): true, |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func (f *F) Fuzz(ff any) { |
| | if f.fuzzCalled { |
| | panic("testing: F.Fuzz called more than once") |
| | } |
| | f.fuzzCalled = true |
| | if f.failed { |
| | return |
| | } |
| | f.Helper() |
| |
|
| | |
| | fn := reflect.ValueOf(ff) |
| | fnType := fn.Type() |
| | if fnType.Kind() != reflect.Func { |
| | panic("testing: F.Fuzz must receive a function") |
| | } |
| | if fnType.NumIn() < 2 || fnType.In(0) != reflect.TypeFor[*T]() { |
| | panic("testing: fuzz target must receive at least two arguments, where the first argument is a *T") |
| | } |
| | if fnType.NumOut() != 0 { |
| | panic("testing: fuzz target must not return a value") |
| | } |
| |
|
| | |
| | var types []reflect.Type |
| | for i := 1; i < fnType.NumIn(); i++ { |
| | t := fnType.In(i) |
| | if !supportedTypes[t] { |
| | panic(fmt.Sprintf("testing: unsupported type for fuzzing %v", t)) |
| | } |
| | types = append(types, t) |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | if f.fstate.mode != fuzzWorker { |
| | for _, c := range f.corpus { |
| | if err := f.fstate.deps.CheckCorpus(c.Values, types); err != nil { |
| | |
| | f.Fatal(err) |
| | } |
| | } |
| |
|
| | |
| | c, err := f.fstate.deps.ReadCorpus(filepath.Join(corpusDir, f.name), types) |
| | if err != nil { |
| | f.Fatal(err) |
| | } |
| | for i := range c { |
| | c[i].IsSeed = true |
| | if f.fstate.mode == fuzzCoordinator { |
| | |
| | |
| | c[i].Values = nil |
| | } |
| | } |
| |
|
| | f.corpus = append(f.corpus, c...) |
| | } |
| |
|
| | |
| | |
| | |
| | run := func(captureOut io.Writer, e corpusEntry) (ok bool) { |
| | if e.Values == nil { |
| | |
| | |
| | panic(fmt.Sprintf("corpus file %q was not unmarshaled", e.Path)) |
| | } |
| | if shouldFailFast() { |
| | return true |
| | } |
| | testName := f.name |
| | if e.Path != "" { |
| | testName = fmt.Sprintf("%s/%s", testName, filepath.Base(e.Path)) |
| | } |
| | if f.tstate.isFuzzing { |
| | |
| | |
| | |
| | |
| | f.tstate.match.clearSubNames() |
| | } |
| |
|
| | ctx, cancelCtx := context.WithCancel(f.ctx) |
| |
|
| | |
| | |
| | |
| | var pc [maxStackLen]uintptr |
| | n := runtime.Callers(2, pc[:]) |
| | t := &T{ |
| | common: common{ |
| | barrier: make(chan bool), |
| | signal: make(chan bool), |
| | name: testName, |
| | parent: &f.common, |
| | level: f.level + 1, |
| | creator: pc[:n], |
| | chatty: f.chatty, |
| | ctx: ctx, |
| | cancelCtx: cancelCtx, |
| | }, |
| | tstate: f.tstate, |
| | } |
| | if captureOut != nil { |
| | |
| | t.parent.w = captureOut |
| | } |
| | t.w = indenter{&t.common} |
| | t.setOutputWriter() |
| | if t.chatty != nil { |
| | t.chatty.Updatef(t.name, "=== RUN %s\n", t.name) |
| | } |
| | f.common.inFuzzFn, f.inFuzzFn = true, true |
| | go tRunner(t, func(t *T) { |
| | args := []reflect.Value{reflect.ValueOf(t)} |
| | for _, v := range e.Values { |
| | args = append(args, reflect.ValueOf(v)) |
| | } |
| | |
| | |
| | |
| | |
| | if f.tstate.isFuzzing { |
| | defer f.fstate.deps.SnapshotCoverage() |
| | f.fstate.deps.ResetCoverage() |
| | } |
| | fn.Call(args) |
| | }) |
| | <-t.signal |
| | if t.chatty != nil && t.chatty.json { |
| | t.chatty.Updatef(t.parent.name, "=== NAME %s\n", t.parent.name) |
| | } |
| | f.common.inFuzzFn, f.inFuzzFn = false, false |
| | return !t.Failed() |
| | } |
| |
|
| | switch f.fstate.mode { |
| | case fuzzCoordinator: |
| | |
| | |
| | |
| | corpusTargetDir := filepath.Join(corpusDir, f.name) |
| | cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name) |
| | err := f.fstate.deps.CoordinateFuzzing( |
| | fuzzDuration.d, |
| | int64(fuzzDuration.n), |
| | minimizeDuration.d, |
| | int64(minimizeDuration.n), |
| | *parallel, |
| | f.corpus, |
| | types, |
| | corpusTargetDir, |
| | cacheTargetDir) |
| | if err != nil { |
| | f.result = fuzzResult{Error: err} |
| | f.Fail() |
| | fmt.Fprintf(f.w, "%v\n", err) |
| | if crashErr, ok := err.(fuzzCrashError); ok { |
| | crashPath := crashErr.CrashPath() |
| | fmt.Fprintf(f.w, "Failing input written to %s\n", crashPath) |
| | testName := filepath.Base(crashPath) |
| | fmt.Fprintf(f.w, "To re-run:\ngo test -run=%s/%s\n", f.name, testName) |
| | } |
| | } |
| | |
| | |
| |
|
| | case fuzzWorker: |
| | |
| | |
| | if err := f.fstate.deps.RunFuzzWorker(func(e corpusEntry) error { |
| | |
| | |
| | |
| | |
| | var buf strings.Builder |
| | if ok := run(&buf, e); !ok { |
| | return errors.New(buf.String()) |
| | } |
| | return nil |
| | }); err != nil { |
| | |
| | |
| | |
| | f.Errorf("communicating with fuzzing coordinator: %v", err) |
| | } |
| |
|
| | default: |
| | |
| | |
| | for _, e := range f.corpus { |
| | name := fmt.Sprintf("%s/%s", f.name, filepath.Base(e.Path)) |
| | if _, ok, _ := f.tstate.match.fullName(nil, name); ok { |
| | run(f.w, e) |
| | } |
| | } |
| | } |
| | } |
| |
|
| | func (f *F) report() { |
| | if *isFuzzWorker || f.parent == nil { |
| | return |
| | } |
| | dstr := fmtDuration(f.duration) |
| | format := "--- %s: %s (%s)\n" |
| | if f.Failed() { |
| | f.flushToParent(f.name, format, "FAIL", f.name, dstr) |
| | } else if f.chatty != nil { |
| | if f.Skipped() { |
| | f.flushToParent(f.name, format, "SKIP", f.name, dstr) |
| | } else { |
| | f.flushToParent(f.name, format, "PASS", f.name, dstr) |
| | } |
| | } |
| | } |
| |
|
| | |
| | type fuzzResult struct { |
| | N int |
| | T time.Duration |
| | Error error |
| | } |
| |
|
| | func (r fuzzResult) String() string { |
| | if r.Error == nil { |
| | return "" |
| | } |
| | return r.Error.Error() |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | type fuzzCrashError interface { |
| | error |
| | Unwrap() error |
| |
|
| | |
| | |
| | |
| | |
| | CrashPath() string |
| | } |
| |
|
| | |
| | type fuzzState struct { |
| | deps testDeps |
| | mode fuzzMode |
| | } |
| |
|
| | type fuzzMode uint8 |
| |
|
| | const ( |
| | seedCorpusOnly fuzzMode = iota |
| | fuzzCoordinator |
| | fuzzWorker |
| | ) |
| |
|
| | |
| | |
| | |
| | func runFuzzTests(deps testDeps, fuzzTests []InternalFuzzTarget, deadline time.Time) (ran, ok bool) { |
| | ok = true |
| | if len(fuzzTests) == 0 || *isFuzzWorker { |
| | return ran, ok |
| | } |
| | m := newMatcher(deps.MatchString, *match, "-test.run", *skip) |
| | var mFuzz *matcher |
| | if *matchFuzz != "" { |
| | mFuzz = newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz", *skip) |
| | } |
| |
|
| | for _, procs := range cpuList { |
| | runtime.GOMAXPROCS(procs) |
| | for i := uint(0); i < *count; i++ { |
| | if shouldFailFast() { |
| | break |
| | } |
| |
|
| | tstate := newTestState(*parallel, m) |
| | tstate.deadline = deadline |
| | fstate := &fuzzState{deps: deps, mode: seedCorpusOnly} |
| | root := common{w: os.Stdout} |
| | if Verbose() { |
| | root.chatty = newChattyPrinter(root.w) |
| | } |
| | for _, ft := range fuzzTests { |
| | if shouldFailFast() { |
| | break |
| | } |
| | testName, matched, _ := tstate.match.fullName(nil, ft.Name) |
| | if !matched { |
| | continue |
| | } |
| | if mFuzz != nil { |
| | if _, fuzzMatched, _ := mFuzz.fullName(nil, ft.Name); fuzzMatched { |
| | |
| | |
| | continue |
| | } |
| | } |
| | ctx, cancelCtx := context.WithCancel(context.Background()) |
| | f := &F{ |
| | common: common{ |
| | signal: make(chan bool), |
| | barrier: make(chan bool), |
| | name: testName, |
| | parent: &root, |
| | level: root.level + 1, |
| | chatty: root.chatty, |
| | ctx: ctx, |
| | cancelCtx: cancelCtx, |
| | }, |
| | tstate: tstate, |
| | fstate: fstate, |
| | } |
| | f.w = indenter{&f.common} |
| | f.setOutputWriter() |
| | if f.chatty != nil { |
| | f.chatty.Updatef(f.name, "=== RUN %s\n", f.name) |
| | } |
| | go fRunner(f, ft.Fn) |
| | <-f.signal |
| | if f.chatty != nil && f.chatty.json { |
| | f.chatty.Updatef(f.parent.name, "=== NAME %s\n", f.parent.name) |
| | } |
| | ok = ok && !f.Failed() |
| | ran = ran || f.ran |
| | } |
| | if !ran { |
| | |
| | |
| | break |
| | } |
| | } |
| | } |
| |
|
| | return ran, ok |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | func runFuzzing(deps testDeps, fuzzTests []InternalFuzzTarget) (ok bool) { |
| | if len(fuzzTests) == 0 || *matchFuzz == "" { |
| | return true |
| | } |
| | m := newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz", *skip) |
| | tstate := newTestState(1, m) |
| | tstate.isFuzzing = true |
| | fstate := &fuzzState{ |
| | deps: deps, |
| | } |
| | root := common{w: os.Stdout} |
| | if *isFuzzWorker { |
| | root.w = io.Discard |
| | fstate.mode = fuzzWorker |
| | } else { |
| | fstate.mode = fuzzCoordinator |
| | } |
| | if Verbose() && !*isFuzzWorker { |
| | root.chatty = newChattyPrinter(root.w) |
| | } |
| | var fuzzTest *InternalFuzzTarget |
| | var testName string |
| | var matched []string |
| | for i := range fuzzTests { |
| | name, ok, _ := tstate.match.fullName(nil, fuzzTests[i].Name) |
| | if !ok { |
| | continue |
| | } |
| | matched = append(matched, name) |
| | fuzzTest = &fuzzTests[i] |
| | testName = name |
| | } |
| | if len(matched) == 0 { |
| | fmt.Fprintln(os.Stderr, "testing: warning: no fuzz tests to fuzz") |
| | return true |
| | } |
| | if len(matched) > 1 { |
| | fmt.Fprintf(os.Stderr, "testing: will not fuzz, -fuzz matches more than one fuzz test: %v\n", matched) |
| | return false |
| | } |
| |
|
| | ctx, cancelCtx := context.WithCancel(context.Background()) |
| | f := &F{ |
| | common: common{ |
| | signal: make(chan bool), |
| | barrier: nil, |
| | name: testName, |
| | parent: &root, |
| | level: root.level + 1, |
| | chatty: root.chatty, |
| | ctx: ctx, |
| | cancelCtx: cancelCtx, |
| | }, |
| | fstate: fstate, |
| | tstate: tstate, |
| | } |
| | f.w = indenter{&f.common} |
| | f.setOutputWriter() |
| | if f.chatty != nil { |
| | f.chatty.Updatef(f.name, "=== RUN %s\n", f.name) |
| | } |
| | go fRunner(f, fuzzTest.Fn) |
| | <-f.signal |
| | if f.chatty != nil { |
| | f.chatty.Updatef(f.parent.name, "=== NAME %s\n", f.parent.name) |
| | } |
| | return !f.failed |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | func fRunner(f *F, fn func(*F)) { |
| | |
| | |
| | |
| | defer func() { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | f.checkRaces() |
| | if f.Failed() { |
| | numFailed.Add(1) |
| | } |
| | err := recover() |
| | if err == nil { |
| | f.mu.RLock() |
| | fuzzNotCalled := !f.fuzzCalled && !f.skipped && !f.failed |
| | if !f.finished && !f.skipped && !f.failed { |
| | err = errNilPanicOrGoexit |
| | } |
| | f.mu.RUnlock() |
| | if fuzzNotCalled && err == nil { |
| | f.Error("returned without calling F.Fuzz, F.Fail, or F.Skip") |
| | } |
| | } |
| |
|
| | |
| | |
| | didPanic := false |
| | defer func() { |
| | if !didPanic { |
| | |
| | |
| | |
| | f.signal <- true |
| | } |
| | }() |
| |
|
| | |
| | |
| | doPanic := func(err any) { |
| | f.Fail() |
| | if r := f.runCleanup(recoverAndReturnPanic); r != nil { |
| | f.Logf("cleanup panicked with %v", r) |
| | } |
| | for root := &f.common; root.parent != nil; root = root.parent { |
| | root.mu.Lock() |
| | root.duration += highPrecisionTimeSince(root.start) |
| | d := root.duration |
| | root.mu.Unlock() |
| | root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d)) |
| | } |
| | didPanic = true |
| | panic(err) |
| | } |
| | if err != nil { |
| | doPanic(err) |
| | } |
| |
|
| | |
| | f.duration += highPrecisionTimeSince(f.start) |
| |
|
| | if len(f.sub) > 0 { |
| | |
| | |
| | |
| | |
| | f.tstate.release() |
| | close(f.barrier) |
| | |
| | for _, sub := range f.sub { |
| | <-sub.signal |
| | } |
| | cleanupStart := highPrecisionTimeNow() |
| | err := f.runCleanup(recoverAndReturnPanic) |
| | f.duration += highPrecisionTimeSince(cleanupStart) |
| | if err != nil { |
| | doPanic(err) |
| | } |
| | } |
| |
|
| | |
| | f.report() |
| | f.done = true |
| | f.setRan() |
| | }() |
| | defer func() { |
| | if len(f.sub) == 0 { |
| | f.runCleanup(normalPanic) |
| | } |
| | }() |
| |
|
| | f.start = highPrecisionTimeNow() |
| | f.resetRaces() |
| | fn(f) |
| |
|
| | |
| | |
| | f.mu.Lock() |
| | f.finished = true |
| | f.mu.Unlock() |
| | } |
| |
|