diff --git a/leaks_test.go b/leaks_test.go index cd67c6a..1d5c65a 100644 --- a/leaks_test.go +++ b/leaks_test.go @@ -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) { @@ -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 + }() +} + diff --git a/options.go b/options.go index 4fabae9..093cd69 100644 --- a/options.go +++ b/options.go @@ -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., // @@ -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 }) } diff --git a/options_test.go b/options_test.go index fd7751e..176a5d7 100644 --- a/options_test.go +++ b/options_test.go @@ -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 } @@ -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 } @@ -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) { diff --git a/utils_test.go b/utils_test.go index 5504774..ba564f1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -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 @@ -45,6 +47,7 @@ func startBlockedG() *blockedG { func (bg *blockedG) run() { close(bg.started) + defer close(bg.done) bg.block() } @@ -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 {