Skip to content

Commit

Permalink
Add Successfully() to StopTrying() to signal that Consistently can en…
Browse files Browse the repository at this point in the history
…d early without failure

fixes #786
  • Loading branch information
onsi committed Oct 29, 2024
1 parent 3bdbc4e commit eeca931
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 3 deletions.
18 changes: 17 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,23 @@ calling `.Now()` will trigger a panic that will signal to `Eventually` that it s

You can also return `StopTrying()` errors and use `StopTrying().Now()` with `Consistently`.

Both `Eventually` and `Consistently` always treat the `StopTrying()` signal as a failure. The failure message will include the message passed in to `StopTrying()`.
By default, both `Eventually` and `Consistently` treat the `StopTrying()` signal as a failure. The failure message will include the message passed in to `StopTrying()`. However, there are cases when you might want to short-circuit `Consistently` early without failing the test (e.g. you are using consistently to monitor the sideeffect of a goroutine and that goroutine has now ended. Once it ends there is no need to continue polling `Consistently`). In this case you can use `StopTrying(message).Successfully()` to signal that `Consistently` can end early without failing. For example:

```
Consistently(func() bool {
select{
case err := <-done: //the process has ended
if err != nil {
return StopTrying("error occurred").Now()
}
StopTrying("success!).Successfully().Now()
default:
return GetCounts()
}
}).Should(BeNumerically("<", 10))
```

note taht `StopTrying(message).Successfully()` is not intended for use with `Eventually`. `Eventually` *always* interprets `StopTrying` as a failure.

You can add additional information to this failure message in a few ways. You can wrap an error via `StopTrying(message).Wrap(wrappedErr)` - now the output will read `<message>: <wrappedErr.Error()>`.

Expand Down
10 changes: 9 additions & 1 deletion internal/async_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,15 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
for _, err := range []error{actualErr, matcherErr} {
if pollingSignalErr, ok := AsPollingSignalError(err); ok {
if pollingSignalErr.IsStopTrying() {
fail("Told to stop trying")
if pollingSignalErr.IsSuccessful() {
if assertion.asyncType == AsyncAssertionTypeEventually {
fail("Told to stop trying (and ignoring call to Successfully(), as it is only relevant with Consistently)")
} else {
return true // early escape hatch for Consistently
}
} else {
fail("Told to stop trying")
}
return false
}
if pollingSignalErr.IsTryAgainAfter() {
Expand Down
26 changes: 25 additions & 1 deletion internal/async_assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,19 @@ var _ = Describe("Asynchronous Assertions", func() {
Ω(ig.FailureMessage).Should(ContainSubstring("Told to stop trying after"))
Ω(ig.FailureMessage).Should(ContainSubstring("bam"))
})

It("fails, even if the match were to happen to succeed and the user uses Succeed", func() {
ig.G.Eventually(func() (int, error) {
i += 1
if i < 3 {
return i, nil
}
return i, StopTrying("bam").Successfully()
}).Should(Equal(3))
Ω(i).Should(Equal(3))
Ω(ig.FailureMessage).Should(ContainSubstring("Told to stop trying (and ignoring call to Successfully(), as it is only relevant with Consistently) after"))
Ω(ig.FailureMessage).Should(ContainSubstring("bam"))
})
})

Context("when returned as the sole actual", func() {
Expand Down Expand Up @@ -1278,7 +1291,7 @@ var _ = Describe("Asynchronous Assertions", func() {
})

Context("when used with consistently", func() {
It("always signifies a failure", func() {
It("signifies a failure", func() {
ig.G.Consistently(func() (int, error) {
i += 1
if i >= 3 {
Expand All @@ -1290,6 +1303,17 @@ var _ = Describe("Asynchronous Assertions", func() {
Ω(ig.FailureMessage).Should(ContainSubstring("Told to stop trying after"))
Ω(ig.FailureMessage).Should(ContainSubstring("bam"))
})

It("signifies success when called Successfully", func() {
Consistently(func() (int, error) {
i += 1
if i >= 3 {
return i, StopTrying("bam").Successfully()
}
return i, nil
}).Should(BeNumerically("<", 10))
Ω(i).Should(Equal(3))
})
})

Context("when StopTrying has attachments", func() {
Expand Down
11 changes: 11 additions & 0 deletions internal/polling_signal_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type PollingSignalError interface {
error
Wrap(err error) PollingSignalError
Attach(description string, obj any) PollingSignalError
Successfully() PollingSignalError
Now()
}

Expand Down Expand Up @@ -45,6 +46,7 @@ type PollingSignalErrorImpl struct {
wrappedErr error
pollingSignalErrorType PollingSignalErrorType
duration time.Duration
successful bool
Attachments []PollingSignalErrorAttachment
}

Expand Down Expand Up @@ -73,6 +75,11 @@ func (s *PollingSignalErrorImpl) Unwrap() error {
return s.wrappedErr
}

func (s *PollingSignalErrorImpl) Successfully() PollingSignalError {
s.successful = true
return s
}

func (s *PollingSignalErrorImpl) Now() {
panic(s)
}
Expand All @@ -81,6 +88,10 @@ func (s *PollingSignalErrorImpl) IsStopTrying() bool {
return s.pollingSignalErrorType == PollingSignalErrorTypeStopTrying
}

func (s *PollingSignalErrorImpl) IsSuccessful() bool {
return s.successful
}

func (s *PollingSignalErrorImpl) IsTryAgainAfter() bool {
return s.pollingSignalErrorType == PollingSignalErrorTypeTryAgainAfter
}
Expand Down

0 comments on commit eeca931

Please sign in to comment.