...

Source file src/go.etcd.io/etcd/client/pkg/v3/testutil/leak.go

Documentation: go.etcd.io/etcd/client/pkg/v3/testutil

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package testutil
     6  
     7  import (
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"regexp"
    12  	"runtime"
    13  	"sort"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  )
    18  
    19  // TODO: Replace with https://github.com/uber-go/goleak.
    20  
    21  /*
    22  CheckLeakedGoroutine verifies tests do not leave any leaky
    23  goroutines. It returns true when there are goroutines still
    24  running(leaking) after all tests.
    25  
    26  	import "go.etcd.io/etcd/client/pkg/v3/testutil"
    27  
    28  	func TestMain(m *testing.M) {
    29  		testutil.MustTestMainWithLeakDetection(m)
    30  	}
    31  
    32  	func TestSample(t *testing.T) {
    33  		RegisterLeakDetection(t)
    34  		...
    35  	}
    36  */
    37  func CheckLeakedGoroutine() bool {
    38  	gs := interestingGoroutines()
    39  	if len(gs) == 0 {
    40  		return false
    41  	}
    42  
    43  	stackCount := make(map[string]int)
    44  	re := regexp.MustCompile(`\(0[0-9a-fx, ]*\)`)
    45  	for _, g := range gs {
    46  		// strip out pointer arguments in first function of stack dump
    47  		normalized := string(re.ReplaceAll([]byte(g), []byte("(...)")))
    48  		stackCount[normalized]++
    49  	}
    50  
    51  	fmt.Fprintf(os.Stderr, "Unexpected goroutines running after all test(s).\n")
    52  	for stack, count := range stackCount {
    53  		fmt.Fprintf(os.Stderr, "%d instances of:\n%s\n", count, stack)
    54  	}
    55  	return true
    56  }
    57  
    58  // CheckAfterTest returns an error if AfterTest would fail with an error.
    59  // Waits for go-routines shutdown for 'd'.
    60  func CheckAfterTest(d time.Duration) error {
    61  	http.DefaultTransport.(*http.Transport).CloseIdleConnections()
    62  	var bad string
    63  	// Presence of these goroutines causes immediate test failure.
    64  	badSubstring := map[string]string{
    65  		").writeLoop(": "a Transport",
    66  		"created by net/http/httptest.(*Server).Start": "an httptest.Server",
    67  		"timeoutHandler":        "a TimeoutHandler",
    68  		"net.(*netFD).connect(": "a timing out dial",
    69  		").noteClientGone(":     "a closenotifier sender",
    70  		").readLoop(":           "a Transport",
    71  		".grpc":                 "a gRPC resource",
    72  		").sendCloseSubstream(": "a stream closing routine",
    73  	}
    74  
    75  	var stacks string
    76  	begin := time.Now()
    77  	for time.Since(begin) < d {
    78  		bad = ""
    79  		goroutines := interestingGoroutines()
    80  		if len(goroutines) == 0 {
    81  			return nil
    82  		}
    83  		stacks = strings.Join(goroutines, "\n\n")
    84  
    85  		for substr, what := range badSubstring {
    86  			if strings.Contains(stacks, substr) {
    87  				bad = what
    88  			}
    89  		}
    90  		// Undesired goroutines found, but goroutines might just still be
    91  		// shutting down, so give it some time.
    92  		runtime.Gosched()
    93  		time.Sleep(50 * time.Millisecond)
    94  	}
    95  	return fmt.Errorf("appears to have leaked %s:\n%s", bad, stacks)
    96  }
    97  
    98  // RegisterLeakDetection is a convenient way to register before-and-after code to a test.
    99  // If you execute RegisterLeakDetection, you don't need to explicitly register AfterTest.
   100  func RegisterLeakDetection(t TB) {
   101  	if err := CheckAfterTest(10 * time.Millisecond); err != nil {
   102  		t.Skip("Found leaked goroutined BEFORE test", err)
   103  		return
   104  	}
   105  	t.Cleanup(func() {
   106  		afterTest(t)
   107  	})
   108  }
   109  
   110  // afterTest is meant to run in a defer that executes after a test completes.
   111  // It will detect common goroutine leaks, retrying in case there are goroutines
   112  // not synchronously torn down, and fail the test if any goroutines are stuck.
   113  func afterTest(t TB) {
   114  	// If test-failed the leaked goroutines list is hidding the real
   115  	// source of problem.
   116  	if !t.Failed() {
   117  		if err := CheckAfterTest(1 * time.Second); err != nil {
   118  			t.Errorf("Test %v", err)
   119  		}
   120  	}
   121  }
   122  
   123  func interestingGoroutines() (gs []string) {
   124  	buf := make([]byte, 2<<20)
   125  	buf = buf[:runtime.Stack(buf, true)]
   126  	for _, g := range strings.Split(string(buf), "\n\n") {
   127  		sl := strings.SplitN(g, "\n", 2)
   128  		if len(sl) != 2 {
   129  			continue
   130  		}
   131  		stack := strings.TrimSpace(sl[1])
   132  		if stack == "" ||
   133  			strings.Contains(stack, "sync.(*WaitGroup).Done") ||
   134  			strings.Contains(stack, "os.(*file).close") ||
   135  			strings.Contains(stack, "os.(*Process).Release") ||
   136  			strings.Contains(stack, "created by os/signal.init") ||
   137  			strings.Contains(stack, "runtime/panic.go") ||
   138  			strings.Contains(stack, "created by testing.RunTests") ||
   139  			strings.Contains(stack, "created by testing.runTests") ||
   140  			strings.Contains(stack, "created by testing.(*T).Run") ||
   141  			strings.Contains(stack, "testing.Main(") ||
   142  			strings.Contains(stack, "runtime.goexit") ||
   143  			strings.Contains(stack, "go.etcd.io/etcd/client/pkg/v3/testutil.interestingGoroutines") ||
   144  			strings.Contains(stack, "go.etcd.io/etcd/client/pkg/v3/logutil.(*MergeLogger).outputLoop") ||
   145  			strings.Contains(stack, "github.com/golang/glog.(*loggingT).flushDaemon") ||
   146  			strings.Contains(stack, "created by runtime.gc") ||
   147  			strings.Contains(stack, "created by text/template/parse.lex") ||
   148  			strings.Contains(stack, "runtime.MHeap_Scavenger") ||
   149  			strings.Contains(stack, "rcrypto/internal/boring.(*PublicKeyRSA).finalize") ||
   150  			strings.Contains(stack, "net.(*netFD).Close(") ||
   151  			strings.Contains(stack, "testing.(*T).Run") {
   152  			continue
   153  		}
   154  		gs = append(gs, stack)
   155  	}
   156  	sort.Strings(gs)
   157  	return gs
   158  }
   159  
   160  func MustCheckLeakedGoroutine() {
   161  	http.DefaultTransport.(*http.Transport).CloseIdleConnections()
   162  
   163  	CheckAfterTest(5 * time.Second)
   164  
   165  	// Let the other goroutines finalize.
   166  	runtime.Gosched()
   167  
   168  	if CheckLeakedGoroutine() {
   169  		os.Exit(1)
   170  	}
   171  }
   172  
   173  // MustTestMainWithLeakDetection expands standard m.Run with leaked
   174  // goroutines detection.
   175  func MustTestMainWithLeakDetection(m *testing.M) {
   176  	v := m.Run()
   177  	if v == 0 {
   178  		MustCheckLeakedGoroutine()
   179  	}
   180  	os.Exit(v)
   181  }
   182  

View as plain text