1 package goroutine
2
3 import (
4 "bufio"
5 "errors"
6 "io"
7 "reflect"
8 "strings"
9 "sync"
10 "testing/iotest"
11 "time"
12
13 . "github.com/onsi/ginkgo/v2"
14 . "github.com/onsi/gomega"
15 )
16
17 var _ = Describe("goroutine", func() {
18
19 const stack = `runtime/debug.Stack()
20 /usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x65
21 runtime/debug.PrintStack()
22 /usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x19
23 main.main()
24 /tmp/sandbox3386995578/prog.go:10 +0x17
25 `
26 const header = `goroutine 666 [running]:
27 `
28 const nextStack = header + `main.hades()
29 /tmp/sandbox3386995578/prog.go:10 +0x17
30 `
31
32 It("prints", func() {
33 Expect(Goroutine{
34 ID: 1234,
35 State: "gone",
36 TopFunction: "gopher.hole",
37 }.String()).To(Equal(
38 "Goroutine ID: 1234, state: gone, top function: gopher.hole"))
39
40 Expect(Goroutine{
41 ID: 1234,
42 State: "gone",
43 TopFunction: "gopher.hole",
44 CreatorFunction: "google",
45 BornAt: "/plan/10:2009",
46 }.String()).To(Equal(
47 "Goroutine ID: 1234, state: gone, top function: gopher.hole, created by: google, at: /plan/10:2009"))
48
49 Expect(Goroutine{
50 ID: 1234,
51 State: "gone",
52 TopFunction: "gopher.hole",
53 CreatorFunction: "google",
54 BornAt: "/plan/10:2009",
55 }.GomegaString()).To(Equal(
56 "{ID: 1234, State: \"gone\", TopFunction: \"gopher.hole\", CreatorFunction: \"google\", BornAt: \"/plan/10:2009\"}"))
57 })
58
59 Context("goroutine header", func() {
60
61 It("parses goroutine header", func() {
62 g := new(header)
63 Expect(g.ID).To(Equal(uint64(666)))
64 Expect(g.State).To(Equal("running"))
65 })
66
67 It("panics on malformed goroutine header", func() {
68 Expect(func() { _ = new("a") }).To(PanicWith(MatchRegexp(`invalid stack header: .*`)))
69 Expect(func() { _ = new("a b") }).To(PanicWith(MatchRegexp(`invalid stack header: .*`)))
70 })
71
72 It("panics on malformed goroutine ID", func() {
73 Expect(func() { _ = new("a b c:\n") }).To(PanicWith(MatchRegexp(`invalid stack header ID: "b", header: ".*"`)))
74 })
75
76 })
77
78 Context("goroutine backtrace", func() {
79
80 It("parses goroutine's backtrace", func() {
81 r := bufio.NewReader(strings.NewReader(stack))
82 topF, backtrace := parseGoroutineBacktrace(r)
83 Expect(topF).To(Equal("runtime/debug.Stack"))
84 Expect(backtrace).To(Equal(stack))
85
86 r.Reset(strings.NewReader(stack[:len(stack)-1]))
87 topF, backtrace = parseGoroutineBacktrace(r)
88 Expect(topF).To(Equal("runtime/debug.Stack"))
89 Expect(backtrace).To(Equal(stack[:len(stack)-1]))
90 })
91
92 It("parses goroutine's backtrace until next goroutine header", func() {
93 r := bufio.NewReader(strings.NewReader(stack + nextStack))
94 topF, backtrace := parseGoroutineBacktrace(r)
95 Expect(topF).To(Equal("runtime/debug.Stack"))
96 Expect(backtrace).To(Equal(stack))
97 })
98
99 It("panics on invalid function call stack entry", func() {
100 r := bufio.NewReader(strings.NewReader(`main.main
101 /somewhere/prog.go:123 +0x666
102 `))
103 Expect(func() { parseGoroutineBacktrace(r) }).To(PanicWith(MatchRegexp(`invalid function call stack entry: "main.main"`)))
104 })
105
106 It("panics on failing reader", func() {
107 Expect(func() {
108 parseGoroutineBacktrace(bufio.NewReader(
109 iotest.ErrReader(errors.New("foo failure"))))
110 }).To(PanicWith("parsing backtrace failed: foo failure"))
111
112 Expect(func() {
113 parseGoroutineBacktrace(
114 bufio.NewReaderSize(
115 iotest.TimeoutReader(strings.NewReader(strings.Repeat("x", 32))),
116 16))
117 }).To(PanicWith("parsing backtrace failed: timeout"))
118
119 Expect(func() {
120 parseGoroutineBacktrace(bufio.NewReader(
121 iotest.ErrReader(io.ErrClosedPipe)))
122 }).To(PanicWith(MatchRegexp(`parsing backtrace failed: .*`)))
123 })
124
125 It("parses goroutine information and stack", func() {
126 gs := parseStack([]byte(header + stack))
127 Expect(gs).To(HaveLen(1))
128 Expect(gs[0]).To(And(
129 HaveField("ID", uint64(666)),
130 HaveField("State", "running"),
131 HaveField("TopFunction", "runtime/debug.Stack"),
132 HaveField("Backtrace", stack)))
133 })
134
135 It("finds its Creator", func() {
136 creator, location := findCreator(`
137 goroutine 42 [chan receive]:
138 main.foo.func1()
139 /home/foo/test.go:6 +0x28
140 created by main.foo
141 /home/foo/test.go:5 +0x64
142 `)
143 Expect(creator).To(Equal("main.foo"))
144 Expect(location).To(Equal("/home/foo/test.go:5"))
145 })
146
147 It("handles missing or invalid creator information", func() {
148 creator, location := findCreator("")
149 Expect(creator).To(BeEmpty())
150 Expect(location).To(BeEmpty())
151
152 creator, location = findCreator(`
153 goroutine 42 [chan receive]:
154 main.foo.func1()
155 /home/foo/test.go:6 +0x28
156 created by`)
157 Expect(creator).To(BeEmpty())
158 Expect(location).To(BeEmpty())
159
160 creator, location = findCreator(`
161 goroutine 42 [chan receive]:
162 main.foo.func1()
163 /home/foo/test.go:6 +0x28
164 created by main.foo`)
165 Expect(creator).To(BeEmpty())
166 Expect(location).To(BeEmpty())
167
168 creator, location = findCreator(`
169 goroutine 42 [chan receive]:
170 main.foo.func1()
171 /home/foo/test.go:6 +0x28
172 created by main.foo
173 /home/foo/test.go:5
174 `)
175 Expect(creator).To(BeEmpty())
176 Expect(location).To(BeEmpty())
177 })
178
179 })
180
181 Context("live", func() {
182
183 It("discovers current goroutine information", func() {
184 type T struct{}
185 pkg := reflect.TypeOf(T{}).PkgPath()
186 gs := goroutines(false)
187 Expect(gs).To(HaveLen(1))
188 Expect(gs[0]).To(And(
189 HaveField("ID", Not(BeZero())),
190 HaveField("State", "running"),
191 HaveField("TopFunction", pkg+".stacks"),
192 HaveField("Backtrace", MatchRegexp(pkg+`.stacks.*
193 `))))
194 })
195
196 It("discovers a goroutine's creator", func() {
197 ch := make(chan Goroutine)
198 go func() {
199 ch <- Current()
200 }()
201 g := <-ch
202 Expect(g.CreatorFunction).NotTo(BeEmpty(), "no creator: %s", g.Backtrace)
203 Expect(g.BornAt).NotTo(BeEmpty())
204 })
205
206 It("discovers all goroutine information", func() {
207 By("creating a chan receive canary goroutine")
208 done := make(chan struct{})
209 go testWait(done)
210 once := sync.Once{}
211 cloze := func() { once.Do(func() { close(done) }) }
212 defer cloze()
213
214 By("getting all goroutines including canary")
215 type T struct{}
216 pkg := reflect.TypeOf(T{}).PkgPath()
217 Eventually(Goroutines).
218 WithTimeout(1 * time.Second).WithPolling(250 * time.Millisecond).
219 Should(ContainElements(
220 And(
221 HaveField("TopFunction", pkg+".stacks"),
222 HaveField("State", "running")),
223 And(
224 HaveField("TopFunction", pkg+".testWait"),
225 HaveField("State", "chan receive")),
226 ))
227
228 By("getting all goroutines after being done with the canary")
229 cloze()
230 Eventually(Goroutines).
231 WithTimeout(1 * time.Second).WithPolling(250 * time.Millisecond).
232 ShouldNot(ContainElement(HaveField("TopFunction", pkg+".testWait")))
233 })
234
235 })
236
237 })
238
239 func testWait(done <-chan struct{}) {
240 <-done
241 }
242
View as plain text