--- /dev/null
+# TODO(jayconrod): support shared memory on more platforms.
+[!darwin] [!linux] [!windows] skip
+
+[short] skip
+
+# When running seed inputs, T.Parallel should let multiple inputs run in
+# parallel.
+go test -run=FuzzSeed
+
+# When fuzzing, T.Parallel should be safe to call, but it should have no effect.
+# We just check that it doesn't hang, which would be the most obvious
+# failure mode.
+# TODO(jayconrod): check for the string "after T.Parallel". It's not printed
+# by 'go test', so we can't distinguish that crasher from some other panic.
+! go test -run=FuzzMutate -fuzz=FuzzMutate
+exists testdata/corpus/FuzzMutate
+
+-- go.mod --
+module fuzz_parallel
+
+go 1.17
+-- fuzz_parallel_test.go --
+package fuzz_parallel
+
+import (
+ "sort"
+ "sync"
+ "testing"
+)
+
+func FuzzSeed(f *testing.F) {
+ for _, v := range [][]byte{{'a'}, {'b'}, {'c'}} {
+ f.Add(v)
+ }
+
+ var mu sync.Mutex
+ var before, after []byte
+ f.Cleanup(func() {
+ sort.Slice(after, func(i, j int) bool { return after[i] < after[j] })
+ got := string(before) + string(after)
+ want := "abcabc"
+ if got != want {
+ f.Fatalf("got %q; want %q", got, want)
+ }
+ })
+
+ f.Fuzz(func(t *testing.T, b []byte) {
+ before = append(before, b...)
+ t.Parallel()
+ mu.Lock()
+ after = append(after, b...)
+ mu.Unlock()
+ })
+}
+
+func FuzzMutate(f *testing.F) {
+ f.Fuzz(func(t *testing.T, _ []byte) {
+ t.Parallel()
+ t.Error("after T.Parallel")
+ })
+}
// stop returns the error the process terminated with, if any (same as
// w.waitErr).
//
-// stop must be called once after start returns successfully, even if the
-// worker process terminates unexpectedly.
+// stop must be called at least once after start returns successfully, even if
+// the worker process terminates unexpectedly.
func (w *worker) stop() error {
if w.termC == nil {
panic("worker was not started successfully")
}
select {
case <-w.termC:
- // Worker already terminated, perhaps unexpectedly.
+ // Worker already terminated.
if w.client == nil {
- panic("worker already stopped")
+ // stop already called.
+ return w.waitErr
}
+ // Possible unexpected termination.
w.client.Close()
w.cmd = nil
w.client = nil
// fn is called in its own goroutine.
//
// TODO(jayconrod,katiehockman): dedupe testdata corpus with entries from f.Add
- // TODO(jayconrod,katiehockman): handle T.Parallel calls within fuzz function.
// TODO(jayconrod,katiehockman): improve output when running the subtest.
// e.g. instead of
// --- FAIL: FuzzSomethingError/#00 (0.00s)
}
f := &F{
common: common{
- signal: make(chan bool),
- name: testName,
- parent: &root,
- level: root.level + 1,
- chatty: root.chatty,
+ signal: make(chan bool),
+ barrier: make(chan bool),
+ name: testName,
+ parent: &root,
+ level: root.level + 1,
+ chatty: root.chatty,
},
testContext: tctx,
fuzzContext: fctx,
target = ft
f = &F{
common: common{
- signal: make(chan bool),
- name: testName,
- parent: &root,
- level: root.level + 1,
- chatty: root.chatty,
+ signal: make(chan bool),
+ barrier: nil, // T.Parallel has no effect when fuzzing.
+ name: testName,
+ parent: &root,
+ level: root.level + 1,
+ chatty: root.chatty,
},
fuzzContext: fctx,
testContext: tctx,
//
// fRunner is analogous with tRunner, which wraps subtests started with T.Run.
// Tests and fuzz targets work a little differently, so for now, these functions
-// aren't consoldiated.
+// aren't consolidated. In particular, because there are no F.Run and F.Parallel
+// methods, i.e., no fuzz sub-targets or parallel fuzz targets, a few
+// simplifications are made. We also require that F.Fuzz, F.Skip, or F.Fail is
+// called.
func fRunner(f *F, fn func(*F)) {
// When this goroutine is done, either because runtime.Goexit was called,
// a panic started, or fn returned normally, record the duration and send
err = errNilPanicOrGoexit
}
+ // 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
+ defer func() {
+ if didPanic {
+ return
+ }
+ 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,
// flush the output log up to the root, then panic.
- if err != nil {
+ doPanic := func(err interface{}) {
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 += time.Since(root.start)
root.mu.Unlock()
root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
}
+ didPanic = true
panic(err)
}
+ if err != nil {
+ doPanic(err)
+ }
- // No panic or inappropriate Goexit. Record duration and report the result.
+ // No panic or inappropriate Goexit.
f.duration += time.Since(f.start)
+
+ if len(f.sub) > 0 {
+ // Run parallel inputs.
+ // Release the parallel subtests.
+ close(f.barrier)
+ // Wait for the subtests to complete.
+ for _, sub := range f.sub {
+ <-sub.signal
+ }
+ cleanupStart := time.Now()
+ err := f.runCleanup(recoverAndReturnPanic)
+ f.duration += time.Since(cleanupStart)
+ if err != nil {
+ doPanic(err)
+ }
+ }
+
+ // Report after all subtests have finished.
f.report()
f.done = true
f.setRan()
-
- // 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
}()
defer func() {
- f.runCleanup(normalPanic)
+ if len(f.sub) == 0 {
+ f.runCleanup(normalPanic)
+ }
}()
f.start = time.Now()