go.etcd.io/etcd@v3.3.27+incompatible/pkg/testutil/leak.go (about)

     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  /*
    20  CheckLeakedGoroutine verifies tests do not leave any leaky
    21  goroutines. It returns true when there are goroutines still
    22  running(leaking) after all tests.
    23  
    24  	import "github.com/coreos/etcd/pkg/testutil"
    25  
    26  	func TestMain(m *testing.M) {
    27  		v := m.Run()
    28  		if v == 0 && testutil.CheckLeakedGoroutine() {
    29  			os.Exit(1)
    30  		}
    31  		os.Exit(v)
    32  	}
    33  
    34  	func TestSample(t *testing.T) {
    35  		defer testutil.AfterTest(t)
    36  		...
    37  	}
    38  
    39  */
    40  func CheckLeakedGoroutine() bool {
    41  	if testing.Short() {
    42  		// not counting goroutines for leakage in -short mode
    43  		return false
    44  	}
    45  	gs := interestingGoroutines()
    46  	if len(gs) == 0 {
    47  		return false
    48  	}
    49  
    50  	stackCount := make(map[string]int)
    51  	re := regexp.MustCompile(`\(0[0-9a-fx, ]*\)`)
    52  	for _, g := range gs {
    53  		// strip out pointer arguments in first function of stack dump
    54  		normalized := string(re.ReplaceAll([]byte(g), []byte("(...)")))
    55  		stackCount[normalized]++
    56  	}
    57  
    58  	fmt.Fprintf(os.Stderr, "Too many goroutines running after all test(s).\n")
    59  	for stack, count := range stackCount {
    60  		fmt.Fprintf(os.Stderr, "%d instances of:\n%s\n", count, stack)
    61  	}
    62  	return true
    63  }
    64  
    65  // CheckAfterTest returns an error if AfterTest would fail with an error.
    66  func CheckAfterTest(d time.Duration) error {
    67  	http.DefaultTransport.(*http.Transport).CloseIdleConnections()
    68  	if testing.Short() {
    69  		return nil
    70  	}
    71  	var bad string
    72  	badSubstring := map[string]string{
    73  		").writeLoop(": "a Transport",
    74  		"created by net/http/httptest.(*Server).Start": "an httptest.Server",
    75  		"timeoutHandler":        "a TimeoutHandler",
    76  		"net.(*netFD).connect(": "a timing out dial",
    77  		").noteClientGone(":     "a closenotifier sender",
    78  		").readLoop(":           "a Transport",
    79  		".grpc":                 "a gRPC resource",
    80  	}
    81  
    82  	var stacks string
    83  	begin := time.Now()
    84  	for time.Since(begin) < d {
    85  		bad = ""
    86  		stacks = strings.Join(interestingGoroutines(), "\n\n")
    87  		for substr, what := range badSubstring {
    88  			if strings.Contains(stacks, substr) {
    89  				bad = what
    90  			}
    91  		}
    92  		if bad == "" {
    93  			return nil
    94  		}
    95  		// Bad stuff found, but goroutines might just still be
    96  		// shutting down, so give it some time.
    97  		time.Sleep(50 * time.Millisecond)
    98  	}
    99  	return fmt.Errorf("appears to have leaked %s:\n%s", bad, stacks)
   100  }
   101  
   102  // AfterTest is meant to run in a defer that executes after a test completes.
   103  // It will detect common goroutine leaks, retrying in case there are goroutines
   104  // not synchronously torn down, and fail the test if any goroutines are stuck.
   105  func AfterTest(t *testing.T) {
   106  	if err := CheckAfterTest(300 * time.Millisecond); err != nil {
   107  		t.Errorf("Test %v", err)
   108  	}
   109  }
   110  
   111  func interestingGoroutines() (gs []string) {
   112  	buf := make([]byte, 2<<20)
   113  	buf = buf[:runtime.Stack(buf, true)]
   114  	for _, g := range strings.Split(string(buf), "\n\n") {
   115  		sl := strings.SplitN(g, "\n", 2)
   116  		if len(sl) != 2 {
   117  			continue
   118  		}
   119  		stack := strings.TrimSpace(sl[1])
   120  		if stack == "" ||
   121  			strings.Contains(stack, "sync.(*WaitGroup).Done") ||
   122  			strings.Contains(stack, "os.(*file).close") ||
   123  			strings.Contains(stack, "created by os/signal.init") ||
   124  			strings.Contains(stack, "runtime/panic.go") ||
   125  			strings.Contains(stack, "created by testing.RunTests") ||
   126  			strings.Contains(stack, "testing.Main(") ||
   127  			strings.Contains(stack, "runtime.goexit") ||
   128  			strings.Contains(stack, "github.com/coreos/etcd/pkg/testutil.interestingGoroutines") ||
   129  			strings.Contains(stack, "github.com/coreos/etcd/pkg/logutil.(*MergeLogger).outputLoop") ||
   130  			strings.Contains(stack, "github.com/golang/glog.(*loggingT).flushDaemon") ||
   131  			strings.Contains(stack, "created by runtime.gc") ||
   132  			strings.Contains(stack, "runtime.MHeap_Scavenger") {
   133  			continue
   134  		}
   135  		gs = append(gs, stack)
   136  	}
   137  	sort.Strings(gs)
   138  	return gs
   139  }