Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions leaks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ func TestVerifyNone(t *testing.T) {
}))
require.True(t, cleanupCalled, "expect cleanup registered callback to be called")
})

t.Run("IgnoreAnyFunction excludes goroutines by creator function", func(t *testing.T) {
stop := make(chan struct{})
spawnIgnoredGoroutine(stop)

ft := &fakeT{}
VerifyNone(ft, testOptions())
require.NotEmpty(t, ft.errors, "Expect errors from VerifyNone on leaked goroutine without ignore")

VerifyNone(t, IgnoreAnyFunction("go.uber.org/goleak.spawnIgnoredGoroutine"))

close(stop)
VerifyNone(t)
})

t.Run("IgnoreAnyFunction excludes goroutines by creator method", func(t *testing.T) {
stop := make(chan struct{})
target := &creatorMethodTarget{}
target.spawn(stop)

ft := &fakeT{}
VerifyNone(ft, testOptions())
require.NotEmpty(t, ft.errors, "Expect errors from VerifyNone on leaked goroutine without ignore")

VerifyNone(t, IgnoreAnyFunction("go.uber.org/goleak.(*creatorMethodTarget).spawn"))

close(stop)
VerifyNone(t)
})
}

func TestIgnoreCurrent(t *testing.T) {
Expand Down Expand Up @@ -201,3 +230,18 @@ func TestVerifyParallel(t *testing.T) {
VerifyNone(t)
})
}

func spawnIgnoredGoroutine(stop <-chan struct{}) {
go func() {
<-stop
}()
}

type creatorMethodTarget struct{}

func (c *creatorMethodTarget) spawn(stop <-chan struct{}) {
go func() {
<-stop
}()
}

4 changes: 2 additions & 2 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func IgnoreTopFunction(f string) Option {
}

// IgnoreAnyFunction ignores goroutines where the specified function
// is present anywhere in the stack.
// is present anywhere in the stack or created the goroutine.
//
// The function name must be fully qualified, e.g.,
//
Expand All @@ -81,7 +81,7 @@ func IgnoreTopFunction(f string) Option {
// go.uber.org/goleak.(*MyType).MyMethod
func IgnoreAnyFunction(f string) Option {
return addFilter(func(s stack.Stack) bool {
return s.HasFunction(f)
return s.HasFunction(f) || s.CreatedBy() == f
})
}

Expand Down
42 changes: 35 additions & 7 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,34 @@ func TestOptionsFilters(t *testing.T) {
opts = buildOpts(IgnoreTopFunction("go.uber.org/goleak.(*blockedG).block"))
require.Zero(t, countUnfiltered(), "blockedG should be filtered out. running: %v", stack.All())

// If we ignore startBlockedG, that should not ignore the blockedG goroutine
// because startBlockedG should be the "created by" function in the stack.
// If we ignore startBlockedG, that should ignore the blockedG goroutine
// because startBlockedG was the creator function in the stack.
opts = buildOpts(IgnoreAnyFunction("go.uber.org/goleak.startBlockedG"))
require.Zero(t, countUnfiltered(),
"startBlockedG should be filtered out. running: %v", stack.All())

// An unrelated function should not filter out the blockedG goroutine.
opts = buildOpts(IgnoreAnyFunction("go.uber.org/goleak.unrelatedFunc"))
require.Equal(t, 1, countUnfiltered(),
"startBlockedG should not be filtered out. running: %v", stack.All())
"unrelatedFunc should not be filtered out. running: %v", stack.All())
}

func TestOptionsIgnoreCreatedBy(t *testing.T) {
stopCh := make(chan struct{})
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
<-stopCh
}()
defer close(stopCh)
defer func() {
close(stopCh)
<-doneCh
}()

cur := stack.Current()
opts := buildOpts(IgnoreCreatedBy("go.uber.org/goleak.TestOptionsIgnoreCreatedBy"))

for _, s := range stack.All() {
for _, s := range getStableAll(t, cur) {
if s.ID() == cur.ID() {
continue
}
Expand All @@ -95,10 +105,14 @@ func TestOptionsIgnoreCreatedBy(t *testing.T) {
}

func TestOptionsIgnoreAnyFunction(t *testing.T) {
bg := startBlockedG()
defer bg.unblock()

cur := stack.Current()
opts := buildOpts(IgnoreAnyFunction("go.uber.org/goleak.(*blockedG).run"))

for _, s := range stack.All() {
// Matching active stack frame (not at top)
opts := buildOpts(IgnoreAnyFunction("go.uber.org/goleak.(*blockedG).run"))
for _, s := range getStableAll(t, cur) {
if s.ID() == cur.ID() {
continue
}
Expand All @@ -109,6 +123,20 @@ func TestOptionsIgnoreAnyFunction(t *testing.T) {

t.Errorf("Unexpected goroutine: %v", s)
}

// Matching creator function
optsCreatedBy := buildOpts(IgnoreAnyFunction("go.uber.org/goleak.startBlockedG"))
for _, s := range getStableAll(t, cur) {
if s.ID() == cur.ID() {
continue
}

if optsCreatedBy.filter(s) {
continue
}

t.Errorf("Unexpected goroutine when filtering by creator: %v", s)
}
}

func TestOptionsRetry(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import (
type blockedG struct {
started chan struct{}
wait chan struct{}
done chan struct{}
}

func startBlockedG() *blockedG {
bg := &blockedG{
started: make(chan struct{}),
wait: make(chan struct{}),
done: make(chan struct{}),
}
go bg.run()
<-bg.started
Expand All @@ -45,6 +47,7 @@ func startBlockedG() *blockedG {

func (bg *blockedG) run() {
close(bg.started)
defer close(bg.done)
bg.block()
}

Expand All @@ -54,6 +57,8 @@ func (bg *blockedG) block() {

func (bg *blockedG) unblock() {
close(bg.wait)
<-bg.done
runtime.Gosched()
}

func getStableAll(t *testing.T, cur stack.Stack) []stack.Stack {
Expand Down