Skip to content

Commit

Permalink
add MustPassRepeatedly(int) to asyncAssertion (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahabana authored Jan 17, 2023
1 parent c7cfea4 commit 4509f72
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 15 deletions.
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions gomega_dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(...)
Expand Down
54 changes: 41 additions & 13 deletions internal/async_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
49 changes: 48 additions & 1 deletion internal/async_assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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())
})

})
})
2 changes: 1 addition & 1 deletion internal/gomega.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4509f72

Please sign in to comment.