@@ -9,155 +9,94 @@ import (
99 "fmt"
1010 "os"
1111 "path"
12- "path/filepath"
1312 "regexp"
1413 "runtime"
1514 "sort"
16- "strconv"
1715 "strings"
1816 "testing"
1917 "time"
2018)
2119
2220var (
23- goidRegex = regexp .MustCompile (`^\S+\s+(\d+)` )
2421 funcNameRegex = regexp .MustCompile (`^(\S+)\(\S+` )
25- fileLineRegex = regexp .MustCompile (`^\s*(\S+):(\d+)` )
2622)
2723
28- type stackTraceEntry struct {
29- GoID int
30- File string
31- Line int
32- Func string
33- Trace []string
34- }
35-
36- func (e * stackTraceEntry ) String () string {
37- return fmt .Sprintf ("%d %s:%d %s\n %s" ,
38- e .GoID , e .File , e .Line , e .Func , strings .Join (e .Trace , "\n " ))
39- }
40-
4124// CheckLeakedGoRoutines is used to check for goroutine leaks at the end of a test.
4225// It is not uncommon to leave a go routine that will never finish,
4326// e.g. blocked on a channel that is unreachable will never be closed.
4427// This function enumerates goroutines and reports any goroutines that are left running.
4528func CheckLeakedGoRoutines (funcWhitelist ... string ) error {
46- _ , err := checkLeakedGoRoutines (funcWhitelist ... )
47- return err
29+ var leaked []string
30+ for tryIdx := 0 ; tryIdx < 8 ; tryIdx ++ {
31+ // We need to explicitly call GC to call full GC procedures for real.
32+ // Otherwise, for example there is high probability of not calling
33+ // Finalizers (which are used in xcontext, for example).
34+ runtime .GC ()
35+ runtime .Gosched ()
36+
37+ leaked = getLeakedGoroutines (funcWhitelist )
38+ if len (leaked ) == 0 {
39+ return nil
40+ }
41+ time .Sleep (10 * time .Millisecond )
42+ }
43+ sort .Strings (leaked )
44+ return fmt .Errorf ("leaked goroutines:\n %s\n " , strings .Join (leaked , "\n " ))
4845}
4946
50- func checkLeakedGoRoutines (funcWhitelist ... string ) (string , error ) {
51- // Allow some time for dying routines to exit.
52- time .Sleep (80 * time .Millisecond )
47+ func getLeakedGoroutines (funcWhitelist []string ) []string {
5348 // Get goroutine stacks
5449 buf := make ([]byte , 1000000 )
5550 n := runtime .Stack (buf , true /* all */ )
56- // First goroutine is always the running one, we skip it.
57- ph := 0
58- var e * stackTraceEntry
5951 strBuf := string (buf [:n ])
60- var badEntries []string
6152
62- addBadEntry := func () {
63- if e == nil {
64- return
65- }
66- found := false
67- for _ , wle := range funcWhitelist {
68- if matched , _ := path .Match (wle , e .Func ); matched {
69- found = true
70- break
71- }
72- }
73- if ! found {
74- badEntries = append (badEntries , e .String ())
75- }
76- }
53+ goroutinesStacks := strings .Split (strBuf , "\n \n " )
7754
78- for ln , line := range strings .Split (strBuf , "\n " ) {
79- switch ph {
80- case 0 : // Look for an empty line
81- if len (line ) == 0 {
82- addBadEntry ()
83- e = nil
84- ph ++
85- } else {
86- if e != nil {
87- e .Trace = append (e .Trace , line )
88- }
89- }
90- case 1 : // Extract goroutine id
91- if m := goidRegex .FindStringSubmatch (line ); m != nil {
92- if goid , err := strconv .Atoi (m [1 ]); err == nil {
93- e = & stackTraceEntry {GoID : goid }
94- ph ++
95- break
96- }
97- }
98- panic (fmt .Sprintf ("Cannot parse backtrace (goid) %d %q\n %s" , ln + 1 , line , strBuf ))
99- case 2 : // Extract function name.
100- e .Trace = append (e .Trace , line )
101- if m := funcNameRegex .FindStringSubmatch (line ); m != nil {
102- e .Func = m [1 ]
103- ph ++
104- break
55+ var result []string
56+ // First goroutine is always the running one, we skip it.
57+ for _ , goroutineStack := range goroutinesStacks [1 :] {
58+ stack := strings .Split (goroutineStack , "\n " )
59+ // first line is goroutine info, skip it
60+ var whitelisted bool
61+ var userGoroutine bool
62+ for idx , line := range stack [1 :] {
63+ if idx % 2 == 1 {
64+ // Lines go in the following order:
65+ // testing.tRunner(0xc000117040, 0x125ec08)
66+ // /usr/local/go/src/testing/testing.go:1259 +0x1db
67+ // created by testing.(*T).Run
68+ // /usr/local/go/src/testing/testing.go:1306 +0x673
69+ // ...
70+ // Should skip path
71+ continue
10572 }
106- if e .Func != "" {
107- // This means entire routine is in stdlib, ignore it.
108- e = nil
109- if line == "" {
110- ph = 1
111- } else {
112- ph = 0
113- }
114- break
73+ m := funcNameRegex .FindStringSubmatch (line )
74+ if m == nil {
75+ continue
11576 }
116- panic (fmt .Sprintf ("Cannot parse backtrace (func) %d %q" , ln , line ))
117- case 3 : // Extract file name
118- e .Trace = append (e .Trace , line )
119- if m := fileLineRegex .FindStringSubmatch (line ); m != nil {
120- e .File = filepath .Base (m [1 ])
121- if ln , err := strconv .Atoi (m [2 ]); err == nil {
122- e .Line = ln
123- }
124- // We are looking for a non-stdlib function.
125- if ! strings .Contains (e .Func , "/" ) || ! strings .Contains (path .Dir (e .Func ), "." ) {
126- ph = 2
127- } else {
128- ph = 0
77+ funcName := m [1 ]
78+
79+ // stdlib functions do not contain '/'
80+ if strings .Contains (funcName , "/" ) {
81+ userGoroutine = true
82+ for _ , wle := range funcWhitelist {
83+ if matched , _ := path .Match (wle , funcName ); matched {
84+ whitelisted = true
85+ break
86+ }
12987 }
130- break
13188 }
132- panic (fmt .Sprintf ("Cannot parse backtrace (file) %d %q" , ln , line ))
89+ }
90+ if userGoroutine && ! whitelisted {
91+ result = append (result , goroutineStack )
13392 }
13493 }
135- addBadEntry ()
136-
137- var err error
138- if len (badEntries ) > 0 {
139- sort .Strings (badEntries )
140- err = fmt .Errorf ("leaked goroutines:\n %s\n " , strings .Join (badEntries , "\n " ))
141- }
142- return strBuf , err
94+ return result
14395}
14496
14597func LeakCheckingTestMain (m * testing.M , funcWhitelist ... string ) {
14698 ret := m .Run ()
14799 if ret == 0 {
148- time .Sleep (20 * time .Millisecond ) // Give stragglers some time to exit.
149-
150- // We need to explicitly call GC to call full GC procedures for real.
151- // Otherwise for example there is high probability of not calling
152- // Finalizers (which are used in xcontext, for example).
153- //
154- // And we do it twice for better reliability (I already had
155- // experience where calling `GC` one was not enough).
156- runtime .GC ()
157- runtime .Gosched ()
158- runtime .GC ()
159- runtime .Gosched ()
160-
161100 if err := CheckLeakedGoRoutines (funcWhitelist ... ); err != nil {
162101 fmt .Fprintf (os .Stderr , "%s" , err )
163102 ret = 1
0 commit comments