Skip to content

Commit

Permalink
test: Add real stack traces
Browse files Browse the repository at this point in the history
To verify the stacktrace parsing logic,
generate real stack traces under the following conditions:

- Go 1.21
- Go 1.20 installed with gimme
- Go 1.21 with tracebackancestors=10 set

The test verifies that the parsed stack traces
do not include functions that we did not expect to see
in a goroutine's trace.
  • Loading branch information
abhinav committed Oct 22, 2023
1 parent 7bdc274 commit b4e7421
Show file tree
Hide file tree
Showing 6 changed files with 570 additions and 0 deletions.
222 changes: 222 additions & 0 deletions internal/stack/stacks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
package stack

import (
"os"
"path/filepath"
"runtime"
"sort"
"strings"
Expand Down Expand Up @@ -242,6 +244,226 @@ func TestParseStackErrors(t *testing.T) {
}
}

func TestParseStackFixtures(t *testing.T) {
type goroutine struct {
// ID must match the goroutine ID in the fixture.
// We use this to ensure that we are matching the right goroutine.
ID int

State string
FirstFunction string

HasFunctions []string // non-exhaustive, in any order
NotHasFunctions []string
}

tests := []struct {
name string // file name inside testdata
stacks []goroutine // in any order
}{
{
name: "http.txt",
stacks: []goroutine{
{
ID: 1,
State: "running",
FirstFunction: "main.getStackBuffer",
HasFunctions: []string{
"main.getStackBuffer",
"main.main",
},
},
{
ID: 4,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.Serve",
},
NotHasFunctions: []string{"main.start"},
},
{
ID: 20,
State: "select",
FirstFunction: "net/http.(*persistConn).readLoop",
},
{
ID: 21,
State: "select",
FirstFunction: "net/http.(*persistConn).writeLoop",
},
{
ID: 8,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.(*conn).serve",
},
NotHasFunctions: []string{"net/http.(*Server).Serve"},
},
},
},
{
name: "http.go1.20.txt",
stacks: []goroutine{
{
ID: 1,
State: "running",
FirstFunction: "main.getStackBuffer",
HasFunctions: []string{
"main.getStackBuffer",
"main.main",
},
},
{
ID: 20,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.(*Server).Serve",
},
NotHasFunctions: []string{"main.start"},
},
{
ID: 24,
State: "select",
FirstFunction: "net/http.(*persistConn).readLoop",
},
{
ID: 25,
State: "select",
FirstFunction: "net/http.(*persistConn).writeLoop",
},
{
ID: 4,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.(*conn).serve",
},
NotHasFunctions: []string{"net/http.(*Server).Serve"},
},
},
},
{
name: "http.tracebackancestors.txt",
stacks: []goroutine{
{
ID: 1,
State: "running",
FirstFunction: "main.getStackBuffer",
HasFunctions: []string{
"main.getStackBuffer",
"main.main",
},
},
{
ID: 20,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.Serve",
},
NotHasFunctions: []string{
"main.start", // created by
"main.main", // tracebackancestors
},
},
{
ID: 24,
State: "select",
FirstFunction: "net/http.(*persistConn).readLoop",
NotHasFunctions: []string{
"net/http.(*Transport).dialConn", // created by
// tracebackancestors:
"net/http.(*Transport).dialConnFor",
"net/http.(*Transport).queueForDial",
"net/http.(*Client).Get",
"main.start",
"main.main",
},
},
{
ID: 4,
State: "IO wait",
FirstFunction: "internal/poll.runtime_pollWait",
HasFunctions: []string{
"internal/poll.runtime_pollWait",
"net/http.(*conn).serve",
},
NotHasFunctions: []string{
"net/http.(*Server).Serve", // created by
// tracebackancestors:
"net/http.Serve",
"main.start",
"main.main",
},
},
{
ID: 25,
State: "select",
FirstFunction: "net/http.(*persistConn).writeLoop",
NotHasFunctions: []string{
"net/http.(*Transport).dialConn", // created by
// tracebackancestors:
"net/http.(*Transport).dialConnFor",
"net/http.(*Transport).queueForDial",
"net/http.(*Client).Get",
"main.start",
"main.main",
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fixture, err := os.Open(filepath.Join("testdata", tt.name))
require.NoError(t, err)
defer func() {
assert.NoError(t, fixture.Close())
}()

stacks, err := newStackParser(fixture).Parse()
require.NoError(t, err)

stacksByID := make(map[int]Stack, len(stacks))
for _, s := range stacks {
stacksByID[s.ID()] = s
}

for _, wantStack := range tt.stacks {
gotStack, ok := stacksByID[wantStack.ID]
if !assert.True(t, ok, "missing stack %v", wantStack.ID) {
continue
}
delete(stacksByID, wantStack.ID)

assert.Equal(t, wantStack.State, gotStack.State())
assert.Equal(t, wantStack.FirstFunction, gotStack.FirstFunction())

for _, fn := range wantStack.HasFunctions {
assert.True(t, gotStack.HasFunction(fn), "missing in stack: %v\n%s", fn, gotStack.Full())
}

for _, fn := range wantStack.NotHasFunctions {
assert.False(t, gotStack.HasFunction(fn), "unexpected in stack: %v\n%s", fn, gotStack.Full())
}
}

for _, s := range stacksByID {
t.Errorf("unexpected stack:\n%s", s.Full())
}
})
}
}

func joinLines(lines ...string) string {
return strings.Join(lines, "\n") + "\n"
}
Expand Down
27 changes: 27 additions & 0 deletions internal/stack/testdata/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.DEFAULT_GOAL := all

GO_VERSION = $(shell go version | cut -d' ' -f3)

# Append to this list to add new stacks.
STACKS =

# In Go 1.21, the output format was changed slightly.
#
# Generate a 1.20 version of the output
# only if we're running on Go 1.20.
ifneq (,$(findstring go1.20,$(GO_VERSION)))
STACKS += http.go1.20.txt
http.go1.20.txt: http.go
go run $< > $@
else
STACKS += http.txt
http.txt: http.go
go run $< > $@
endif

STACKS += http.tracebackancestors.txt
http.tracebackancestors.txt: http.go
GODEBUG=tracebackancestors=10 go run $< > $@

.PHONY: all
all: $(STACKS)
48 changes: 48 additions & 0 deletions internal/stack/testdata/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build ignore

package main

import (
"fmt"
"net"
"net/http"
"runtime"
"time"
)

func main() {
if err := start(); err != nil {
panic(err)
}

fmt.Println(string(getStackBuffer()))
}

func start() error {
ln, err := net.Listen("tcp", ":0")
if err != nil {
return err
}

go http.Serve(ln, nil)

// Wait until HTTP server is ready.
url := "http://" + ln.Addr().String()
for i := 0; i < 10; i++ {
if _, err := http.Get(url); err == nil {
return nil
}
time.Sleep(100 * time.Millisecond)
}

return fmt.Errorf("failed to start HTTP server")
}

func getStackBuffer() []byte {
for i := 4096; ; i *= 2 {
buf := make([]byte, i)
if n := runtime.Stack(buf, true /* all */); n < i {
return buf[:n]
}
}
}
64 changes: 64 additions & 0 deletions internal/stack/testdata/http.go1.20.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
goroutine 1 [running]:
main.getStackBuffer()
/home/abg/src/goleak/internal/stack/testdata/http.go:44 +0x4f
main.main()
/home/abg/src/goleak/internal/stack/testdata/http.go:18 +0x2a

goroutine 20 [IO wait]:
internal/poll.runtime_pollWait(0x7866b1e34f08, 0x72)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/runtime/netpoll.go:306 +0x89
internal/poll.(*pollDesc).wait(0xc0000dc000?, 0x16?, 0x0)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:84 +0x32
internal/poll.(*pollDesc).waitRead(...)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Accept(0xc0000dc000)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_unix.go:614 +0x2bd
net.(*netFD).accept(0xc0000dc000)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/fd_unix.go:172 +0x35
net.(*TCPListener).accept(0xc0000a00f0)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/tcpsock_posix.go:148 +0x25
net.(*TCPListener).Accept(0xc0000a00f0)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/tcpsock.go:297 +0x3d
net/http.(*Server).Serve(0xc000076000, {0x73dbe0, 0xc0000a00f0})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:3059 +0x385
net/http.Serve({0x73dbe0, 0xc0000a00f0}, {0x0?, 0x0})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:2581 +0x74
created by main.start
/home/abg/src/goleak/internal/stack/testdata/http.go:27 +0x8e

goroutine 24 [select]:
net/http.(*persistConn).readLoop(0xc0000b4480)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:2227 +0xd85
created by net/http.(*Transport).dialConn
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:1765 +0x16ea

goroutine 25 [select]:
net/http.(*persistConn).writeLoop(0xc0000b4480)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:2410 +0xf2
created by net/http.(*Transport).dialConn
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/transport.go:1766 +0x173d

goroutine 4 [IO wait]:
internal/poll.runtime_pollWait(0x7866b1e34d28, 0x72)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/runtime/netpoll.go:306 +0x89
internal/poll.(*pollDesc).wait(0xc00007e000?, 0xc000106000?, 0x0)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:84 +0x32
internal/poll.(*pollDesc).waitRead(...)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_poll_runtime.go:89
internal/poll.(*FD).Read(0xc00007e000, {0xc000106000, 0x1000, 0x1000})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/internal/poll/fd_unix.go:167 +0x299
net.(*netFD).Read(0xc00007e000, {0xc000106000?, 0x4a92e6?, 0x0?})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/fd_posix.go:55 +0x29
net.(*conn).Read(0xc000014028, {0xc000106000?, 0x0?, 0xc0000781e8?})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/net.go:183 +0x45
net/http.(*connReader).Read(0xc0000781e0, {0xc000106000, 0x1000, 0x1000})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:782 +0x171
bufio.(*Reader).fill(0xc000104000)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/bufio/bufio.go:106 +0xff
bufio.(*Reader).Peek(0xc000104000, 0x4)
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/bufio/bufio.go:144 +0x5d
net/http.(*conn).serve(0xc000100000, {0x73df98, 0xc0000780f0})
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:2030 +0x77c
created by net/http.(*Server).Serve
/home/abg/.gimme/versions/go1.20.10.linux.amd64/src/net/http/server.go:3089 +0x5ed

Loading

0 comments on commit b4e7421

Please sign in to comment.