Skip to content

testing: fix racy t.done usage by adding a separate race-canary field #67796

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/testing/fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ func fRunner(f *F, fn func(*F)) {

// Report after all subtests have finished.
f.report()
f.doneRaceCanary = true
f.done = true
f.setRan()
}()
Expand Down
41 changes: 25 additions & 16 deletions src/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,20 +593,21 @@ const maxStackLen = 50
// common holds the elements common between T and B and
// 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.
failed bool // Test or benchmark has failed.
skipped bool // Test or benchmark has been 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.
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.
failed bool // Test or benchmark has failed.
skipped bool // Test or benchmark has been skipped.
done bool // Test is finished and all subtests have completed.
doneRaceCanary bool // Shadows all interactions with done except test-completion to trigger a race on misbehavior.
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.

chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
bench bool // Whether the current test is a benchmark.
Expand Down Expand Up @@ -949,6 +950,7 @@ func (c *common) Fail() {
c.mu.Lock()
defer c.mu.Unlock()
// c.done needs to be locked to synchronize checks to c.done in parent tests.
_ = c.doneRaceCanary
if c.done {
panic("Fail in goroutine after " + c.name + " has completed")
}
Expand All @@ -960,6 +962,7 @@ func (c *common) Failed() bool {
c.mu.RLock()
defer c.mu.RUnlock()

_ = c.doneRaceCanary
if !c.done && int64(race.Errors()) > c.lastRaceErrors.Load() {
c.mu.RUnlock()
c.checkRaces()
Expand Down Expand Up @@ -1015,12 +1018,14 @@ func (c *common) log(s string) {
func (c *common) logDepth(s string, depth int) {
c.mu.Lock()
defer c.mu.Unlock()
_ = c.doneRaceCanary
if c.done {
// This test has already finished. Try and log this message
// with our parent. If we don't have a parent, panic.
for parent := c.parent; parent != nil; parent = parent.parent {
parent.mu.Lock()
defer parent.mu.Unlock()
_ = parent.doneRaceCanary
if !parent.done {
parent.output = append(parent.output, parent.decorate(s, depth+1)...)
return
Expand Down Expand Up @@ -1672,9 +1677,13 @@ func tRunner(t *T, fn func(t *T)) {
}
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.
// Do not lock around t.done's race canary to allow race detector to detect
// cases where the user does not appropriately synchronize a goroutine...
t.doneRaceCanary = true
t.mu.Lock()
// ...but do lock around t.done so after-done behavior is consistent.
t.done = true
t.mu.Unlock()
if t.parent != nil && !t.hasSub.Load() {
t.setRan()
}
Expand Down