diff --git a/leaks.go b/leaks.go index cc206f1..06ea736 100644 --- a/leaks.go +++ b/leaks.go @@ -23,10 +23,23 @@ package goleak import ( "errors" "fmt" + "strings" "go.uber.org/goleak/internal/stack" ) +// DefaultIgnoreFunctionSet can be set to a comma-separated list +// of functions that may leak and should be ignored by goleak. +// The registered set of functions will be then used similar to parameters +// passed to [IgnoreAnyFunction]: any goroutine leaks with the given function +// name(s) will be ignored by goleak. +// This is helpful for library or tool authors that "owns" the leaking +// goroutine and do not have explicit control over how the tests are run +// by their consumers. +// Unless you are in such a situation, you should be using [IgnoreAnyFunction], +// [IgnoreCurrent], or [IgnoreTopFunction] instead. +var DefaultIgnoreFunctionSet string + // TestingT is the minimal subset of testing.TB that we use. type TestingT interface { Error(...interface{}) @@ -50,15 +63,30 @@ func filterStacks(stacks []stack.Stack, skipID int, opts *opts) []stack.Stack { return filtered } +func parseDefaultIgnoreFunctions() []Option { + // parse DefaultIgnoreFunctionSet and add it to filters + funcs := strings.Split(DefaultIgnoreFunctionSet, ",") + opts := make([]Option, 0, len(funcs)) + for _, f := range funcs { + if f != "" { + opts = append(opts, IgnoreAnyFunction(f)) + } + } + return opts +} + // Find looks for extra goroutines, and returns a descriptive error if // any are found. func Find(options ...Option) error { cur := stack.Current().ID() + options = append(options, parseDefaultIgnoreFunctions()...) + opts := buildOpts(options...) if opts.cleanup != nil { return errors.New("Cleanup can only be passed to VerifyNone or VerifyTestMain") } + var stacks []stack.Stack retry := true for i := 0; retry; i++ { diff --git a/leaks_test.go b/leaks_test.go index 992c85b..55fc28f 100644 --- a/leaks_test.go +++ b/leaks_test.go @@ -201,3 +201,50 @@ func TestVerifyParallel(t *testing.T) { VerifyNone(t) }) } + +func TestDefaultIgnoreFunctionSet(t *testing.T) { + t.Run("single function", func(t *testing.T) { + done := make(chan struct{}) + go func() { + <-done + }() + require.Error(t, Find(), "expected the leaking goroutine to get flagged") + + DefaultIgnoreFunctionSet = "go.uber.org/goleak.TestDefaultIgnoreFunctionSet.func1.1" + assert.Equal(t, 1, len(parseDefaultIgnoreFunctions())) + assert.NoError(t, Find(), "expected the goroutine to get ignored after setting DefaultIgnoreFunctionSet") + + DefaultIgnoreFunctionSet = "" + assert.Error(t, Find(), "expected the leaking goroutine to get flagged again after resetting DefaultIgnoreFunctionSet") + + close(done) + assert.NoError(t, Find()) + }) + + t.Run("many functions", func(t *testing.T) { + bg := startBlockedG() + bg2 := blockedG2() + + require.Error(t, Find(), "expected the leaking goroutine to get flagged") + + DefaultIgnoreFunctionSet = "go.uber.org/goleak.(*blockedG).block,go.uber.org/goleak.blockedG2.func1" + + assert.Equal(t, 2, len(parseDefaultIgnoreFunctions()), "expected 2 filters to be added with DefaultIgnoreFunctionSet") + assert.NoError(t, Find(), "expected the goroutine to get ignored after setting DefaultIgnoreFunctionSet") + + DefaultIgnoreFunctionSet = "" + assert.Error(t, Find(), "expected the leaking goroutine to get flagged again after resetting DefaultIgnoreFunctionSet") + + bg.unblock() + close(bg2) + assert.NoError(t, Find()) + }) +} + +func blockedG2() chan struct{} { + ch := make(chan struct{}) + go func() { + <-ch + }() + return ch +}