diff --git a/docs/index.md b/docs/index.md index cf84f79ae..d039c36b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -280,7 +280,7 @@ You can also configure the context in this way: Eventually(ACTUAL).WithTimeout(TIMEOUT).WithPolling(POLLING_INTERVAL).WithContext(ctx).Should(MATCHER) ``` -When no explicit timeout is provided, `Eventually` will use the default timeout. However if no explicit timeout is provided _and_ a context is provided, `Eventually` will not apply a timeout but will instead keep trying until the context is cancelled. If both a context and a timeout are provided, `Eventually` will keep trying until either the context is cancelled or time runs out, whichever comes first. +When no explicit timeout is provided, `Eventually` will use the default timeout. If both a context and a timeout are provided, `Eventually` will keep trying until either the context is cancelled or time runs out, whichever comes first. However if no explicit timeout is provided _and_ a context is provided, `Eventually` will not apply a timeout but will instead keep trying until the context is cancelled. This behavior is intentional in order to allow a single `context` to control the duration of a collection of `Eventually` assertions. To opt out of this behavior you can call the global `EnforceDefaultTimeoutsWhenUsingContexts()` configuration to force `Eventually` to apply a default timeout even when a context is provided. You can also ensure a number of consecutive pass before continuing with `MustPassRepeatedly`: @@ -588,6 +588,8 @@ SetDefaultConsistentlyPollingInterval(t time.Duration) You can also adjust these global timeouts by setting the `GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT`, `GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL`, `GOMEGA_DEFAULT_CONSISTENTLY_DURATION`, and `GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL` environment variables to a parseable duration string. The environment variables have a lower precedence than `SetDefault...()`. +As discussed [above](#category-2-making-eventually-assertions-on-functions) `Eventually`s that are passed a `context` object without an explicit timeout will only stop polling when the context is cancelled. If you would like to enforce the default timeout when a context is provided you can call `EnforceDefaultTimeoutsWhenUsingContexts()` (to go back to the default behavior call `DoNotEnforceDefaultTimeoutsWhenUsingContexts()`). You can also set the `GOMEGA_ENFORCE_DEFAULT_TIMEOUTS_WHEN_USING_CONTEXTS` environment variable to enforce the default timeout when a context is provided. + ## Making Assertions in Helper Functions While writing [custom matchers](#adding-your-own-matchers) is an expressive way to make assertions against your code, it is often more convenient to write one-off helper functions like so: diff --git a/gomega_dsl.go b/gomega_dsl.go index edacf8c13..74610d6bd 100644 --- a/gomega_dsl.go +++ b/gomega_dsl.go @@ -319,7 +319,19 @@ you an also use Eventually().WithContext(ctx) to pass in the context. Passed-in Eventually(client.FetchCount).WithContext(ctx).WithArguments("/users").Should(BeNumerically(">=", 17)) }, SpecTimeout(time.Second)) -Either way the context passd to Eventually is also passed to the underlying function. Now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit. +Either way the context pasesd to Eventually is also passed to the underlying function. Now, when Ginkgo cancels the context both the FetchCount client and Gomega will be informed and can exit. + +By default, when a context is passed to Eventually *without* an explicit timeout, Gomega will rely solely on the context's cancellation to determine when to stop polling. If you want to specify a timeout in addition to the context you can do so using the .WithTimeout() method. For example: + + Eventually(client.FetchCount).WithContext(ctx).WithTimeout(10*time.Second).Should(BeNumerically(">=", 17)) + +now either the context cacnellation or the timeout will cause Eventually to stop polling. + +If, instead, you would like to opt out of this behavior and have Gomega's default timeouts govern Eventuallys that take a context you can call: + + EnforceDefaultTimeoutsWhenUsingContexts() + +in the DSL (or on a Gomega instance). Now all calls to Eventually that take a context will fail if eitehr the context is cancelled or the default timeout elapses. **Category 3: Making assertions _in_ the function passed into Eventually** diff --git a/internal/async_assertion.go b/internal/async_assertion.go index cde9e2ec8..a368a16de 100644 --- a/internal/async_assertion.go +++ b/internal/async_assertion.go @@ -335,7 +335,7 @@ func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time { if assertion.asyncType == AsyncAssertionTypeConsistently { return time.After(assertion.g.DurationBundle.ConsistentlyDuration) } else { - if assertion.ctx == nil { + if assertion.ctx == nil || assertion.g.DurationBundle.EnforceDefaultTimeoutsWhenUsingContexts { return time.After(assertion.g.DurationBundle.EventuallyTimeout) } else { return nil diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go index c966dead2..3b52ad025 100644 --- a/internal/async_assertion_test.go +++ b/internal/async_assertion_test.go @@ -346,6 +346,26 @@ var _ = Describe("Asynchronous Assertions", func() { Ω(ig.FailureMessage).Should(ContainSubstring("Context was cancelled after")) }) + It("uses the default timeout if the user explicitly opts into EnforceDefaultTimeoutsWhenUsingContexts()", func() { + ig.G.SetDefaultEventuallyTimeout(time.Millisecond * 100) + ig.G.SetDefaultEventuallyPollingInterval(time.Millisecond * 10) + ig.G.EnforceDefaultTimeoutsWhenUsingContexts() + t := time.Now() + ctx, cancel := context.WithCancel(context.Background()) + iterations := 0 + ig.G.Eventually(func() string { + iterations += 1 + if time.Since(t) > time.Millisecond*1000 { + cancel() + } + return "A" + }).WithContext(ctx).Should(Equal("B")) + Ω(time.Since(t)).Should(BeNumerically("~", time.Millisecond*100, time.Millisecond*50)) + Ω(iterations).Should(BeNumerically("~", 100/10, 2)) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ctx.Err()).Should(BeNil()) + }) + It("uses the explicit timeout when it is provided", func() { t := time.Now() ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/duration_bundle.go b/internal/duration_bundle.go index 6e0d90d3a..2e026c336 100644 --- a/internal/duration_bundle.go +++ b/internal/duration_bundle.go @@ -8,10 +8,11 @@ import ( ) type DurationBundle struct { - EventuallyTimeout time.Duration - EventuallyPollingInterval time.Duration - ConsistentlyDuration time.Duration - ConsistentlyPollingInterval time.Duration + EventuallyTimeout time.Duration + EventuallyPollingInterval time.Duration + ConsistentlyDuration time.Duration + ConsistentlyPollingInterval time.Duration + EnforceDefaultTimeoutsWhenUsingContexts bool } const ( @@ -20,15 +21,19 @@ const ( ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION" ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL" + + EnforceDefaultTimeoutsWhenUsingContextsEnvVarName = "GOMEGA_ENFORCE_DEFAULT_TIMEOUTS_WHEN_USING_CONTEXTS" ) func FetchDefaultDurationBundle() DurationBundle { + _, EnforceDefaultTimeoutsWhenUsingContexts := os.LookupEnv(EnforceDefaultTimeoutsWhenUsingContextsEnvVarName) return DurationBundle{ EventuallyTimeout: durationFromEnv(EventuallyTimeoutEnvVarName, time.Second), EventuallyPollingInterval: durationFromEnv(EventuallyPollingIntervalEnvVarName, 10*time.Millisecond), - ConsistentlyDuration: durationFromEnv(ConsistentlyDurationEnvVarName, 100*time.Millisecond), - ConsistentlyPollingInterval: durationFromEnv(ConsistentlyPollingIntervalEnvVarName, 10*time.Millisecond), + ConsistentlyDuration: durationFromEnv(ConsistentlyDurationEnvVarName, 100*time.Millisecond), + ConsistentlyPollingInterval: durationFromEnv(ConsistentlyPollingIntervalEnvVarName, 10*time.Millisecond), + EnforceDefaultTimeoutsWhenUsingContexts: EnforceDefaultTimeoutsWhenUsingContexts, } } diff --git a/internal/duration_bundle_test.go b/internal/duration_bundle_test.go index aceeb043d..b053c2b31 100644 --- a/internal/duration_bundle_test.go +++ b/internal/duration_bundle_test.go @@ -43,6 +43,7 @@ var _ = Describe("DurationBundle and Duration Support", func() { Ω(bundle.EventuallyPollingInterval).Should(Equal(10 * time.Millisecond)) Ω(bundle.ConsistentlyDuration).Should(Equal(100 * time.Millisecond)) Ω(bundle.ConsistentlyPollingInterval).Should(Equal(10 * time.Millisecond)) + Ω(bundle.EnforceDefaultTimeoutsWhenUsingContexts).Should(BeFalse()) }) }) @@ -52,6 +53,7 @@ var _ = Describe("DurationBundle and Duration Support", func() { os.Setenv(internal.EventuallyPollingIntervalEnvVarName, "2s") os.Setenv(internal.ConsistentlyDurationEnvVarName, "1h") os.Setenv(internal.ConsistentlyPollingIntervalEnvVarName, "3ms") + os.Setenv(internal.EnforceDefaultTimeoutsWhenUsingContextsEnvVarName, "") }) It("returns an appropriate bundle", func() { @@ -60,6 +62,7 @@ var _ = Describe("DurationBundle and Duration Support", func() { Ω(bundle.EventuallyPollingInterval).Should(Equal(2 * time.Second)) Ω(bundle.ConsistentlyDuration).Should(Equal(time.Hour)) Ω(bundle.ConsistentlyPollingInterval).Should(Equal(3 * time.Millisecond)) + Ω(bundle.EnforceDefaultTimeoutsWhenUsingContexts).Should(BeTrue()) }) }) diff --git a/internal/gomega.go b/internal/gomega.go index de1f4f336..c6e2fcc0e 100644 --- a/internal/gomega.go +++ b/internal/gomega.go @@ -127,3 +127,11 @@ func (g *Gomega) SetDefaultConsistentlyDuration(t time.Duration) { func (g *Gomega) SetDefaultConsistentlyPollingInterval(t time.Duration) { g.DurationBundle.ConsistentlyPollingInterval = t } + +func (g *Gomega) EnforceDefaultTimeoutsWhenUsingContexts() { + g.DurationBundle.EnforceDefaultTimeoutsWhenUsingContexts = true +} + +func (g *Gomega) DisableDefaultTimeoutsWhenUsingContext() { + g.DurationBundle.EnforceDefaultTimeoutsWhenUsingContexts = false +}