diff options
Diffstat (limited to 'src/testing/testing.go')
| -rw-r--r-- | src/testing/testing.go | 402 |
1 files changed, 201 insertions, 201 deletions
diff --git a/src/testing/testing.go b/src/testing/testing.go index ed8b3630f1..96f71c89b9 100644 --- a/src/testing/testing.go +++ b/src/testing/testing.go @@ -480,7 +480,7 @@ var ( cpuList []int testlogFile *os.File - numFailed atomic.Uint32 // number of test failures + anyFailed atomic.Bool running sync.Map // map[string]time.Time of running, unpaused tests ) @@ -593,38 +593,40 @@ const maxStackLen = 50 // captures common methods such as Errorf. type common struct { mu sync.RWMutex // guards this group of fields - output []byte // Output generated by test or benchmark. - w io.Writer // For flushToParent. - ran bool // Test or benchmark (or one of its subtests) was executed. + output []byte // output generated by test or benchmark + w io.Writer // output to which flushToParent should write + ranAnyLeaf bool // Test or benchmark ran to completion without calling Run itself, or had a subtest that did so. failed bool // Test or benchmark has failed. skipped bool // Test or benchmark has been skipped. + goexiting bool // Test function is attempting to abort by goexit (implies failed || skipped). done bool // Test is finished and all subtests have completed. helperPCs map[uintptr]struct{} // functions to be skipped when writing file/line info helperNames map[string]struct{} // helperPCs converted to function names cleanups []func() // optional functions to be called at the end of the test - cleanupName string // Name of the cleanup function. - cleanupPc []uintptr // The stack trace at the point where Cleanup was called. - finished bool // Test function has completed. - inFuzzFn bool // Whether the fuzz target, if this is one, is running. + cleanupName string // name of the cleanup function currently running + cleanupPc []uintptr // stack trace at the point where Cleanup was called - chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set. - bench bool // Whether the current test is a benchmark. - hasSub atomic.Bool // whether there are sub-benchmarks. - cleanupStarted atomic.Bool // Registered cleanup callbacks have started to execute - runner string // Function name of tRunner running the test. - isParallel bool // Whether the test is parallel. + chatty *chattyPrinter // copy of chattyPrinter, if the chatty flag is set + bench bool // Current test is a benchmark. + runner string // function name of tRunner running the test + isParallel bool // Test is running in parallel. + inFuzzFn bool // Fuzz target, if this is one, is running. + inCleanup bool // Cleanup callbacks, if any, are running. - parent *common - level int // Nesting depth of test or benchmark. - creator []uintptr // If level > 0, the stack trace at the point where the parent called t.Run. - 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. 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. + parent *common + level int // nesting depth of test or benchmark + creator []uintptr // if level > 0, the stack trace at the point where the parent called t.Run + name string // name of test or benchmark + start time.Time // time test or benchmark started or resumed + duration time.Duration // time in the test function, excluding time blocked in t.Parallel + runParallel chan struct{} // Closed when parallel subtests may start. Nil when T.Parallel is not present (B) or not usable (when fuzzing). + doneOrParallel chan struct{} // Closed when the test is either blocked in Parallel or done running. - lastRaceErrors atomic.Int64 // Max value of race.Errors seen during the test or its subtests. + hasSub atomic.Bool // Test or benchmark has subtests or sub-benchmarks. + parallelSubtests sync.WaitGroup + runMu sync.Mutex // Held during calls to Run to prevent the total number of active subtests from exceeding the parallelism limit. + + lastRaceErrors atomic.Int64 // max value of race.Errors seen during the test or its subtests raceErrorLogged atomic.Bool tempDirMu sync.Mutex @@ -931,13 +933,13 @@ func (c *common) Name() string { return c.name } -func (c *common) setRan() { +func (c *common) setRanLeaf() { if c.parent != nil { - c.parent.setRan() + c.parent.setRanLeaf() } c.mu.Lock() defer c.mu.Unlock() - c.ran = true + c.ranAnyLeaf = true } // Fail marks the function as having failed but continues execution. @@ -952,6 +954,7 @@ func (c *common) Fail() { panic("Fail in goroutine after " + c.name + " has completed") } c.failed = true + anyFailed.Store(true) } // Failed reports whether the function has failed. @@ -1000,7 +1003,7 @@ func (c *common) FailNow() { // a top-of-stack deferred function now, we know that the send // only happens after any other stacked defers have completed. c.mu.Lock() - c.finished = true + c.goexiting = true c.mu.Unlock() runtime.Goexit() } @@ -1115,7 +1118,7 @@ func (c *common) SkipNow() { c.checkFuzzFn("SkipNow") c.mu.Lock() c.skipped = true - c.finished = true + c.goexiting = true c.mu.Unlock() runtime.Goexit() } @@ -1318,8 +1321,8 @@ const ( // If ph is recoverAndReturnPanic, it will catch panics, and return the // recovered value if any. func (c *common) runCleanup(ph panicHandling) (panicVal any) { - c.cleanupStarted.Store(true) - defer c.cleanupStarted.Store(false) + c.inCleanup = true + defer func() { c.inCleanup = false }() if ph == recoverAndReturnPanic { defer func() { @@ -1446,8 +1449,7 @@ func (t *T) Parallel() { if t.isEnvSet { panic("testing: t.Parallel called after t.Setenv; cannot set environment variables in parallel tests") } - t.isParallel = true - if t.parent.barrier == nil { + if t.parent.runParallel == nil { // T.Parallel has no effect when fuzzing. // Multiple processes may run in parallel, but only one input can run at a // time per process so we can attribute crashes to specific inputs. @@ -1460,7 +1462,7 @@ func (t *T) Parallel() { t.duration += time.Since(t.start) // Add to the list of tests to be released by the parent. - t.parent.sub = append(t.parent.sub, t) + t.parent.parallelSubtests.Add(1) // Report any races during execution of this test up to this point. // @@ -1479,9 +1481,19 @@ func (t *T) Parallel() { } running.Delete(t.name) - t.signal <- true // Release calling test. - <-t.parent.barrier // Wait for the parent test to complete. - t.context.waitParallel() + t.isParallel = true + + // Release the parent test to run. We can't just use parallelSem tokens for + // this because some callers (notably TestParallelSub) expect to be able to + // call Run from multiple goroutines and have those calls succeed. + // + // Instead, we close a channel to unblock the parent's call to Run, allowing + // it to resume. Then, we wait for it to finish and unblock its parallel + // subtests, and acquire a parallel run token so that we don't run too many of + // the subtests together all at once. + close(t.doneOrParallel) + <-t.parent.runParallel + t.context.acquireParallel() if t.chatty != nil { t.chatty.Updatef(t.name, "=== CONT %s\n", t.name) @@ -1538,19 +1550,13 @@ var errNilPanicOrGoexit = errors.New("test executed panic(nil) or runtime.Goexit func tRunner(t *T, fn func(t *T)) { t.runner = callerName(0) + returned := false // When this goroutine is done, either because fn(t) // returned normally or because a test failure triggered // a call to runtime.Goexit, record the duration and send // a signal saying that the test is done. defer func() { - t.checkRaces() - - // TODO(#61034): This is the wrong place for this check. - if t.Failed() { - numFailed.Add(1) - } - // Check if the test panicked or Goexited inappropriately. // // If this happens in a normal test, print output but continue panicking. @@ -1559,132 +1565,120 @@ func tRunner(t *T, fn func(t *T)) { // If this happens while fuzzing, recover from the panic and treat it like a // normal failure. It's important that the process keeps running in order to // find short inputs that cause panics. - err := recover() - signal := true - - t.mu.RLock() - finished := t.finished - t.mu.RUnlock() - if !finished && err == nil { - err = errNilPanicOrGoexit + panicVal := recover() + if !returned && !t.goexiting && panicVal == nil { + panicVal = errNilPanicOrGoexit for p := t.parent; p != nil; p = p.parent { p.mu.RLock() - finished = p.finished + pGoexiting := p.goexiting p.mu.RUnlock() - if finished { - if !t.isParallel { - t.Errorf("%v: subtest may have called FailNow on a parent test", err) - err = nil - } - signal = false + if pGoexiting { + t.Errorf("%v: subtest may have called FailNow on a parent test", panicVal) + panicVal = nil break } } } - if err != nil && t.context.isFuzzing { + if panicVal != nil && t.context.isFuzzing { prefix := "panic: " - if err == errNilPanicOrGoexit { + if panicVal == 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 + t.Errorf("%s%s\n%s\n", prefix, panicVal, string(debug.Stack())) + panicVal = nil + } + + if panicVal != nil { + // Mark the test as failed so that Cleanup functions can see its correct status. + t.Fail() + } else if t.runParallel != nil { + // Run parallel subtests. + + // 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. + t.checkRaces() + + if t.isParallel { + // Release our own parallel token first, so that it is available for + // subtests to acquire. + t.context.releaseParallel() + } + close(t.runParallel) + t.parallelSubtests.Wait() + if t.isParallel { + // Re-acquire a parallel token to limit concurrent cleanup. + t.context.acquireParallel() + } } // 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() { + cleanupPanic := recover() + if panicVal == nil { + panicVal = cleanupPanic + } + // 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 didPanic { - return - } - if err != nil { - panic(err) - } - running.Delete(t.name) - t.signal <- signal - }() + if panicVal != nil { + // Flush the output log up to the root before dying. + for root := &t.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)) - doPanic := func(err any) { - t.Fail() - if r := t.runCleanup(recoverAndReturnPanic); r != nil { - t.Logf("cleanup panicked with %v", r) - } - // Flush the output log up to the root before dying. - for root := &t.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)) - if r := root.parent.runCleanup(recoverAndReturnPanic); r != nil { - fmt.Fprintf(root.parent.w, "cleanup panicked with %v", r) + // 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) - } - t.duration += time.Since(t.start) + t.checkRaces() + t.duration += time.Since(t.start) + t.report() - if len(t.sub) > 0 { - // Run parallel subtests. - // Decrease the running count for this test. - t.context.release() - // Release the parallel subtests. - close(t.barrier) - // Wait for subtests to complete. - for _, sub := range t.sub { - <-sub.signal - } - cleanupStart := time.Now() - err := t.runCleanup(recoverAndReturnPanic) - t.duration += time.Since(cleanupStart) - if err != nil { - doPanic(err) + // Do not lock t.done to allow race detector to detect race in case + // the user does not appropriately synchronize a goroutine. + t.done = true + if t.parent != nil && !t.hasSub.Load() { + t.setRanLeaf() } - t.checkRaces() - if !t.isParallel { - // Reacquire the count for sequential tests. See comment in Run. - t.context.waitParallel() + + running.Delete(t.name) + if t.isParallel { + t.context.releaseParallel() + t.parent.parallelSubtests.Done() + } else { + close(t.doneOrParallel) } - } else if t.isParallel { - // Only release the count for this test if it was run as a parallel - // test. See comment in Run method. - t.context.release() - } - t.report() // Report after all subtests have finished. + }() - // Do not lock t.done to allow race detector to detect race in case - // the user does not appropriately synchronize a goroutine. - t.done = true - if t.parent != nil && !t.hasSub.Load() { - t.setRan() - } - }() - defer func() { - if len(t.sub) == 0 { - t.runCleanup(normalPanic) - } + t.runCleanup(normalPanic) }() + // Run the actual test function. t.start = time.Now() t.resetRaces() fn(t) - // code beyond here will not be executed when FailNow is invoked - t.mu.Lock() - t.finished = true - t.mu.Unlock() + // Code beyond this point will not be executed when FailNow or SkipNow + // is invoked. + returned = true } // Run runs f as a subtest of t called name. It runs f in a separate goroutine @@ -1694,7 +1688,7 @@ func tRunner(t *T, fn func(t *T)) { // Run may be called simultaneously from multiple goroutines, but all such calls // must return before the outer test function for t returns. func (t *T) Run(name string, f func(t *T)) bool { - if t.cleanupStarted.Load() { + if t.inCleanup { panic("testing: t.Run called during t.Cleanup") } @@ -1708,40 +1702,56 @@ func (t *T) Run(name string, f func(t *T)) bool { // continue walking the stack into the parent test. var pc [maxStackLen]uintptr n := runtime.Callers(2, pc[:]) - t = &T{ + sub := &T{ common: common{ - barrier: make(chan bool), - signal: make(chan bool, 1), - name: testName, - parent: &t.common, - level: t.level + 1, - creator: pc[:n], - chatty: t.chatty, + runParallel: make(chan struct{}), + doneOrParallel: make(chan struct{}), + name: testName, + parent: &t.common, + level: t.level + 1, + creator: pc[:n], + chatty: t.chatty, }, context: t.context, } - t.w = indenter{&t.common} + sub.w = indenter{&sub.common} - if t.chatty != nil { - t.chatty.Updatef(t.name, "=== RUN %s\n", t.name) + // Ensure that only one non-parallel subtest runs at a time so that we don't + // exceed the -parallel setting due to concurrent calls. + // (Run may be called concurrently even if the test is not marked parallel — + // see TestParallelSub.) + t.runMu.Lock() + defer t.runMu.Unlock() + + if t.isParallel { + // Release our parallelism token for the subtest to use + // for its own parallel subtests. + t.context.releaseParallel() + defer t.context.acquireParallel() } - running.Store(t.name, time.Now()) + + if sub.chatty != nil { + sub.chatty.Updatef(sub.name, "=== RUN %s\n", sub.name) + } + running.Store(sub.name, time.Now()) // Instead of reducing the running count of this test before calling the // tRunner and increasing it afterwards, we rely on tRunner keeping the // count correct. This ensures that a sequence of sequential tests runs // without being preempted, even when their parent is a parallel test. This // may especially reduce surprises if *parallel == 1. - go tRunner(t, f) - if !<-t.signal { - // At this point, it is likely that FailNow was called on one of the - // parent tests by one of the subtests. Continue aborting up the chain. + go tRunner(sub, f) + <-sub.doneOrParallel + if t.goexiting { + // The parent test (t) thinks it called runtime.Goexit, but here we are + // still running. It is likely that this subtest called FailNow or SkipNow + // on the t instead of sub, so propagate the Goexit to the parent goroutine. runtime.Goexit() } if t.chatty != nil && t.chatty.json { - t.chatty.Updatef(t.parent.name, "=== NAME %s\n", t.parent.name) + t.chatty.Updatef(t.name, "=== NAME %s\n", t.name) } - return !t.failed + return !sub.failed } // Deadline reports the time at which the test binary will have @@ -1765,53 +1775,43 @@ type testContext struct { // does not match). isFuzzing bool - mu sync.Mutex - - // Channel used to signal tests that are ready to be run in parallel. - startParallel chan bool - - // running is the number of tests currently running in parallel. - // This does not include tests that are waiting for subtests to complete. - running int - - // numWaiting is the number tests waiting to be run in parallel. - numWaiting int - - // maxParallel is a copy of the parallel flag. - maxParallel int + // parallelSem is a counting semaphore to limit concurrency of Parallel tests. + // It has a capacity equal to the parallel flag. + // Send a token to acquire; receive to release. + // Non-parallel tests do not require a token. + parallelSem chan token } +// A token is a semaphore token. +type token struct{} + +// newTestContext returns a new testContext with the given parallelism and matcher. func newTestContext(maxParallel int, m *matcher) *testContext { - return &testContext{ - match: m, - startParallel: make(chan bool), - maxParallel: maxParallel, - running: 1, // Set the count to 1 for the main (sequential) test. + tc := &testContext{ + match: m, + parallelSem: make(chan token, maxParallel), } + return tc } -func (c *testContext) waitParallel() { - c.mu.Lock() - if c.running < c.maxParallel { - c.running++ - c.mu.Unlock() - return - } - c.numWaiting++ - c.mu.Unlock() - <-c.startParallel +// acquireParallel blocks until it can obtain a semaphore token for running a +// parallel test. +func (c *testContext) acquireParallel() { + c.parallelSem <- token{} } -func (c *testContext) release() { - c.mu.Lock() - if c.numWaiting == 0 { - c.running-- - c.mu.Unlock() - return +// releaseParallel returns a semaphore token obtained by acquireParallel. +func (c *testContext) releaseParallel() { + select { + case <-c.parallelSem: + default: + panic("testing: internal error: no parallel token to release") } - c.numWaiting-- - c.mu.Unlock() - c.startParallel <- true // Pick a waiting test to be run. +} + +// running returns the number of semaphore tokens outstanding. +func (c *testContext) running() int { + return len(c.parallelSem) } // No one should be using func Main anymore. @@ -2132,9 +2132,9 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT ctx.deadline = deadline t := &T{ common: common{ - signal: make(chan bool, 1), - barrier: make(chan bool), - w: os.Stdout, + doneOrParallel: make(chan struct{}), + runParallel: make(chan struct{}), + w: os.Stdout, }, context: ctx, } @@ -2147,12 +2147,12 @@ func runTests(matchString func(pat, str string) (bool, error), tests []InternalT } }) select { - case <-t.signal: + case <-t.doneOrParallel: default: - panic("internal error: tRunner exited without sending on t.signal") + panic("internal error: tRunner exited without closing t.doneOrParallel") } ok = ok && !t.Failed() - ran = ran || t.ran + ran = ran || t.ranAnyLeaf } } return ran, ok @@ -2390,5 +2390,5 @@ func parseCpuList() { } func shouldFailFast() bool { - return *failFast && numFailed.Load() > 0 + return *failFast && anyFailed.Load() } |
