1 package gleak
2
3 import (
4 "fmt"
5 "path"
6 "path/filepath"
7 "reflect"
8 "strconv"
9 "strings"
10
11 "github.com/onsi/gomega"
12 "github.com/onsi/gomega/format"
13 "github.com/onsi/gomega/gleak/goroutine"
14 "github.com/onsi/gomega/types"
15 )
16
17
18
19
20
21
22
23
24
25
26
27
28
29 var ReportFilenameWithPath = false
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 var standardFilters = []types.GomegaMatcher{
45
46 IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode"),
47 IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode..."),
48 gomega.And(IgnoringTopFunction("runtime.goexit1"), IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*Suite).runNode")),
49 IgnoringTopFunction("github.com/onsi/ginkgo/v2/internal/interrupt_handler.(*InterruptHandler).registerForInterrupts..."),
50 IgnoringTopFunction("github.com/onsi/ginkgo/internal/specrunner.(*SpecRunner).registerForInterrupts"),
51 IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*genericOutputInterceptor).ResumeIntercepting"),
52 IgnoringCreator("github.com/onsi/ginkgo/v2/internal.(*genericOutputInterceptor).ResumeIntercepting..."),
53 IgnoringCreator("github.com/onsi/ginkgo/v2/internal.RegisterForProgressSignal"),
54
55
56 IgnoringTopFunction("testing.RunTests [chan receive]"),
57 IgnoringTopFunction("testing.(*T).Run [chan receive]"),
58 IgnoringTopFunction("testing.(*T).Parallel [chan receive]"),
59
60
61
62 IgnoringTopFunction("os/signal.signal_recv"),
63 IgnoringTopFunction("os/signal.loop"),
64
65
66 IgnoringInBacktrace("runtime.ensureSigM"),
67
68
69 IgnoringInBacktrace("runtime.ReadTrace"),
70 }
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127 func HaveLeaked(ignoring ...interface{}) types.GomegaMatcher {
128 m := &HaveLeakedMatcher{filters: standardFilters}
129 for _, ign := range ignoring {
130 switch ign := ign.(type) {
131 case string:
132 m.filters = append(m.filters, IgnoringTopFunction(ign))
133 case []Goroutine:
134 m.filters = append(m.filters, IgnoringGoroutines(ign))
135 case types.GomegaMatcher:
136 m.filters = append(m.filters, ign)
137 default:
138 panic(fmt.Sprintf("HaveLeaked expected a string, []Goroutine, or GomegaMatcher, but got:\n%s", format.Object(ign, 1)))
139 }
140 }
141 return m
142 }
143
144
145
146
147 type HaveLeakedMatcher struct {
148 filters []types.GomegaMatcher
149 leaked []Goroutine
150 }
151
152 var gsT = reflect.TypeOf([]Goroutine{})
153
154
155
156
157 func (matcher *HaveLeakedMatcher) Match(actual interface{}) (success bool, err error) {
158 val := reflect.ValueOf(actual)
159 switch val.Kind() {
160 case reflect.Array, reflect.Slice:
161 if !val.Type().AssignableTo(gsT) {
162 return false, fmt.Errorf(
163 "HaveLeaked matcher expects an array or slice of goroutines. Got:\n%s",
164 format.Object(actual, 1))
165 }
166 default:
167 return false, fmt.Errorf(
168 "HaveLeaked matcher expects an array or slice of goroutines. Got:\n%s",
169 format.Object(actual, 1))
170 }
171 goroutines := val.Convert(gsT).Interface().([]Goroutine)
172 matcher.leaked, err = matcher.filter(goroutines, matcher.filters)
173 if err != nil {
174 return false, err
175 }
176 if len(matcher.leaked) == 0 {
177 return false, nil
178 }
179 return true, nil
180 }
181
182
183 func (matcher *HaveLeakedMatcher) FailureMessage(actual interface{}) (message string) {
184 return fmt.Sprintf("Expected to leak %d goroutines:\n%s", len(matcher.leaked), matcher.listGoroutines(matcher.leaked, 1))
185 }
186
187
188 func (matcher *HaveLeakedMatcher) NegatedFailureMessage(actual interface{}) (message string) {
189 return fmt.Sprintf("Expected not to leak %d goroutines:\n%s", len(matcher.leaked), matcher.listGoroutines(matcher.leaked, 1))
190 }
191
192
193
194
195 func (matcher *HaveLeakedMatcher) listGoroutines(gs []Goroutine, indentation uint) string {
196 var buff strings.Builder
197 indent := strings.Repeat(format.Indent, int(indentation))
198 backtraceIdent := strings.Repeat(format.Indent, int(indentation+1))
199 for gidx, g := range gs {
200 if gidx > 0 {
201 buff.WriteRune('\n')
202 }
203 buff.WriteString(indent)
204 buff.WriteString("goroutine ")
205 buff.WriteString(strconv.FormatUint(g.ID, 10))
206 buff.WriteString(" [")
207 buff.WriteString(g.State)
208 buff.WriteString("]\n")
209
210 backtrace := g.Backtrace
211 for backtrace != "" {
212 buff.WriteString(backtraceIdent)
213
214
215 nlIdx := strings.IndexRune(backtrace, '\n')
216 if nlIdx < 0 {
217
218 buff.WriteString(backtrace)
219 break
220 }
221 calledFuncName := backtrace[:nlIdx]
222
223
224
225 location := backtrace[nlIdx+1:]
226 nnlIdx := strings.IndexRune(location, '\n')
227 if nnlIdx >= 0 {
228 backtrace, location = location[nnlIdx+1:], location[:nnlIdx]
229 } else {
230 backtrace = ""
231 }
232
233
234 location = strings.TrimSpace(location)
235 lineno := ""
236 if linenoIdx := strings.LastIndex(location, ":"); linenoIdx >= 0 {
237 location, lineno = location[:linenoIdx], location[linenoIdx+1:]
238 }
239 location = formatFilename(location) + ":" + lineno
240
241 buff.WriteString(calledFuncName)
242 buff.WriteString(" at ")
243
244
245
246 if offsetIdx := strings.LastIndexFunc(location,
247 func(r rune) bool { return r == ' ' }); offsetIdx >= 0 {
248 buff.WriteString(location[:offsetIdx])
249 } else {
250 buff.WriteString(location)
251 }
252 if backtrace != "" {
253 buff.WriteRune('\n')
254 }
255 }
256 }
257 return buff.String()
258 }
259
260
261
262
263
264
265
266 func (matcher *HaveLeakedMatcher) filter(
267 goroutines []Goroutine, filters []types.GomegaMatcher,
268 ) ([]Goroutine, error) {
269 gs := make([]Goroutine, 0, len(goroutines))
270 myID := goroutine.Current().ID
271 nextgoroutine:
272 for _, g := range goroutines {
273 if g.ID == myID {
274 continue
275 }
276 for _, filter := range filters {
277 matches, err := filter.Match(g)
278 if err != nil {
279 return nil, err
280 }
281 if matches {
282 continue nextgoroutine
283 }
284 }
285 gs = append(gs, g)
286 }
287 return gs, nil
288 }
289
290
291
292
293
294 func formatFilename(filename string) string {
295 if ReportFilenameWithPath {
296 return filename
297 }
298 dir := filepath.Dir(filename)
299 pkg := filepath.Base(dir)
300 switch pkg {
301 case ".", "..", "/", "\\":
302 pkg = ""
303 }
304
305
306
307 return path.Join(pkg, filepath.ToSlash(filepath.Base(filename)))
308 }
309
View as plain text