Skip to content

Commit 7dad533

Browse files
authored
Use context.Context for configurable timeouts (#12)
* use context.Context for timeouts * don't support go pre 1.7 * readme update * preserve sleep between check behavior
1 parent 0db74e8 commit 7dad533

File tree

4 files changed

+77
-27
lines changed

4 files changed

+77
-27
lines changed

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
language: go
22
go:
3-
- 1.5.3
4-
- 1.6.3
53
- 1.7
4+
- 1.8
65
- tip
76

87
script:

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
Leaktest [![Build Status](https://travis-ci.org/fortytw2/leaktest.svg?branch=master)](https://travis-ci.org/fortytw2/leaktest)
1+
Leaktest [![Build Status](https://travis-ci.org/fortytw2/leaktest.svg?branch=master)](https://travis-ci.org/fortytw2/leaktest) [![codecov](https://codecov.io/gh/fortytw2/leaktest/branch/master/graph/badge.svg)](https://codecov.io/gh/fortytw2/leaktest)
22
------
33

4-
Refactored, tested variant of the goroutine leak detector found in both `net/http` tests and the `cockroachdb`
5-
source tree.
4+
Refactored, tested variant of the goroutine leak detector found in both
5+
`net/http` tests and the `cockroachdb` source tree.
66

77
Takes a snapshot of running goroutines at the start of a test, and at the end -
88
compares the two and *voila*. Ignores runtime/sys goroutines. Doesn't play nice
99
with `t.Parallel()` right now, but there are plans to do so.
1010

1111
### Installation
1212

13+
Go 1.7+
14+
1315
```
1416
go get -u github.com/fortytw2/leaktest
1517
```
1618

19+
Go 1.5/1.6 need to use the tag `v1.0.0`, as newer versions depend on
20+
`context.Context`.
21+
1722
### Example
1823

19-
This test fails, because it leaks a goroutine :o
24+
These tests fail, because they leak a goroutine
2025

2126
```go
27+
// Default "Check" will poll for 5 seconds to check that all
28+
// goroutines are cleaned up
2229
func TestPool(t *testing.T) {
2330
defer leaktest.Check(t)()
2431

@@ -28,6 +35,29 @@ func TestPool(t *testing.T) {
2835
}
2936
}()
3037
}
38+
39+
// Helper function to timeout after X duration
40+
func TestPoolTimeout(t *testing.T) {
41+
defer leaktest.CheckTimeout(t, time.Second)()
42+
43+
go func() {
44+
for {
45+
time.Sleep(time.Second)
46+
}
47+
}()
48+
}
49+
50+
// Use Go 1.7+ context.Context for cancellation
51+
func TestPoolContext(t *testing.T) {
52+
ctx, _ := context.WithTimeout(context.Background(), time.Second)
53+
defer leaktest.CheckContext(ctx, t)()
54+
55+
go func() {
56+
for {
57+
time.Sleep(time.Second)
58+
}
59+
}()
60+
}
3161
```
3262

3363

leaktest.go

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
package leaktest
1111

1212
import (
13+
"context"
1314
"runtime"
1415
"sort"
1516
"strings"
@@ -59,34 +60,52 @@ type ErrorReporter interface {
5960

6061
// Check snapshots the currently-running goroutines and returns a
6162
// function to be run at the end of tests to see whether any
62-
// goroutines leaked.
63+
// goroutines leaked, waiting up to 5 seconds in error conditions
6364
func Check(t ErrorReporter) func() {
65+
return CheckTimeout(t, 5*time.Second)
66+
}
67+
68+
// CheckTimeout is the same as Check, but with a configurable timeout
69+
func CheckTimeout(t ErrorReporter, dur time.Duration) func() {
70+
ctx, cancel := context.WithTimeout(context.Background(), dur)
71+
fn := CheckContext(ctx, t)
72+
return func() {
73+
fn()
74+
cancel()
75+
}
76+
}
77+
78+
// CheckContext is the same as Check, but uses a context.Context for
79+
// cancellation and timeout control
80+
func CheckContext(ctx context.Context, t ErrorReporter) func() {
6481
orig := map[string]bool{}
6582
for _, g := range interestingGoroutines() {
6683
orig[g] = true
6784
}
6885
return func() {
69-
// Loop, waiting for goroutines to shut down.
70-
// Wait up to 5 seconds, but finish as quickly as possible.
71-
deadline := time.Now().Add(5 * time.Second)
86+
var leaked []string
7287
for {
73-
var leaked []string
74-
for _, g := range interestingGoroutines() {
75-
if !orig[g] {
76-
leaked = append(leaked, g)
88+
select {
89+
case <-ctx.Done():
90+
t.Errorf("leaktest: timed out checking goroutines")
91+
default:
92+
leaked = make([]string, 0)
93+
for _, g := range interestingGoroutines() {
94+
if !orig[g] {
95+
leaked = append(leaked, g)
96+
}
7797
}
78-
}
79-
if len(leaked) == 0 {
80-
return
81-
}
82-
if time.Now().Before(deadline) {
83-
time.Sleep(50 * time.Millisecond)
98+
if len(leaked) == 0 {
99+
return
100+
}
101+
// don't spin needlessly
102+
time.Sleep(time.Millisecond * 50)
84103
continue
85104
}
86-
for _, g := range leaked {
87-
t.Errorf("Leaked goroutine: %v", g)
88-
}
89-
return
105+
break
106+
}
107+
for _, g := range leaked {
108+
t.Errorf("leaktest: leaked goroutine: %v", g)
90109
}
91110
}
92111
}

leaktest_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package leaktest
22

33
import (
4+
"context"
45
"fmt"
56
"sync"
67
"testing"
@@ -61,13 +62,12 @@ var leakyFuncs = []func(){
6162
}
6263

6364
func TestCheck(t *testing.T) {
64-
6565
// this works because the running goroutine is left running at the
6666
// start of the next test case - so the previous leaks don't affect the
6767
// check for the next one
6868
for i, fn := range leakyFuncs {
6969
checker := &testReporter{}
70-
snapshot := Check(checker)
70+
snapshot := CheckTimeout(checker, time.Second)
7171
go fn()
7272

7373
snapshot()
@@ -78,6 +78,8 @@ func TestCheck(t *testing.T) {
7878
}
7979

8080
func TestEmptyLeak(t *testing.T) {
81-
defer Check(t)()
81+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
82+
defer cancel()
83+
defer CheckContext(ctx, t)()
8284
time.Sleep(time.Second)
8385
}

0 commit comments

Comments
 (0)