tstest: simplify goroutine leak tests

Use tb.Cleanup to simplify both the API and the implementation.

One behavior change: When the number of goroutines shrinks, don't log.
I've never found these logs to be useful, and they frequently add noise.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
This commit is contained in:
Josh Bleecher Snyder 2021-02-02 11:30:46 -08:00 committed by Josh Bleecher Snyder
parent 9a70789853
commit e8cd7bb66f
5 changed files with 34 additions and 82 deletions

View File

@ -16,9 +16,7 @@ import (
func TestReadWrite(t *testing.T) { func TestReadWrite(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
tstest.ResourceCheck(t)
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
buf := bytes.Buffer{} buf := bytes.Buffer{}
err := WriteMsg(&buf, []byte("Test string1")) err := WriteMsg(&buf, []byte("Test string1"))
@ -64,9 +62,7 @@ func TestReadWrite(t *testing.T) {
func TestClientServer(t *testing.T) { func TestClientServer(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
tstest.ResourceCheck(t)
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
b := &FakeBackend{} b := &FakeBackend{}
var bs *BackendServer var bs *BackendServer

View File

@ -12,8 +12,7 @@ import (
) )
func TestGetList(t *testing.T) { func TestGetList(t *testing.T) {
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
pl, err := GetList(nil) pl, err := GetList(nil)
if err != nil { if err != nil {
@ -26,8 +25,7 @@ func TestGetList(t *testing.T) {
} }
func TestIgnoreLocallyBoundPorts(t *testing.T) { func TestIgnoreLocallyBoundPorts(t *testing.T) {
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {

View File

@ -14,64 +14,30 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
type ResourceCheck struct { func ResourceCheck(tb testing.TB) {
startNumRoutines int startN, startStacks := goroutines()
startDump string tb.Cleanup(func() {
} if tb.Failed() {
// Something else went wrong.
func NewResourceCheck() *ResourceCheck { return
// NOTE(apenwarr): I'd rather not pre-generate a goroutine dump here. }
// However, it turns out to be tricky to debug when eg. the initial tb.Helper()
// goroutine count > the ending goroutine count, because of course // Goroutines might be still exiting.
// the missing ones are not in the final dump. Also, we have to
// render the profile as a string right away, because the
// pprof.Profile object doesn't stay stable over time. Every time
// you render the string, you might get a different answer.
r := &ResourceCheck{}
r.startNumRoutines, r.startDump = goroutineDump()
return r
}
func goroutineDump() (int, string) {
p := pprof.Lookup("goroutine")
b := &bytes.Buffer{}
p.WriteTo(b, 1)
return p.Count(), b.String()
}
func (r *ResourceCheck) Assert(t testing.TB) {
if t.Failed() {
// Something else went wrong.
// Assume that that is responsible for the leak
// and don't pile on a bunch of extra of output.
return
}
t.Helper()
want := r.startNumRoutines
// Some goroutines might be still exiting, so give them a chance
got := runtime.NumGoroutine()
if want != got {
_, dump := goroutineDump()
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
got = runtime.NumGoroutine() if runtime.NumGoroutine() <= startN {
if want == got { return
break
} }
time.Sleep(1 * time.Millisecond) time.Sleep(1 * time.Millisecond)
} }
endN, endStacks := goroutines()
// If the count is *still* wrong, that's a failure. tb.Logf("goroutine diff:\n%v\n", cmp.Diff(startStacks, endStacks))
if want != got { tb.Fatalf("goroutine count: expected %d, got %d\n", startN, endN)
t.Logf("goroutine diff:\n%v\n", cmp.Diff(r.startDump, dump)) })
t.Logf("goroutine count: expected %d, got %d\n", want, got) }
// Don't fail if there are *fewer* goroutines than
// expected. That just might be some leftover ones func goroutines() (int, []byte) {
// from the previous test, which are pretty hard to p := pprof.Lookup("goroutine")
// eliminate. b := new(bytes.Buffer)
if want < got { p.WriteTo(b, 1)
t.Fatalf("ResourceCheck: goroutine count: expected %d, got %d\n", want, got) return p.Count(), b.Bytes()
}
}
}
} }

View File

@ -331,8 +331,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
func TestNewConn(t *testing.T) { func TestNewConn(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
epCh := make(chan string, 16) epCh := make(chan string, 16)
epFunc := func(endpoints []string) { epFunc := func(endpoints []string) {
@ -398,8 +397,7 @@ func pickPort(t testing.TB) uint16 {
func TestDerpIPConstant(t *testing.T) { func TestDerpIPConstant(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
if DerpMagicIP != derpMagicIP.String() { if DerpMagicIP != derpMagicIP.String() {
t.Errorf("str %q != IP %v", DerpMagicIP, derpMagicIP) t.Errorf("str %q != IP %v", DerpMagicIP, derpMagicIP)
@ -411,8 +409,7 @@ func TestDerpIPConstant(t *testing.T) {
func TestPickDERPFallback(t *testing.T) { func TestPickDERPFallback(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
c := newConn() c := newConn()
c.derpMap = derpmap.Prod() c.derpMap = derpmap.Prod()
@ -519,8 +516,7 @@ func parseCIDR(t *testing.T, addr string) netaddr.IPPrefix {
// -count=10000 to be sure. // -count=10000 to be sure.
func TestDeviceStartStop(t *testing.T) { func TestDeviceStartStop(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
conn, err := NewConn(Options{ conn, err := NewConn(Options{
EndpointsFunc: func(eps []string) {}, EndpointsFunc: func(eps []string) {},
@ -838,8 +834,7 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
// get exercised. // get exercised.
func testActiveDiscovery(t *testing.T, d *devices) { func testActiveDiscovery(t *testing.T, d *devices) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
tlogf, setT := makeNestable(t) tlogf, setT := makeNestable(t)
setT(t) setT(t)
@ -900,8 +895,7 @@ func testActiveDiscovery(t *testing.T, d *devices) {
func testTwoDevicePing(t *testing.T, d *devices) { func testTwoDevicePing(t *testing.T, d *devices) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
// This gets reassigned inside every test, so that the connections // This gets reassigned inside every test, so that the connections
// all log using the "current" t.Logf function. Sigh. // all log using the "current" t.Logf function. Sigh.
@ -1145,8 +1139,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
// TestAddrSet tests addrSet appendDests and updateDst. // TestAddrSet tests addrSet appendDests and updateDst.
func TestAddrSet(t *testing.T) { func TestAddrSet(t *testing.T) {
tstest.PanicOnLog() tstest.PanicOnLog()
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
mustIPPortPtr := func(s string) *netaddr.IPPort { mustIPPortPtr := func(s string) *netaddr.IPPort {
t.Helper() t.Helper()

View File

@ -275,8 +275,7 @@ func TestResolveReverse(t *testing.T) {
} }
func TestDelegate(t *testing.T) { func TestDelegate(t *testing.T) {
rc := tstest.NewResourceCheck() tstest.ResourceCheck(t)
defer rc.Assert(t)
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site.")) dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN) dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN)