1 package gleak
2
3 import (
4 "os"
5 "os/signal"
6 "sync"
7 "time"
8
9 . "github.com/onsi/ginkgo/v2"
10 . "github.com/onsi/gomega"
11 )
12
13
14
15
16 var _ = Describe("HaveLeaked", func() {
17
18 It("renders indented goroutine information including (malformed) backtrace", func() {
19 gs := []Goroutine{
20 {
21 ID: 42,
22 State: "stoned",
23 Backtrace: `main.foo.func1()
24 /home/foo/test.go:6 +0x28
25 created by main.foo
26 /home/foo/test.go:5 +0x64
27 `,
28 },
29 }
30 m := HaveLeaked().(*HaveLeakedMatcher)
31 Expect(m.listGoroutines(gs, 1)).To(Equal(` goroutine 42 [stoned]
32 main.foo.func1() at foo/test.go:6
33 created by main.foo at foo/test.go:5`))
34
35 gs = []Goroutine{
36 {
37 ID: 42,
38 State: "stoned",
39 Backtrace: `main.foo.func1()
40 /home/foo/test.go:6 +0x28
41 created by main.foo
42 /home/foo/test.go:5 +0x64`,
43 },
44 }
45 Expect(m.listGoroutines(gs, 1)).To(Equal(` goroutine 42 [stoned]
46 main.foo.func1() at foo/test.go:6
47 created by main.foo at foo/test.go:5`))
48
49 gs = []Goroutine{
50 {
51 ID: 42,
52 State: "stoned",
53 Backtrace: `main.foo.func1()
54 /home/foo/test.go:6 +0x28
55 created by main.foo
56 /home/foo/test.go:5`,
57 },
58 }
59 Expect(m.listGoroutines(gs, 1)).To(Equal(` goroutine 42 [stoned]
60 main.foo.func1() at foo/test.go:6
61 created by main.foo at foo/test.go:5`))
62
63 gs = []Goroutine{
64 {
65 ID: 42,
66 State: "stoned",
67 Backtrace: `main.foo.func1()
68 /home/foo/test.go:6 +0x28
69 created by main.foo`,
70 },
71 }
72 Expect(m.listGoroutines(gs, 1)).To(Equal(` goroutine 42 [stoned]
73 main.foo.func1() at foo/test.go:6
74 created by main.foo`))
75 })
76
77 It("considers testing and runtime goroutines not to be leaks", func() {
78 Eventually(Goroutines).WithTimeout(2*time.Second).WithPolling(250*time.Millisecond).
79 ShouldNot(HaveLeaked(), "should not find any leaks by default")
80 })
81
82 When("using signals", func() {
83
84 It("doesn't find leaks", func() {
85 c := make(chan os.Signal, 1)
86 signal.Notify(c, os.Interrupt)
87 Eventually(Goroutines).WithTimeout(2*time.Second).WithPolling(250*time.Millisecond).
88 ShouldNot(HaveLeaked(), "found signal.Notify leaks")
89
90 signal.Reset(os.Interrupt)
91 Eventually(Goroutines).WithTimeout(2*time.Second).WithPolling(250*time.Millisecond).
92 ShouldNot(HaveLeaked(), "found signal.Reset leaks")
93 })
94
95 })
96
97 It("checks against list of expected goroutines", func() {
98 By("taking a snapshot")
99 gs := Goroutines()
100 m := HaveLeaked(gs)
101
102 By("starting a goroutine")
103 done := make(chan struct{})
104 var once sync.Once
105 go func() {
106 <-done
107 }()
108 defer once.Do(func() { close(done) })
109
110 By("detecting the goroutine")
111 Expect(m.Match(Goroutines())).To(BeTrue())
112
113 By("terminating the goroutine and ensuring it has terminated")
114 once.Do(func() { close(done) })
115 Eventually(func() (bool, error) {
116 return m.Match(Goroutines())
117 }).Should(BeFalse())
118 })
119
120 Context("failure messages", func() {
121
122 var snapshot []Goroutine
123
124 BeforeEach(func() {
125 snapshot = Goroutines()
126 done := make(chan struct{})
127 go func() {
128 <-done
129 }()
130 DeferCleanup(func() {
131 close(done)
132 Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot))
133 })
134 })
135
136 It("returns a failure message", func() {
137 m := HaveLeaked(snapshot)
138 gs := Goroutines()
139 Expect(m.Match(gs)).To(BeTrue())
140 Expect(m.FailureMessage(gs)).To(MatchRegexp(`Expected to leak 1 goroutines:
141 goroutine \d+ \[.+\]
142 .* at .*:\d+
143 created by .* at .*:\d+`))
144 })
145
146 It("returns a negated failure message", func() {
147 m := HaveLeaked(snapshot)
148 gs := Goroutines()
149 Expect(m.Match(gs)).To(BeTrue())
150 Expect(m.NegatedFailureMessage(gs)).To(MatchRegexp(`Expected not to leak 1 goroutines:
151 goroutine \d+ \[.+\]
152 .* at .*:\d+
153 created by .* at .*:\d+`))
154 })
155
156 When("things go wrong", func() {
157
158 It("rejects unsupported filter args types", func() {
159 Expect(func() { _ = HaveLeaked(42) }).To(PanicWith(
160 "HaveLeaked expected a string, []Goroutine, or GomegaMatcher, but got:\n <int>: 42"))
161 })
162
163 It("accepts plain strings as filters", func() {
164 m := HaveLeaked("foo.bar")
165 Expect(m.Match([]Goroutine{
166 {TopFunction: "foo.bar"},
167 })).To(BeFalse())
168 })
169
170 It("expects actual to be a slice of Goroutine", func() {
171 m := HaveLeaked()
172 Expect(m.Match(nil)).Error().To(MatchError(
173 "HaveLeaked matcher expects an array or slice of goroutines. Got:\n <nil>: nil"))
174 Expect(m.Match("foo!")).Error().To(MatchError(
175 "HaveLeaked matcher expects an array or slice of goroutines. Got:\n <string>: foo!"))
176 Expect(m.Match([]string{"foo!"})).Error().To(MatchError(
177 "HaveLeaked matcher expects an array or slice of goroutines. Got:\n <[]string | len:1, cap:1>: [\"foo!\"]"))
178 })
179
180 It("handles filter matcher errors", func() {
181 m := HaveLeaked(HaveField("foobar", BeNil()))
182 Expect(m.Match([]Goroutine{
183 {ID: 0},
184 })).Error().To(HaveOccurred())
185 })
186
187 })
188
189 })
190
191 Context("wrapped around test nodes", func() {
192
193 var snapshot []Goroutine
194
195 When("not leaking", func() {
196
197 BeforeEach(func() {
198 snapshot = Goroutines()
199 })
200
201 AfterEach(func() {
202 Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot))
203 })
204
205 It("doesn't leak in test", func() {
206
207 })
208
209 })
210
211 When("leaking", func() {
212
213 done := make(chan struct{})
214
215 BeforeEach(func() {
216 snapshot = Goroutines()
217 })
218
219 AfterEach(func() {
220 Expect(Goroutines()).To(HaveLeaked(snapshot))
221 close(done)
222 Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot))
223 })
224
225 It("leaks in test", func() {
226 go func() {
227 <-done
228 }()
229 })
230
231 })
232
233 })
234
235 Context("handling file names and paths in backtraces", func() {
236
237 When("ReportFilenameWithPath is true", Ordered, func() {
238
239 var oldState bool
240
241 BeforeAll(func() {
242 oldState = ReportFilenameWithPath
243 ReportFilenameWithPath = true
244 DeferCleanup(func() {
245 ReportFilenameWithPath = oldState
246 })
247 })
248
249 It("doesn't shorten filenames", func() {
250 Expect(formatFilename("/home/foo/bar/baz.go")).To(Equal("/home/foo/bar/baz.go"))
251 })
252
253 })
254
255 When("ReportFilenameWithPath is false", Ordered, func() {
256
257 var oldState bool
258
259 BeforeAll(func() {
260 oldState = ReportFilenameWithPath
261 ReportFilenameWithPath = false
262 DeferCleanup(func() {
263 ReportFilenameWithPath = oldState
264 })
265 })
266
267 It("does return only package and filename, but no path", func() {
268 Expect(formatFilename("/home/foo/bar/baz.go")).To(Equal("bar/baz.go"))
269 Expect(formatFilename("/bar/baz.go")).To(Equal("bar/baz.go"))
270 Expect(formatFilename("/baz.go")).To(Equal("baz.go"))
271 Expect(formatFilename("/")).To(Equal("/"))
272 })
273
274 })
275
276 })
277
278 })
279
View as plain text