diff --git a/leaks_test.go b/leaks_test.go index cd67c6a..1c69028 100644 --- a/leaks_test.go +++ b/leaks_test.go @@ -36,7 +36,7 @@ var _ = TestingT(testing.TB(nil)) // testOptions passes a shorter max sleep time, used so tests don't wait // ~1 second in cases where we expect Find to error out. func testOptions() Option { - return maxSleep(time.Millisecond) + return MaxSleep(time.Millisecond) } func TestFind(t *testing.T) { @@ -74,6 +74,41 @@ func TestFindRetry(t *testing.T) { require.NoError(t, Find(), "Find should retry while background goroutine ends") } +func TestFindMaxRetries(t *testing.T) { + t.Run("zero retries does not retry and fails immediately", func(t *testing.T) { + bg := startBlockedG() + defer bg.unblock() + + start := time.Now() + err := Find(MaxRetries(0)) + require.Error(t, err, "Should find leaks without retrying") + assert.Less(t, time.Since(start), 50*time.Millisecond, "Should return immediately with 0 retries") + }) + + t.Run("custom retries allows delayed goroutine exit", func(t *testing.T) { + bg := startBlockedG() + go func() { + time.Sleep(5 * time.Millisecond) + bg.unblock() + }() + + err := Find(MaxRetries(20), MaxSleep(5*time.Millisecond)) + require.NoError(t, err, "Find should retry and succeed when goroutine finishes") + }) +} + +func TestFindMaxSleep(t *testing.T) { + t.Run("shorter max sleep fails faster", func(t *testing.T) { + bg := startBlockedG() + defer bg.unblock() + + start := time.Now() + err := Find(MaxRetries(5), MaxSleep(time.Millisecond)) + require.Error(t, err) + assert.Less(t, time.Since(start), 100*time.Millisecond, "Find should return quickly with short max sleep") + }) +} + type fakeT struct { errors []string } @@ -103,6 +138,15 @@ func TestVerifyNone(t *testing.T) { })) require.True(t, cleanupCalled, "expect cleanup registered callback to be called") }) + + t.Run("VerifyNone with MaxRetries and MaxSleep", func(t *testing.T) { + ft := &fakeT{} + bg := startBlockedG() + defer bg.unblock() + + VerifyNone(ft, MaxRetries(1), MaxSleep(time.Millisecond)) + require.NotEmpty(t, ft.errors, "Expect errors from VerifyNone with custom MaxRetries and MaxSleep") + }) } func TestIgnoreCurrent(t *testing.T) { diff --git a/options.go b/options.go index 4fabae9..85521de 100644 --- a/options.go +++ b/options.go @@ -126,12 +126,29 @@ func RunOnFailure() Option { }) } -func maxSleep(d time.Duration) Option { +// MaxRetries sets the maximum retry attempts when looking for extra goroutines. +// When looking for leaking goroutines, goleak will retry until all goroutines +// terminate or until the maximum retry attempts are exhausted. +// If not configured, defaults to 20. +func MaxRetries(n int) Option { + return optionFunc(func(opts *opts) { + opts.maxRetries = n + }) +} + +// MaxSleep sets the maximum sleep duration in-between each retry attempt. +// The sleep duration grows in an exponential backoff, up to the maximum specified here. +// If not configured, defaults to 100 milliseconds. +func MaxSleep(d time.Duration) Option { return optionFunc(func(opts *opts) { opts.maxSleep = d }) } +func maxSleep(d time.Duration) Option { + return MaxSleep(d) +} + func addFilter(f func(stack.Stack) bool) Option { return optionFunc(func(opts *opts) { opts.filters = append(opts.filters, f) @@ -170,9 +187,12 @@ func (o *opts) retry(i int) bool { return false } - d := time.Duration(int(time.Microsecond) << uint(i)) - if d > o.maxSleep { - d = o.maxSleep + d := o.maxSleep + if i < 40 { + d = time.Duration(int(time.Microsecond) << uint(i)) + if d > o.maxSleep { + d = o.maxSleep + } } time.Sleep(d) return true diff --git a/options_test.go b/options_test.go index fd7751e..18bccbd 100644 --- a/options_test.go +++ b/options_test.go @@ -111,14 +111,29 @@ func TestOptionsIgnoreAnyFunction(t *testing.T) { } } -func TestOptionsRetry(t *testing.T) { +func TestMaxRetriesAndMaxSleep(t *testing.T) { opts := buildOpts() - opts.maxRetries = 50 // initial attempt + 50 retries = 11 - opts.maxSleep = time.Millisecond + assert.Equal(t, _defaultRetries, opts.maxRetries) + assert.Equal(t, 100*time.Millisecond, opts.maxSleep) + + opts = buildOpts(MaxRetries(50), MaxSleep(time.Millisecond)) + assert.Equal(t, 50, opts.maxRetries) + assert.Equal(t, time.Millisecond, opts.maxSleep) +} + +func TestOptionsRetry(t *testing.T) { + opts := buildOpts(MaxRetries(50), MaxSleep(time.Millisecond)) for i := 0; i < 50; i++ { - assert.True(t, opts.retry(i), "Attempt %v/51 should allow retrying", i) + assert.True(t, opts.retry(i), "Attempt %v/50 should allow retrying", i) } - assert.False(t, opts.retry(51), "Attempt 51/51 should not allow retrying") - assert.False(t, opts.retry(52), "Attempt 52/51 should not allow retrying") + assert.False(t, opts.retry(50), "Attempt 50/50 should not allow retrying") + assert.False(t, opts.retry(51), "Attempt 51/50 should not allow retrying") + assert.False(t, opts.retry(52), "Attempt 52/50 should not allow retrying") +} + +func TestOptionsRetryLargeAttempt(t *testing.T) { + opts := buildOpts(MaxRetries(100), MaxSleep(time.Millisecond)) + assert.True(t, opts.retry(65), "Attempt 65 should not overflow") + assert.False(t, opts.retry(100), "Attempt 100 should not allow retrying") }