diff --git a/docs/index.md b/docs/index.md index c28b3b338..19022005a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -276,6 +276,12 @@ Eventually(ACTUAL).WithTimeout(TIMEOUT).WithPolling(POLLING_INTERVAL).WithContex 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. +You can also ensure a number of consecutive pass before continuing with `MustPassRepeatedly`: + +```go +Eventually(ACTUAL).MustPassRepeatedly(NUMBER).Should(MATCHER) +``` + Eventually works with any Gomega compatible matcher and supports making assertions against three categories of `ACTUAL` value: #### Category 1: Making `Eventually` assertions on values diff --git a/gomega_dsl.go b/gomega_dsl.go index b65c8be9b..e60055b22 100644 --- a/gomega_dsl.go +++ b/gomega_dsl.go @@ -360,6 +360,16 @@ You can also pass additional arugments to functions that take a Gomega. The onl g.Expect(elements).To(ConsistOf(expected)) }).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed()) +You can ensure that you get a number of consecutive successful tries before succeeding using `MustPassRepeatedly(int)`. For Example: + + int count := 0 + Eventually(func() bool { + count++ + return count > 2 + }).MustPassRepeatedly(2).Should(BeTrue()) + // Because we had to wait for 2 calls that returned true + Expect(count).To(Equal(3)) + Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods: Eventually(..., "1s", "2s", ctx).Should(...) diff --git a/internal/async_assertion.go b/internal/async_assertion.go index cc8615a11..1f6fc13b0 100644 --- a/internal/async_assertion.go +++ b/internal/async_assertion.go @@ -55,21 +55,23 @@ type AsyncAssertion struct { actual interface{} argsToForward []interface{} - timeoutInterval time.Duration - pollingInterval time.Duration - ctx context.Context - offset int - g *Gomega + timeoutInterval time.Duration + pollingInterval time.Duration + mustPassRepeatedly int + ctx context.Context + offset int + g *Gomega } -func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, ctx context.Context, offset int) *AsyncAssertion { +func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, mustPassRepeatedly int, ctx context.Context, offset int) *AsyncAssertion { out := &AsyncAssertion{ - asyncType: asyncType, - timeoutInterval: timeoutInterval, - pollingInterval: pollingInterval, - offset: offset, - ctx: ctx, - g: g, + asyncType: asyncType, + timeoutInterval: timeoutInterval, + pollingInterval: pollingInterval, + mustPassRepeatedly: mustPassRepeatedly, + offset: offset, + ctx: ctx, + g: g, } out.actual = actualInput @@ -115,6 +117,11 @@ func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) typ return assertion } +func (assertion *AsyncAssertion) MustPassRepeatedly(count int) types.AsyncAssertion { + assertion.mustPassRepeatedly = count + return assertion +} + func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { assertion.g.THelper() vetOptionalDescription("Asynchronous assertion", optionalDescription...) @@ -202,6 +209,13 @@ You can learn more at https://onsi.github.io/gomega/#eventually `, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType) } +func (assertion *AsyncAssertion) invalidMustPassRepeatedlyError(reason string) error { + return fmt.Errorf(`Invalid use of MustPassRepeatedly with %s %s + +You can learn more at https://onsi.github.io/gomega/#eventually +`, assertion.asyncType, reason) +} + func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) { if !assertion.actualIsFunc { return func() (interface{}, error) { return assertion.actual, nil }, nil @@ -257,6 +271,13 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error return nil, assertion.argumentMismatchError(actualType, len(inValues)) } + if assertion.mustPassRepeatedly != 1 && assertion.asyncType != AsyncAssertionTypeEventually { + return nil, assertion.invalidMustPassRepeatedlyError("it can only be used with Eventually") + } + if assertion.mustPassRepeatedly < 1 { + return nil, assertion.invalidMustPassRepeatedlyError("parameter can't be < 1") + } + return func() (actual interface{}, err error) { var values []reflect.Value assertionFailure = nil @@ -396,6 +417,8 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch } } + // Used to count the number of times in a row a step passed + passedRepeatedlyCount := 0 for { var nextPoll <-chan time.Time = nil var isTryAgainAfterError = false @@ -413,13 +436,18 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch if err == nil && matches == desiredMatch { if assertion.asyncType == AsyncAssertionTypeEventually { - return true + passedRepeatedlyCount += 1 + if passedRepeatedlyCount == assertion.mustPassRepeatedly { + return true + } } } else if !isTryAgainAfterError { if assertion.asyncType == AsyncAssertionTypeConsistently { fail("Failed") return false } + // Reset the consecutive pass count + passedRepeatedlyCount = 0 } if oracleMatcherSaysStop { diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go index f58509cba..e1c0cf079 100644 --- a/internal/async_assertion_test.go +++ b/internal/async_assertion_test.go @@ -1452,7 +1452,7 @@ sprocket: Ω(times[4]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10)) }) - It("doesn count as a failure if a timeout occurs during the try again after window", func() { + It("doesn't count as a failure if a timeout occurs during the try again after window", func() { ig.G.Consistently(func() (int, error) { times = append(times, time.Since(t)) t = time.Now() @@ -1512,4 +1512,51 @@ sprocket: }).NotTo(Panic()) }) }) + + When("using MustPassRepeatedly", func() { + It("errors when using on Consistently", func() { + ig.G.Consistently(func(g Gomega) {}).MustPassRepeatedly(2).Should(Succeed()) + Ω(ig.FailureMessage).Should(ContainSubstring("Invalid use of MustPassRepeatedly with Consistently it can only be used with Eventually")) + Ω(ig.FailureSkip).Should(Equal([]int{2})) + }) + It("errors when using with 0", func() { + ig.G.Eventually(func(g Gomega) {}).MustPassRepeatedly(0).Should(Succeed()) + Ω(ig.FailureMessage).Should(ContainSubstring("Invalid use of MustPassRepeatedly with Eventually parameter can't be < 1")) + Ω(ig.FailureSkip).Should(Equal([]int{2})) + }) + + It("should wait 2 success before success", func() { + counter := 0 + ig.G.Eventually(func() bool { + counter++ + return counter > 5 + }).MustPassRepeatedly(2).Should(BeTrue()) + Ω(counter).Should(Equal(7)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("should fail if it never succeeds twice in a row", func() { + counter := 0 + ig.G.Eventually(func() int { + counter++ + return counter % 2 + }).WithTimeout(200 * time.Millisecond).WithPolling(20 * time.Millisecond).MustPassRepeatedly(2).Should(Equal(1)) + Ω(counter).Should(Equal(10)) + Ω(ig.FailureMessage).ShouldNot(BeZero()) + }) + + It("TryAgainAfter doesn't restore count", func() { + counter := 0 + ig.G.Eventually(func() (bool, error) { + counter++ + if counter == 5 { + return false, TryAgainAfter(time.Millisecond * 200) + } + return counter >= 4, nil + }).MustPassRepeatedly(3).Should(BeTrue()) + Ω(counter).Should(Equal(7)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + }) }) diff --git a/internal/gomega.go b/internal/gomega.go index 2d92877f3..de1f4f336 100644 --- a/internal/gomega.go +++ b/internal/gomega.go @@ -109,7 +109,7 @@ func (g *Gomega) makeAsyncAssertion(asyncAssertionType AsyncAssertionType, offse } } - return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, ctx, offset) + return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, 1, ctx, offset) } func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) { diff --git a/types/types.go b/types/types.go index 125de6497..7c7adb941 100644 --- a/types/types.go +++ b/types/types.go @@ -75,6 +75,7 @@ type AsyncAssertion interface { ProbeEvery(interval time.Duration) AsyncAssertion WithContext(ctx context.Context) AsyncAssertion WithArguments(argsToForward ...interface{}) AsyncAssertion + MustPassRepeatedly(count int) AsyncAssertion } // Assertions are returned by Ω and Expect and enable assertions against Gomega matchers