package leaktest import ( "context" "errors" "fmt" "sync" "testing" "time" ) type testReporter struct { failed bool msg string } func (tr *testReporter) Errorf(format string, args ...interface{}) { tr.failed = true tr.msg = fmt.Sprintf(format, args...) } var leakyFuncs = []func(){ // Infinite for loop func() { for { time.Sleep(time.Second) } }, // Select on a channel not referenced by other goroutines. func() { c := make(chan struct{}) <-c }, // Blocked select on channels not referenced by other goroutines. func() { c := make(chan struct{}) c2 := make(chan struct{}) select { case <-c: case c2 <- struct{}{}: } }, // Blocking wait on sync.Mutex that isn't referenced by other goroutines. func() { var mu sync.Mutex mu.Lock() mu.Lock() }, // Blocking wait on sync.RWMutex that isn't referenced by other goroutines. func() { var mu sync.RWMutex mu.RLock() mu.Lock() }, func() { var mu sync.Mutex mu.Lock() c := sync.NewCond(&mu) c.Wait() }, } func TestCheck(t *testing.T) { // this works because the running goroutine is left running at the // start of the next test case - so the previous leaks don't affect the // check for the next one for i, fn := range leakyFuncs { checker := &testReporter{} snapshot := CheckTimeout(checker, time.Second) go fn() snapshot() if !checker.failed { t.Errorf("didn't catch sleeping goroutine, test #%d", i) } } } // TestSlowTest verifies that the timeout works on slow tests: it should // be based on time after the test finishes rather than time after the test's // start. func TestSlowTest(t *testing.T) { defer CheckTimeout(t, 1000 * time.Millisecond)() go time.Sleep(1500 * time.Millisecond) time.Sleep(750 * time.Millisecond) } func TestEmptyLeak(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() defer CheckContext(ctx, t)() time.Sleep(time.Second) } // TestChangingStackTrace validates that a change in a preexisting goroutine's // stack is not detected as a leaked goroutine. func TestChangingStackTrace(t *testing.T) { started := make(chan struct{}) c1 := make(chan struct{}) c2 := make(chan struct{}) defer close(c2) go func() { close(started) <-c1 <-c2 }() <-started func() { defer CheckTimeout(t, time.Second)() close(c1) }() } func TestInterestingGoroutine(t *testing.T) { s := "goroutine 123 [running]:\nmain.main()" gr, err := interestingGoroutine(s) if err != nil { t.Errorf("unexpected error: %s", err) } if gr.id != 123 { t.Errorf("goroutine id = %d; want %d", gr.id, 123) } if gr.stack != s { t.Errorf("goroutine stack = %q; want %q", gr.stack, s) } stacks := []struct { stack string err error }{ { stack: "goroutine 123 [running]:", err: errors.New(`error parsing stack: "goroutine 123 [running]:"`), }, { stack: "goroutine 123 [running]:\ntesting.RunTests", err: nil, }, { stack: "goroutine 123 [running]:\nfoo\nbar\nruntime.goexit\nbaz", err: nil, }, { stack: "goroutine 123:\nmain.main()", err: errors.New(`error parsing stack header: "goroutine 123:"`), }, { stack: "goroutine NaN [running]:\nmain.main()", err: errors.New(`error parsing goroutine id: strconv.ParseUint: parsing "NaN": invalid syntax`), }, } for i, s := range stacks { gr, err := interestingGoroutine(s.stack) if s.err == nil && err != nil { t.Errorf("%d: error = %v; want nil", i, err) } else if s.err != nil && (err == nil || err.Error() != s.err.Error()) { t.Errorf("%d: error = %v; want %s", i, err, s.err) } if gr != nil { t.Errorf("%d: gr = %v; want nil", i, gr) } } }