Skip to content

Commit

Permalink
testing: add T.Context method
Browse files Browse the repository at this point in the history
From the doc comment:

Context returns the context for the current test or benchmark.
The context is cancelled when the test or benchmark finishes.
A goroutine started during a test or benchmark can wait for the
context's Done channel to become readable as a signal that the
test or benchmark is over, so that the goroutine can exit.

Fixes #16221.
Fixes #17552.

Change-Id: I657df946be2c90048cc74615436c77c7d9d1226c
Reviewed-on: https://go-review.googlesource.com/31724
Reviewed-by: Rob Pike <r@golang.org>
  • Loading branch information
bradfitz authored and rsc committed Nov 3, 2016
1 parent 606f81e commit 26827bc
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 34 deletions.
2 changes: 1 addition & 1 deletion src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ var pkgDeps = map[string][]string{
"runtime/trace": {"L0"},
"text/tabwriter": {"L2"},

"testing": {"L2", "flag", "fmt", "internal/race", "os", "runtime/debug", "runtime/pprof", "runtime/trace", "time"},
"testing": {"L2", "context", "flag", "fmt", "internal/race", "os", "runtime/debug", "runtime/pprof", "runtime/trace", "time"},
"testing/iotest": {"L2", "log"},
"testing/quick": {"L2", "flag", "fmt", "reflect"},
"internal/testenv": {"L2", "OS", "flag", "testing", "syscall"},
Expand Down
4 changes: 4 additions & 0 deletions src/testing/benchmark.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package testing

import (
"context"
"flag"
"fmt"
"internal/race"
Expand Down Expand Up @@ -127,6 +128,9 @@ func (b *B) nsPerOp() int64 {

// runN runs a single benchmark for the specified number of iterations.
func (b *B) runN(n int) {
b.ctx, b.cancel = context.WithCancel(b.parentContext())
defer b.cancel()

benchmarkLock.Lock()
defer benchmarkLock.Unlock()
// Try to get a comparable environment for each run
Expand Down
51 changes: 29 additions & 22 deletions src/testing/sub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package testing

import (
"bytes"
"context"
"regexp"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -277,28 +278,33 @@ func TestTRun(t *T) {
ok: true,
maxPar: 4,
f: func(t *T) {
t.Parallel()
for i := 0; i < 12; i++ {
t.Run("a", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("b", func(t *T) {
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("c", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
t.Run("d1", func(t *T) {})
t.Run("d2", func(t *T) {})
t.Run("d3", func(t *T) {})
t.Run("d4", func(t *T) {})
})
}
})
}
})
}
// t.Parallel doesn't work in the pseudo-T we start with:
// it leaks a goroutine.
// Call t.Run to get a real one.
t.Run("X", func(t *T) {
t.Parallel()
for i := 0; i < 12; i++ {
t.Run("a", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("b", func(t *T) {
time.Sleep(time.Nanosecond)
for i := 0; i < 12; i++ {
t.Run("c", func(t *T) {
t.Parallel()
time.Sleep(time.Nanosecond)
t.Run("d1", func(t *T) {})
t.Run("d2", func(t *T) {})
t.Run("d3", func(t *T) {})
t.Run("d4", func(t *T) {})
})
}
})
}
})
}
})
},
}, {
desc: "skip output",
Expand Down Expand Up @@ -341,6 +347,7 @@ func TestTRun(t *T) {
},
context: ctx,
}
root.ctx, root.cancel = context.WithCancel(context.Background())
ok := root.Run(tc.desc, tc.f)
ctx.release()

Expand Down
35 changes: 29 additions & 6 deletions src/testing/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ package testing

import (
"bytes"
"context"
"errors"
"flag"
"fmt"
Expand Down Expand Up @@ -261,12 +262,14 @@ type common struct {
mu sync.RWMutex // guards output, failed, and done.
output []byte // Output generated by test or benchmark.
w io.Writer // For flushToParent.
chatty bool // A copy of the chatty flag.
ran bool // Test or benchmark (or one of its subtests) was executed.
failed bool // Test or benchmark has failed.
skipped bool // Test of benchmark has been skipped.
finished bool // Test function has completed.
done bool // Test is finished and all subtests have completed.
ctx context.Context
cancel context.CancelFunc
chatty bool // A copy of the chatty flag.
ran bool // Test or benchmark (or one of its subtests) was executed.
failed bool // Test or benchmark has failed.
skipped bool // Test of benchmark has been skipped.
finished bool // Test function has completed.
done bool // Test is finished and all subtests have completed.
hasSub bool
raceErrors int // number of races detected during test

Expand All @@ -280,6 +283,13 @@ type common struct {
sub []*T // Queue of subtests to be run in parallel.
}

func (c *common) parentContext() context.Context {
if c == nil || c.parent == nil || c.parent.ctx == nil {
return context.Background()
}
return c.parent.ctx
}

// Short reports whether the -test.short flag is set.
func Short() bool {
return *short
Expand Down Expand Up @@ -376,6 +386,7 @@ func fmtDuration(d time.Duration) string {

// TB is the interface common to T and B.
type TB interface {
Context() context.Context
Error(args ...interface{})
Errorf(format string, args ...interface{})
Fail()
Expand Down Expand Up @@ -423,6 +434,15 @@ func (c *common) Name() string {
return c.name
}

// Context returns the context for the current test or benchmark.
// The context is cancelled when the test or benchmark finishes.
// A goroutine started during a test or benchmark can wait for the
// context's Done channel to become readable as a signal that the
// test or benchmark is over, so that the goroutine can exit.
func (c *common) Context() context.Context {
return c.ctx
}

func (c *common) setRan() {
if c.parent != nil {
c.parent.setRan()
Expand Down Expand Up @@ -599,6 +619,9 @@ type InternalTest struct {
}

func tRunner(t *T, fn func(t *T)) {
t.ctx, t.cancel = context.WithCancel(t.parentContext())
defer t.cancel()

// 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
Expand Down
38 changes: 33 additions & 5 deletions src/testing/testing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,42 @@
package testing_test

import (
"fmt"
"os"
"runtime"
"testing"
"time"
)

// This is exactly what a test would do without a TestMain.
// It's here only so that there is at least one package in the
// standard library with a TestMain, so that code is executed.

func TestMain(m *testing.M) {
os.Exit(m.Run())
g0 := runtime.NumGoroutine()

code := m.Run()
if code != 0 {
os.Exit(code)
}

// Check that there are no goroutines left behind.
t0 := time.Now()
stacks := make([]byte, 1<<20)
for {
g1 := runtime.NumGoroutine()
if g1 == g0 {
return
}
stacks = stacks[:runtime.Stack(stacks, true)]
time.Sleep(50 * time.Millisecond)
if time.Since(t0) > 2*time.Second {
fmt.Fprintf(os.Stderr, "Unexpected leftover goroutines detected: %v -> %v\n%s\n", g0, g1, stacks)
os.Exit(1)
}
}
}

func TestContextCancel(t *testing.T) {
ctx := t.Context()
// Tests we don't leak this goroutine:
go func() {
<-ctx.Done()
}()
}

0 comments on commit 26827bc

Please sign in to comment.