1
2
3
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
21
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
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
59
60 func CheckAfterTest(d time.Duration) error {
61 http.DefaultTransport.(*http.Transport).CloseIdleConnections()
62 var bad string
63
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
91
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
99
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
111
112
113 func afterTest(t TB) {
114
115
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
166 runtime.Gosched()
167
168 if CheckLeakedGoroutine() {
169 os.Exit(1)
170 }
171 }
172
173
174
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