]> Cypherpunks repositories - gostls13.git/commitdiff
testing: address feedback for dev.fuzz merge
authorJay Conrod <jayconrod@google.com>
Tue, 21 Sep 2021 23:30:06 +0000 (16:30 -0700)
committerJay Conrod <jayconrod@google.com>
Fri, 24 Sep 2021 21:42:31 +0000 (21:42 +0000)
Based on comments in CL 348469.

Note that with this change, F.Fuzz no longer calls
runtime.Goexit. This simplifies our logic and makes F.Fuzz more
predictable.

Change-Id: I6c3c65b0e8e8f261621cbe2f17375e8164ef60a0
Reviewed-on: https://go-review.googlesource.com/c/go/+/351316
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
12 files changed:
src/cmd/go/alldocs.go
src/cmd/go/internal/cache/cache.go
src/cmd/go/internal/clean/clean.go
src/cmd/go/internal/help/helpdoc.go
src/cmd/go/internal/load/flag.go
src/cmd/go/internal/load/pkg.go
src/cmd/go/internal/test/test.go
src/cmd/go/internal/work/init.go
src/cmd/go/testdata/script/test_fuzz.txt
src/cmd/go/testdata/script/test_fuzz_mutate_crash.txt
src/testing/fuzz.go
src/testing/testing.go

index 0036d8615fe38b3c36cdbf5108757395ae230c97..02d2afc5828b88333e13abdff37d05019916200c 100644 (file)
 // dependencies.
 //
 // The -fuzzcache flag causes clean to remove files stored in the Go build
-// cache for fuzz testing. Files stored in source testdata directories
-// are left in place.
+// cache for fuzz testing. The fuzzing engine caches files that expand
+// code coverage, so removing them may make fuzzing less effective until
+// new inputs are found that provide the same coverage. These files are
+// distinct from those stored in testdata directory; clean does not remove
+// those files.
 //
 // For more about build flags, see 'go help build'.
 //
 // See 'go help test' for details. Running 'go clean -testcache' removes
 // all cached test results (but not cached build results).
 //
+// The go command also caches values used in fuzzing with 'go test -fuzz',
+// specifically, values that expanded code coverage when passed to a
+// fuzz function. These values are not used for regular building and
+// testing, but they're stored in a subdirectory of the build cache.
+// Running 'go clean -fuzzcache' removes all cached fuzzing values.
+// This may make fuzzing less effective, temporarily.
+//
 // The GODEBUG environment variable can enable printing of debugging
 // information about the state of the cache:
 //
index 596f22e8fc1fca11debc31466408ae5dc50bc34d..93d7c25658fa07eef33d282ff3e61e90cf63d616 100644 (file)
@@ -540,6 +540,8 @@ func (c *Cache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
 // This directory is managed by the internal/fuzz package. Files in this
 // directory aren't removed by the 'go clean -cache' command or by Trim.
 // They may be removed with 'go clean -fuzzcache'.
+//
+// TODO(#48526): make Trim remove unused files from this directory.
 func (c *Cache) FuzzDir() string {
        return filepath.Join(c.dir, "fuzz")
 }
index ca7623ea215660b4f124b818054418f1a1833a5b..dc93cdf5983126c42adeddb2504276a8a41f834f 100644 (file)
@@ -76,8 +76,11 @@ download cache, including unpacked source code of versioned
 dependencies.
 
 The -fuzzcache flag causes clean to remove files stored in the Go build
-cache for fuzz testing. Files stored in source testdata directories
-are left in place.
+cache for fuzz testing. The fuzzing engine caches files that expand
+code coverage, so removing them may make fuzzing less effective until
+new inputs are found that provide the same coverage. These files are
+distinct from those stored in testdata directory; clean does not remove
+those files.
 
 For more about build flags, see 'go help build'.
 
@@ -220,7 +223,7 @@ func runClean(ctx context.Context, cmd *base.Command, args []string) {
                }
                if !cfg.BuildN {
                        if err := os.RemoveAll(fuzzDir); err != nil {
-                               base.Errorf("go clean -fuzzcache: %v", err)
+                               base.Errorf("go: %v", err)
                        }
                }
        }
index 749dcf192b0f090803cd85574507409eea42014d..035235fe1b5a1d39a3e8228daa20362fee8af86c 100644 (file)
@@ -775,6 +775,13 @@ The go command also caches successful package test results.
 See 'go help test' for details. Running 'go clean -testcache' removes
 all cached test results (but not cached build results).
 
+The go command also caches values used in fuzzing with 'go test -fuzz',
+specifically, values that expanded code coverage when passed to a
+fuzz function. These values are not used for regular building and
+testing, but they're stored in a subdirectory of the build cache.
+Running 'go clean -fuzzcache' removes all cached fuzzing values.
+This may make fuzzing less effective, temporarily.
+
 The GODEBUG environment variable can enable printing of debugging
 information about the state of the cache:
 
index 24670524fc11afbfd86860bd868c31c3bfc6a6a0..4e0cb5bc19ef76bd43899380599854ccc73ca5f5 100644 (file)
@@ -22,9 +22,8 @@ var (
 // that allows specifying different effective flags for different packages.
 // See 'go help build' for more details about per-package flags.
 type PerPackageFlag struct {
-       present      bool
-       values       []ppfValue
-       seenPackages map[*Package]bool // the packages for which the flags have already been set
+       present bool
+       values  []ppfValue
 }
 
 // A ppfValue is a single <pattern>=<flags> per-package flag value.
index 317053d9182320b3f95d5bab6ed03b54f050cd61..4013330bc47f9dc18c1c7b728ad7625ab3a4bf77 100644 (file)
@@ -2630,20 +2630,10 @@ func (e *mainPackageError) ImportPath() string {
 
 func setToolFlags(pkgs ...*Package) {
        for _, p := range PackageList(pkgs) {
-               appendFlags(p, &p.Internal.Asmflags, &BuildAsmflags)
-               appendFlags(p, &p.Internal.Gcflags, &BuildGcflags)
-               appendFlags(p, &p.Internal.Ldflags, &BuildLdflags)
-               appendFlags(p, &p.Internal.Gccgoflags, &BuildGccgoflags)
-       }
-}
-
-func appendFlags(p *Package, flags *[]string, packageFlag *PerPackageFlag) {
-       if !packageFlag.seenPackages[p] {
-               if packageFlag.seenPackages == nil {
-                       packageFlag.seenPackages = make(map[*Package]bool)
-               }
-               packageFlag.seenPackages[p] = true
-               *flags = append(*flags, packageFlag.For(p)...)
+               p.Internal.Asmflags = BuildAsmflags.For(p)
+               p.Internal.Gcflags = BuildGcflags.For(p)
+               p.Internal.Ldflags = BuildLdflags.For(p)
+               p.Internal.Gccgoflags = BuildGccgoflags.For(p)
        }
 }
 
index c8305c7808a9d245cdc3c74cf63748c777723fd2..518555ecba4567f573226e46de33385c98e9c021 100644 (file)
@@ -824,7 +824,7 @@ func runTest(ctx context.Context, cmd *base.Command, args []string) {
        if testFuzz != "" && fuzzFlags != nil {
                // Don't instrument packages which may affect coverage guidance but are
                // unlikely to be useful. Most of these are used by the testing or
-               // internal/fuzz concurrently with fuzzing.
+               // internal/fuzz packages concurrently with fuzzing.
                var fuzzNoInstrument = map[string]bool{
                        "context":       true,
                        "internal/fuzz": true,
index 34d2e1cbe1b70ff13bf48d0a6c27e6782147be80..1f8ec02df182407a03df0f1ee8eb42f527cbb02f 100644 (file)
@@ -61,8 +61,11 @@ func BuildInit() {
 }
 
 // FuzzInstrumentFlags returns compiler flags that enable fuzzing instrumation
-// on supported platforms. On unsupported platforms, FuzzInstrumentFlags returns
-// nil.
+// on supported platforms.
+//
+// On unsupported platforms, FuzzInstrumentFlags returns nil, meaning no
+// instrumentation is added. 'go test -fuzz' still works without coverage,
+// but it generates random inputs without guidance, so it's much less effective.
 func FuzzInstrumentFlags() []string {
        // TODO: expand the set of supported platforms, with testing.
        // Nothing about the instrumentation is OS specific, but only amd64 and arm64
index b1a02f46ebc37caa523c199afddb710d2dcaa239..c9930aa37e4bb48eb3f37b1ab800dca63f66b975 100644 (file)
@@ -77,10 +77,15 @@ stdout 'f.Fuzz function'
 stdout FAIL
 stdout 'f.Fuzz function'
 
-# Test that a call to f.Fatal after the Fuzz func is never executed.
-go test fatal_after_fuzz_func_fuzz_test.go
-stdout ok
-! stdout FAIL
+# Test that runtime.Goexit within the fuzz function is an error.
+! go test goexit_fuzz_fn_fuzz_test.go
+! stdout ^ok
+stdout FAIL
+
+# Test that a call to f.Fatal after the Fuzz func is executed.
+! go test fatal_after_fuzz_func_fuzz_test.go
+! stdout ok
+stdout FAIL
 
 # Test that missing *T in f.Fuzz causes a non-zero exit status.
 ! go test incomplete_fuzz_call_fuzz_test.go
@@ -267,6 +272,18 @@ func Fuzz(f *testing.F) {
     })
 }
 
+-- goexit_fuzz_fn_fuzz_test.go --
+package goexit_fuzz_fn_fuzz
+
+import "testing"
+
+func Fuzz(f *testing.F) {
+    f.Add([]byte("aa"))
+    f.Fuzz(func(t *testing.T, b []byte) {
+        runtime.Goexit()
+    })
+}
+
 -- fatal_after_fuzz_func_fuzz_test.go --
 package fatal_after_fuzz_func_fuzz
 
index 1b8b79b3ddea92a64dfc3e9e76a248f7df0eb03f..79476ecb282ebb3e9fb00c6c59dc771d56bdc20a 100644 (file)
@@ -29,6 +29,11 @@ stdout 'testdata[/\\]fuzz[/\\]FuzzWithNilPanic[/\\]'
 stdout 'runtime.Goexit'
 go run check_testdata.go FuzzWithNilPanic
 
+! go test -run=FuzzWithGoexit -fuzz=FuzzWithGoexit -fuzztime=100x -fuzzminimizetime=1000x
+stdout 'testdata[/\\]fuzz[/\\]FuzzWithGoexit[/\\]'
+stdout 'runtime.Goexit'
+go run check_testdata.go FuzzWithGoexit
+
 ! go test -run=FuzzWithFail -fuzz=FuzzWithFail -fuzztime=100x -fuzzminimizetime=1000x
 stdout 'testdata[/\\]fuzz[/\\]FuzzWithFail[/\\]'
 go run check_testdata.go FuzzWithFail
@@ -108,7 +113,8 @@ go 1.16
 package fuzz_crash
 
 import (
-    "os"
+       "os"
+       "runtime"
        "testing"
 )
 
@@ -130,6 +136,15 @@ func FuzzWithNilPanic(f *testing.F) {
        })
 }
 
+func FuzzWithGoexit(f *testing.F) {
+       f.Add([]byte("aa"))
+       f.Fuzz(func(t *testing.T, b []byte) {
+               if string(b) != "aa" {
+                       runtime.Goexit()
+               }
+       })
+}
+
 func FuzzWithFail(f *testing.F) {
        f.Add([]byte("aa"))
        f.Fuzz(func(t *testing.T, b []byte) {
index ddce065783a5d6e74208958567bb867bab12de1c..771917b0695b1053f7008e8c573aba89452b1c8e 100644 (file)
@@ -69,7 +69,7 @@ type F struct {
        // from testdata.
        corpus []corpusEntry
 
-       result     FuzzResult
+       result     fuzzResult
        fuzzCalled bool
 }
 
@@ -290,17 +290,20 @@ var supportedTypes = map[reflect.Type]bool{
 // whose remaining arguments are the types to be fuzzed.
 // For example:
 //
-// f.Fuzz(func(t *testing.T, b []byte, i int) { ... })
+//     f.Fuzz(func(t *testing.T, b []byte, i int) { ... })
 //
-// This function should be fast, deterministic, and stateless.
+// The following types are allowed: []byte, string, bool, byte, rune, float32,
+// float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64.
+// More types may be supported in the future.
 //
-// No mutatable input arguments, or pointers to them, should be retained between
-// executions of the fuzz function, as the memory backing them may be mutated
-// during a subsequent invocation.
+// This function sould be fast and deterministic, and its behavior should not
+// depend on shared state. No mutatable input arguments, or pointers to them,
+// should be retained between executions of the fuzz function, as the memory
+// backing them may be mutated during a subsequent invocation.
 //
-// This is a terminal function which will terminate the currently running fuzz
-// target by calling runtime.Goexit.
-// To run any code after fuzzing stops, use (*F).Cleanup.
+// When fuzzing, F.Fuzz does not return until a problem is found, time runs
+// out (set with -fuzztime), or the test process is interrupted by a signal.
+// F.Fuzz should be called exactly once unless F.Skip or F.Fail is called.
 func (f *F) Fuzz(ff interface{}) {
        if f.fuzzCalled {
                panic("testing: F.Fuzz called more than once")
@@ -440,7 +443,7 @@ func (f *F) Fuzz(ff interface{}) {
                        corpusTargetDir,
                        cacheTargetDir)
                if err != nil {
-                       f.result = FuzzResult{Error: err}
+                       f.result = fuzzResult{Error: err}
                        f.Fail()
                        fmt.Fprintf(f.w, "%v\n", err)
                        if crashErr, ok := err.(fuzzCrashError); ok {
@@ -469,16 +472,6 @@ func (f *F) Fuzz(ff interface{}) {
                        run(e)
                }
        }
-
-       // Record that the fuzz function (or coordinateFuzzing or runFuzzWorker)
-       // returned normally. This is used to distinguish runtime.Goexit below
-       // from panic(nil).
-       f.finished = true
-
-       // Terminate the goroutine. F.Fuzz should not return.
-       // We cannot call runtime.Goexit from a deferred function: if there is a
-       // panic, that would replace the panic value with nil.
-       runtime.Goexit()
 }
 
 func (f *F) report() {
@@ -498,14 +491,14 @@ func (f *F) report() {
        }
 }
 
-// FuzzResult contains the results of a fuzz run.
-type FuzzResult struct {
+// fuzzResult contains the results of a fuzz run.
+type fuzzResult struct {
        N     int           // The number of iterations.
        T     time.Duration // The total time taken.
        Error error         // Error is the error from the crash
 }
 
-func (r FuzzResult) String() string {
+func (r fuzzResult) String() string {
        s := ""
        if r.Error == nil {
                return s
@@ -698,27 +691,28 @@ func fRunner(f *F, fn func(*F)) {
                        atomic.AddUint32(&numFailed, 1)
                }
                err := recover()
-               f.mu.RLock()
-               ok := f.skipped || f.failed || (f.fuzzCalled && f.finished)
-               f.mu.RUnlock()
-               if err == nil && !ok {
-                       err = errNilPanicOrGoexit
+               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")
+                       }
                }
 
                // Use a deferred call to ensure that we report that the test is
-               // complete even if a cleanup function calls t.FailNow. See issue 41355.
+               // complete even if a cleanup function calls F.FailNow. See issue 41355.
                didPanic := false
                defer func() {
-                       if didPanic {
-                               return
+                       if !didPanic {
+                               // Only report that the test is complete if it doesn't panic,
+                               // as otherwise the test binary can exit before the panic is
+                               // reported to the user. See issue 41479.
+                               f.signal <- true
                        }
-                       if err != nil {
-                               panic(err)
-                       }
-                       // Only report that the test is complete if it doesn't panic,
-                       // as otherwise the test binary can exit before the panic is
-                       // reported to the user. See issue 41479.
-                       f.signal <- true
                }()
 
                // If we recovered a panic or inappropriate runtime.Goexit, fail the test,
@@ -747,8 +741,9 @@ func fRunner(f *F, fn func(*F)) {
 
                if len(f.sub) > 0 {
                        // Unblock inputs that called T.Parallel while running the seed corpus.
-                       // T.Parallel has no effect while fuzzing, so this only affects fuzz
-                       // targets run as normal tests.
+                       // This only affects fuzz targets run as normal tests.
+                       // While fuzzing, T.Parallel has no effect, so f.sub is empty, and this
+                       // branch is not taken. f.barrier is nil in that case.
                        close(f.barrier)
                        // Wait for the subtests to complete.
                        for _, sub := range f.sub {
@@ -776,11 +771,9 @@ func fRunner(f *F, fn func(*F)) {
        f.start = time.Now()
        fn(f)
 
-       // Code beyond this point is only executed if fn returned normally.
-       // That means fn did not call F.Fuzz or F.Skip. It should have called F.Fail.
+       // Code beyond this point will not be executed when FailNow or SkipNow
+       // is invoked.
        f.mu.Lock()
-       defer f.mu.Unlock()
-       if !f.failed {
-               panic(f.name + " returned without calling F.Fuzz, F.Fail, or F.Skip")
-       }
+       f.finished = true
+       f.mu.Unlock()
 }
index ac1e52af85ddfc81b27c958f45570b6e6592490c..b3f4b4da585b039f637aefdcca8fd0f6687cd8c8 100644 (file)
 //
 // For example:
 //
-// func FuzzHex(f *testing.F) {
-//   for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
-//     f.Add(seed)
-//   }
-//   f.Fuzz(func(t *testing.T, in []byte) {
-//     enc := hex.EncodeToString(in)
-//     out, err := hex.DecodeString(enc)
-//     if err != nil {
-//       t.Fatalf("%v: decode: %v", in, err)
-//     }
-//     if !bytes.Equal(in, out) {
-//       t.Fatalf("%v: not equal after round trip: %v", in, out)
+//     func FuzzHex(f *testing.F) {
+//       for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} {
+//         f.Add(seed)
+//       }
+//       f.Fuzz(func(t *testing.T, in []byte) {
+//         enc := hex.EncodeToString(in)
+//         out, err := hex.DecodeString(enc)
+//         if err != nil {
+//           t.Fatalf("%v: decode: %v", in, err)
+//         }
+//         if !bytes.Equal(in, out) {
+//           t.Fatalf("%v: not equal after round trip: %v", in, out)
+//         }
+//       })
 //     }
-//   })
-// }
 //
 // Seed inputs may be registered by calling F.Add or by storing files in the
 // directory testdata/fuzz/<Name> (where <Name> is the name of the fuzz target)
@@ -506,7 +506,7 @@ type common struct {
        name     string    // Name of test or benchmark.
        start    time.Time // Time test or benchmark started
        duration time.Duration
-       barrier  chan bool // To signal parallel subtests they may start.
+       barrier  chan bool // To signal parallel subtests they may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing).
        signal   chan bool // To signal a test is done.
        sub      []*T      // Queue of subtests to be run in parallel.
 
@@ -628,13 +628,6 @@ func (c *common) frameSkip(skip int) runtime.Frame {
 // and inserts the final newline if needed and indentation spaces for formatting.
 // This function must be called with c.mu held.
 func (c *common) decorate(s string, skip int) string {
-       if c.helperNames == nil {
-               c.helperNames = make(map[string]struct{})
-               for pc := range c.helperPCs {
-                       c.helperNames[pcToName(pc)] = struct{}{}
-               }
-       }
-
        frame := c.frameSkip(skip)
        file := frame.File
        line := frame.Line
@@ -1280,14 +1273,6 @@ func tRunner(t *T, fn func(t *T)) {
                err := recover()
                signal := true
 
-               if err != nil && t.isFuzzing() {
-                       t.Errorf("panic: %s\n%s\n", err, string(debug.Stack()))
-                       t.mu.Lock()
-                       t.finished = true
-                       t.mu.Unlock()
-                       err = nil
-               }
-
                t.mu.RLock()
                finished := t.finished
                t.mu.RUnlock()
@@ -1306,6 +1291,18 @@ func tRunner(t *T, fn func(t *T)) {
                        }
                }
 
+               if err != nil && t.isFuzzing() {
+                       prefix := "panic: "
+                       if err == errNilPanicOrGoexit {
+                               prefix = ""
+                       }
+                       t.Errorf("%s%s\n%s\n", prefix, err, string(debug.Stack()))
+                       t.mu.Lock()
+                       t.finished = true
+                       t.mu.Unlock()
+                       err = nil
+               }
+
                // Use a deferred call to ensure that we report that the test is
                // complete even if a cleanup function calls t.FailNow. See issue 41355.
                didPanic := false