leak.go 4.0 KB

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