aboutsummaryrefslogtreecommitdiff
path: root/src/testing
diff options
context:
space:
mode:
authorJay Conrod <jayconrod@google.com>2020-12-17 17:25:42 -0500
committerJay Conrod <jayconrod@google.com>2020-12-23 16:33:50 +0000
commita1646595e63cc0bf7f566bb9b657f826cbda22a1 (patch)
tree56f50513d0ee0e56fda0125cd38c72bc12ded09a /src/testing
parent3ea342eb2e2f65a02bc84e206a4e7615747df49a (diff)
downloadgo-a1646595e63cc0bf7f566bb9b657f826cbda22a1.tar.xz
[dev.fuzz] cmd/go: implement -fuzztime flag and support cancellation
fuzz.CoordinateFuzzing and RunFuzzWorker now accept a context.Context parameter. They should terminate gracefully when the context is cancelled. The worker should exit quickly without processing more inputs. The coordinator should save interesting inputs to the cache. The testing package can't import context directly, so it provides a timeout argument to testdeps.CoordinateFuzzing instead. The testdeps wrapper sets the timeout and installs an interrupt handler (for SIGINT on POSIX and the equivalent on Windows) that cancels the context when ^C is pressed. Note that on POSIX platforms, pressing ^C causes the shell to deliver SIGINT to all processes in the active group: so 'go test', the coordinator, and the workers should all react to that. On Windows, pressing ^C only interrupts 'go test'. We may want to look at that separately. Change-Id: I924d3be2905f9685dae82ff3c047ca3d6b5e2357 Reviewed-on: https://go-review.googlesource.com/c/go/+/279487 Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Katie Hockman <katie@golang.org> Trust: Katie Hockman <katie@golang.org> Trust: Jay Conrod <jayconrod@google.com>
Diffstat (limited to 'src/testing')
-rw-r--r--src/testing/fuzz.go6
-rw-r--r--src/testing/internal/testdeps/deps.go49
-rw-r--r--src/testing/testing.go32
3 files changed, 70 insertions, 17 deletions
diff --git a/src/testing/fuzz.go b/src/testing/fuzz.go
index 996e361300..4351704b58 100644
--- a/src/testing/fuzz.go
+++ b/src/testing/fuzz.go
@@ -16,12 +16,14 @@ import (
func initFuzzFlags() {
matchFuzz = flag.String("test.fuzz", "", "run the fuzz target matching `regexp`")
+ fuzzDuration = flag.Duration("test.fuzztime", 0, "time to spend fuzzing; default (0) is to run indefinitely")
fuzzCacheDir = flag.String("test.fuzzcachedir", "", "directory where interesting fuzzing inputs are stored")
isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values")
}
var (
matchFuzz *string
+ fuzzDuration *time.Duration
fuzzCacheDir *string
isFuzzWorker *bool
@@ -136,7 +138,7 @@ func (f *F) Fuzz(ff interface{}) {
}
corpusTargetDir := filepath.Join(corpusDir, f.name)
cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name)
- err := f.context.coordinateFuzzing(*parallel, seed, corpusTargetDir, cacheTargetDir)
+ err := f.context.coordinateFuzzing(*fuzzDuration, *parallel, seed, corpusTargetDir, cacheTargetDir)
if err != nil {
f.Fail()
f.result = FuzzResult{Error: err}
@@ -279,7 +281,7 @@ func (r FuzzResult) String() string {
type fuzzContext struct {
runMatch *matcher
fuzzMatch *matcher
- coordinateFuzzing func(int, [][]byte, string, string) error
+ coordinateFuzzing func(time.Duration, int, [][]byte, string, string) error
runFuzzWorker func(func([]byte) error) error
readCorpus func(string) ([][]byte, error)
}
diff --git a/src/testing/internal/testdeps/deps.go b/src/testing/internal/testdeps/deps.go
index dcca6032d0..12da4f3863 100644
--- a/src/testing/internal/testdeps/deps.go
+++ b/src/testing/internal/testdeps/deps.go
@@ -12,13 +12,17 @@ package testdeps
import (
"bufio"
+ "context"
"internal/fuzz"
"internal/testlog"
"io"
+ "os"
+ "os/signal"
"regexp"
"runtime/pprof"
"strings"
"sync"
+ "time"
)
// TestDeps is an implementation of the testing.testDeps interface,
@@ -128,12 +132,51 @@ func (TestDeps) SetPanicOnExit0(v bool) {
testlog.SetPanicOnExit0(v)
}
-func (TestDeps) CoordinateFuzzing(parallel int, seed [][]byte, corpusDir, cacheDir string) error {
- return fuzz.CoordinateFuzzing(parallel, seed, corpusDir, cacheDir)
+func (TestDeps) CoordinateFuzzing(timeout time.Duration, parallel int, seed [][]byte, corpusDir, cacheDir string) error {
+ // Fuzzing may be interrupted with a timeout or if the user presses ^C.
+ // In either case, we'll stop worker processes gracefully and save
+ // crashers and interesting values.
+ ctx := context.Background()
+ cancel := func() {}
+ if timeout > 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ }
+ interruptC := make(chan os.Signal, 1)
+ signal.Notify(interruptC, os.Interrupt)
+ go func() {
+ <-interruptC
+ cancel()
+ }()
+ defer close(interruptC)
+
+ err := fuzz.CoordinateFuzzing(ctx, parallel, seed, corpusDir, cacheDir)
+ if err == ctx.Err() {
+ return nil
+ }
+ return err
}
func (TestDeps) RunFuzzWorker(fn func([]byte) error) error {
- return fuzz.RunFuzzWorker(fn)
+ // Worker processes may or may not receive a signal when the user presses ^C
+ // On POSIX operating systems, a signal sent to a process group is delivered
+ // to all processes in that group. This is not the case on Windows.
+ // If the worker is interrupted, return quickly and without error.
+ // If only the coordinator process is interrupted, it tells each worker
+ // process to stop by closing its "fuzz_in" pipe.
+ ctx, cancel := context.WithCancel(context.Background())
+ interruptC := make(chan os.Signal, 1)
+ signal.Notify(interruptC, os.Interrupt)
+ go func() {
+ <-interruptC
+ cancel()
+ }()
+ defer close(interruptC)
+
+ err := fuzz.RunFuzzWorker(ctx, fn)
+ if err == ctx.Err() {
+ return nil
+ }
+ return nil
}
func (TestDeps) ReadCorpus(dir string) ([][]byte, error) {
diff --git a/src/testing/testing.go b/src/testing/testing.go
index e3e35fa13a..39316122a6 100644
--- a/src/testing/testing.go
+++ b/src/testing/testing.go
@@ -1353,17 +1353,19 @@ var errMain = errors.New("testing: unexpected use of func Main")
type matchStringOnly func(pat, str string) (bool, error)
-func (f matchStringOnly) MatchString(pat, str string) (bool, error) { return f(pat, str) }
-func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain }
-func (f matchStringOnly) StopCPUProfile() {}
-func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain }
-func (f matchStringOnly) ImportPath() string { return "" }
-func (f matchStringOnly) StartTestLog(io.Writer) {}
-func (f matchStringOnly) StopTestLog() error { return errMain }
-func (f matchStringOnly) SetPanicOnExit0(bool) {}
-func (f matchStringOnly) CoordinateFuzzing(int, [][]byte, string, string) error { return errMain }
-func (f matchStringOnly) RunFuzzWorker(func([]byte) error) error { return errMain }
-func (f matchStringOnly) ReadCorpus(string) ([][]byte, error) { return nil, errMain }
+func (f matchStringOnly) MatchString(pat, str string) (bool, error) { return f(pat, str) }
+func (f matchStringOnly) StartCPUProfile(w io.Writer) error { return errMain }
+func (f matchStringOnly) StopCPUProfile() {}
+func (f matchStringOnly) WriteProfileTo(string, io.Writer, int) error { return errMain }
+func (f matchStringOnly) ImportPath() string { return "" }
+func (f matchStringOnly) StartTestLog(io.Writer) {}
+func (f matchStringOnly) StopTestLog() error { return errMain }
+func (f matchStringOnly) SetPanicOnExit0(bool) {}
+func (f matchStringOnly) CoordinateFuzzing(time.Duration, int, [][]byte, string, string) error {
+ return errMain
+}
+func (f matchStringOnly) RunFuzzWorker(func([]byte) error) error { return errMain }
+func (f matchStringOnly) ReadCorpus(string) ([][]byte, error) { return nil, errMain }
// Main is an internal function, part of the implementation of the "go test" command.
// It was exported because it is cross-package and predates "internal" packages.
@@ -1406,7 +1408,7 @@ type testDeps interface {
StartTestLog(io.Writer)
StopTestLog() error
WriteProfileTo(string, io.Writer, int) error
- CoordinateFuzzing(int, [][]byte, string, string) error
+ CoordinateFuzzing(time.Duration, int, [][]byte, string, string) error
RunFuzzWorker(func([]byte) error) error
ReadCorpus(string) ([][]byte, error)
}
@@ -1448,6 +1450,12 @@ func (m *M) Run() (code int) {
m.exitCode = 2
return
}
+ if *fuzzDuration < 0 {
+ fmt.Fprintln(os.Stderr, "testing: -fuzztime can only be given a positive duration, or zero to run indefinitely")
+ flag.Usage()
+ m.exitCode = 2
+ return
+ }
if *matchFuzz != "" && *fuzzCacheDir == "" {
fmt.Fprintln(os.Stderr, "testing: internal error: -test.fuzzcachedir must be set if -test.fuzz is set")
flag.Usage()