aboutsummaryrefslogtreecommitdiff
path: root/src/testing/fuzz.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/testing/fuzz.go')
-rw-r--r--src/testing/fuzz.go199
1 files changed, 99 insertions, 100 deletions
diff --git a/src/testing/fuzz.go b/src/testing/fuzz.go
index d50ea793e0..5a3117b1e0 100644
--- a/src/testing/fuzz.go
+++ b/src/testing/fuzz.go
@@ -70,10 +70,6 @@ type F struct {
fuzzContext *fuzzContext
testContext *testContext
- // inFuzzFn is true when the fuzz function is running. Most F methods cannot
- // be called when inFuzzFn is true.
- inFuzzFn bool
-
// corpus is a set of seed corpus entries, added with F.Add and loaded
// from testdata.
corpus []corpusEntry
@@ -300,13 +296,13 @@ func (f *F) Fuzz(ff any) {
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,
+ runParallel: make(chan struct{}),
+ doneOrParallel: make(chan struct{}),
+ name: testName,
+ parent: &f.common,
+ level: f.level + 1,
+ creator: pc[:n],
+ chatty: f.chatty,
},
context: f.testContext,
}
@@ -318,7 +314,7 @@ func (f *F) Fuzz(ff any) {
if t.chatty != nil {
t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
}
- f.common.inFuzzFn, f.inFuzzFn = true, true
+ f.inFuzzFn = true
go tRunner(t, func(t *T) {
args := []reflect.Value{reflect.ValueOf(t)}
for _, v := range e.Values {
@@ -334,11 +330,11 @@ func (f *F) Fuzz(ff any) {
}
fn.Call(args)
})
- <-t.signal
+ <-t.doneOrParallel
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
+ f.inFuzzFn = false
return !t.Failed()
}
@@ -510,12 +506,12 @@ func runFuzzTests(deps testDeps, fuzzTests []InternalFuzzTarget, deadline time.T
}
f := &F{
common: common{
- signal: make(chan bool),
- barrier: make(chan bool),
- name: testName,
- parent: &root,
- level: root.level + 1,
- chatty: root.chatty,
+ doneOrParallel: make(chan struct{}),
+ runParallel: make(chan struct{}),
+ name: testName,
+ parent: &root,
+ level: root.level + 1,
+ chatty: root.chatty,
},
testContext: tctx,
fuzzContext: fctx,
@@ -525,12 +521,12 @@ func runFuzzTests(deps testDeps, fuzzTests []InternalFuzzTarget, deadline time.T
f.chatty.Updatef(f.name, "=== RUN %s\n", f.name)
}
go fRunner(f, ft.Fn)
- <-f.signal
+ <-f.doneOrParallel
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
+ ran = ran || f.ranAnyLeaf
}
if !ran {
// There were no tests to run on this iteration.
@@ -592,12 +588,12 @@ func runFuzzing(deps testDeps, fuzzTests []InternalFuzzTarget) (ok bool) {
f := &F{
common: common{
- signal: make(chan bool),
- barrier: nil, // T.Parallel has no effect when fuzzing.
- name: testName,
- parent: &root,
- level: root.level + 1,
- chatty: root.chatty,
+ doneOrParallel: make(chan struct{}),
+ runParallel: nil, // T.Parallel has no effect when fuzzing.
+ name: testName,
+ parent: &root,
+ level: root.level + 1,
+ chatty: root.chatty,
},
fuzzContext: fctx,
testContext: tctx,
@@ -607,7 +603,7 @@ func runFuzzing(deps testDeps, fuzzTests []InternalFuzzTarget) (ok bool) {
f.chatty.Updatef(f.name, "=== RUN %s\n", f.name)
}
go fRunner(f, fuzzTest.Fn)
- <-f.signal
+ <-f.doneOrParallel
if f.chatty != nil {
f.chatty.Updatef(f.parent.name, "=== NAME %s\n", f.parent.name)
}
@@ -625,6 +621,12 @@ func runFuzzing(deps testDeps, fuzzTests []InternalFuzzTarget) (ok bool) {
// simplifications are made. We also require that F.Fuzz, F.Skip, or F.Fail is
// called.
func fRunner(f *F, fn func(*F)) {
+ // TODO(bcmills): This function has a lot of code and structure in common with
+ // tRunner. At some point it would probably be good to factor out the common
+ // parts to make the differences easier to spot.
+
+ returned := false
+
// When this goroutine is done, either because runtime.Goexit was called, a
// panic started, or fn returned normally, record the duration and send
// t.signal, indicating the fuzz test is done.
@@ -636,96 +638,93 @@ func fRunner(f *F, fn func(*F)) {
// Unfortunately, recovering here adds stack frames, but the location of
// the original panic should still be
// clear.
- 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 {
+
+ panicVal := recover()
+ if panicVal == nil && !f.skipped && !f.failed {
+ if !returned {
+ panicVal = errNilPanicOrGoexit
+ } else if !f.fuzzCalled {
f.Error("returned without calling F.Fuzz, F.Fail, or F.Skip")
}
}
+ if panicVal != nil {
+ // Mark the test as failed so that Cleanup functions can see its correct status.
+ f.Fail()
+ } else if f.runParallel != nil {
+ // Unblock inputs that called T.Parallel while running the seed corpus.
+ // This only affects fuzz tests run as normal tests.
+ // While fuzzing, T.Parallel has no effect, so f.parallelSubtests is empty
+ // and this is a no-op.
+
+ // Check for races before starting parallel subtests, so that if a
+ // parallel subtest *also* triggers a data race we will report the two
+ // races to the two tests and not attribute all of them to the subtest.
+ f.checkRaces()
+
+ close(f.runParallel)
+ f.parallelSubtests.Wait()
+ }
+
// Use a deferred call to ensure that we report that the test is
- // complete even if a cleanup function calls F.FailNow. See issue 41355.
- didPanic := false
+ // complete even if a cleanup function calls t.FailNow. See issue 41355.
defer func() {
- 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
+ cleanupPanic := recover()
+ if panicVal == nil {
+ panicVal = cleanupPanic
}
- }()
- // If we recovered a panic or inappropriate runtime.Goexit, fail the test,
- // flush the output log up to the root, then panic.
- 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 += time.Since(root.start)
- d := root.duration
- root.mu.Unlock()
- root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
+ // 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.
+ if panicVal != nil {
+ // Flush the output log up to the root before dying.
+ for root := &f.common; root.parent != nil; root = root.parent {
+ root.mu.Lock()
+ root.duration += time.Since(root.start)
+ d := root.duration
+ root.mu.Unlock()
+ root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
+
+ // Since the parent will never finish running, do its cleanup now.
+ // Run the cleanup in a fresh goroutine in case it calls runtime.Goexit,
+ // which we cannot recover.
+ cleanupDone := make(chan struct{})
+ go func() {
+ defer close(cleanupDone)
+ if r := root.parent.runCleanup(recoverAndReturnPanic); r != nil {
+ fmt.Fprintf(root.parent.w, "cleanup panicked with %v", r)
+ }
+ }()
+ <-cleanupDone
+ }
+ panic(panicVal)
}
- didPanic = true
- panic(err)
- }
- if err != nil {
- doPanic(err)
- }
- // No panic or inappropriate Goexit.
- f.duration += time.Since(f.start)
+ f.checkRaces()
+ f.duration += time.Since(f.start)
+ f.report()
- if len(f.sub) > 0 {
- // Unblock inputs that called T.Parallel while running the seed corpus.
- // This only affects fuzz tests 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.
- f.testContext.release()
- 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)
+ // Do not lock f.done to allow race detector to detect race in case
+ // the user does not appropriately synchronize a goroutine.
+ f.done = true
+ if f.parent != nil && !f.hasSub.Load() {
+ f.setRanLeaf()
}
- }
- // Report after all subtests have finished.
- f.report()
- f.done = true
- f.setRan()
- }()
- defer func() {
- if len(f.sub) == 0 {
- f.runCleanup(normalPanic)
- }
+ running.Delete(f.name)
+ close(f.doneOrParallel)
+ }()
+
+ f.runCleanup(normalPanic)
}()
+ // Run the actual fuzz function.
f.start = time.Now()
f.resetRaces()
fn(f)
// Code beyond this point will not be executed when FailNow or SkipNow
// is invoked.
- f.mu.Lock()
- f.finished = true
- f.mu.Unlock()
+ returned = true
}