Skip to content

Commit

Permalink
Add EnforceDefaultTimeoutsWhenUsingContexts()
Browse files Browse the repository at this point in the history
Resolves #781
  • Loading branch information
onsi committed Oct 29, 2024
1 parent 7cabed6 commit e4c4265
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 9 deletions.
4 changes: 3 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down Expand Up @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion gomega_dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
2 changes: 1 addition & 1 deletion internal/async_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions internal/async_assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
17 changes: 11 additions & 6 deletions internal/duration_bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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,
}
}

Expand Down
3 changes: 3 additions & 0 deletions internal/duration_bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})

Expand All @@ -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() {
Expand All @@ -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())
})
})

Expand Down
8 changes: 8 additions & 0 deletions internal/gomega.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit e4c4265

Please sign in to comment.