diff --git a/internal/jsonrpc2/jsonrpc2_test.go b/internal/jsonrpc2/jsonrpc2_test.go index 072f1d5fa2..e586009b57 100644 --- a/internal/jsonrpc2/jsonrpc2_test.go +++ b/internal/jsonrpc2/jsonrpc2_test.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/internal/event/export/eventtest" "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/stack/stacktest" ) var logRPC = flag.Bool("logrpc", false, "Enable jsonrpc2 communication logging") @@ -62,6 +63,7 @@ func (test *callTest) verifyResults(t *testing.T, results interface{}) { } func TestCall(t *testing.T) { + stacktest.NoLeak(t) ctx := eventtest.NewContext(context.Background(), t) for _, headers := range []bool{false, true} { name := "Plain" diff --git a/internal/jsonrpc2/serve_test.go b/internal/jsonrpc2/serve_test.go index 1b95610e0f..c9c1fbd1c6 100644 --- a/internal/jsonrpc2/serve_test.go +++ b/internal/jsonrpc2/serve_test.go @@ -10,9 +10,12 @@ import ( "sync" "testing" "time" + + "golang.org/x/tools/internal/stack/stacktest" ) func TestIdleTimeout(t *testing.T) { + stacktest.NoLeak(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() diff --git a/internal/stack/stacktest/stacktest.go b/internal/stack/stacktest/stacktest.go new file mode 100644 index 0000000000..e23f03e036 --- /dev/null +++ b/internal/stack/stacktest/stacktest.go @@ -0,0 +1,50 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package stacktest + +import ( + "testing" + "time" + + "golang.org/x/tools/internal/stack" +) + +//this is only needed to support pre 1.14 when testing.TB did not have Cleanup +type withCleanup interface { + Cleanup(func()) +} + +// the maximum amount of time to wait for goroutines to clean themselves up. +const maxWait = time.Second + +// NoLeak checks that a test (or benchmark) does not leak any goroutines. +func NoLeak(t testing.TB) { + c, ok := t.(withCleanup) + if !ok { + return + } + before := stack.Capture() + c.Cleanup(func() { + var delta stack.Delta + start := time.Now() + delay := time.Millisecond + for { + after := stack.Capture() + delta = stack.Diff(before, after) + if len(delta.After) == 0 { + // no leaks + return + } + if time.Since(start) > maxWait { + break + } + time.Sleep(delay) + delay *= 2 + } + // it's been long enough, and leaks are still present + summary := stack.Summarize(delta.After) + t.Errorf("goroutine leak detected:\n%+v", summary) + }) +}