From eeca931c42d3e00f631b64d883b8c103b414105a Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Tue, 29 Oct 2024 14:09:37 -0600 Subject: [PATCH] Add Successfully() to StopTrying() to signal that Consistently can end early without failure fixes #786 --- docs/index.md | 18 +++++++++++++++++- internal/async_assertion.go | 10 +++++++++- internal/async_assertion_test.go | 26 +++++++++++++++++++++++++- internal/polling_signal_error.go | 11 +++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index d039c36b8..e9cdaa80e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 `: `. diff --git a/internal/async_assertion.go b/internal/async_assertion.go index a368a16de..8b4cd1f5b 100644 --- a/internal/async_assertion.go +++ b/internal/async_assertion.go @@ -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() { diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go index 3b52ad025..4eb90aaa1 100644 --- a/internal/async_assertion_test.go +++ b/internal/async_assertion_test.go @@ -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() { @@ -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 { @@ -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() { diff --git a/internal/polling_signal_error.go b/internal/polling_signal_error.go index 83b04b1a4..3a4f7ddd9 100644 --- a/internal/polling_signal_error.go +++ b/internal/polling_signal_error.go @@ -17,6 +17,7 @@ type PollingSignalError interface { error Wrap(err error) PollingSignalError Attach(description string, obj any) PollingSignalError + Successfully() PollingSignalError Now() } @@ -45,6 +46,7 @@ type PollingSignalErrorImpl struct { wrappedErr error pollingSignalErrorType PollingSignalErrorType duration time.Duration + successful bool Attachments []PollingSignalErrorAttachment } @@ -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) } @@ -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 }