diff --git a/internal/async_assertion.go b/internal/async_assertion.go index 1f6fc13b0..251b6b14d 100644 --- a/internal/async_assertion.go +++ b/internal/async_assertion.go @@ -2,6 +2,7 @@ package internal import ( "context" + "errors" "fmt" "reflect" "runtime" @@ -16,6 +17,22 @@ var errInterface = reflect.TypeOf((*error)(nil)).Elem() var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem() var contextType = reflect.TypeOf(new(context.Context)).Elem() +type formattedGomegaError interface { + FormattedGomegaError() string +} + +type asyncPolledActualError struct { + message string +} + +func (err *asyncPolledActualError) Error() string { + return err.message +} + +func (err *asyncPolledActualError) FormattedGomegaError() string { + return err.message +} + type contextWithAttachProgressReporter interface { AttachProgressReporter(func() string) func() } @@ -148,7 +165,9 @@ func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interfa func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (interface{}, error) { if len(values) == 0 { - return nil, fmt.Errorf("No values were returned by the function passed to Gomega") + return nil, &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s did not return any values", assertion.asyncType), + } } actual := values[0].Interface() @@ -171,10 +190,12 @@ func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (in continue } if i == len(values)-2 && extraType.Implements(errInterface) { - err = fmt.Errorf("function returned error: %w", extra.(error)) + err = extra.(error) } if err == nil { - err = fmt.Errorf("Unexpected non-nil/non-zero return value at index %d:\n\t<%T>: %#v", i+1, extra, extra) + err = &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s had an unexpected non-nil/non-zero return value at index %d:\n%s", assertion.asyncType, i+1, format.Object(extra, 1)), + } } } @@ -253,7 +274,9 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error skip = callerSkip[0] } _, file, line, _ := runtime.Caller(skip + 1) - assertionFailure = fmt.Errorf("Assertion in callback at %s:%d failed:\n%s", file, line, message) + assertionFailure = &asyncPolledActualError{ + message: fmt.Sprintf("The function passed to %s failed at %s:%d with:\n%s", assertion.asyncType, file, line, message), + } // we throw an asyncGomegaHaltExecutionError so that defer GinkgoRecover() can catch this error if the user makes an assertion in a goroutine panic(asyncGomegaHaltExecutionError{}) }))) @@ -359,22 +382,39 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch timeout := assertion.afterTimeout() lock := sync.Mutex{} - var matches bool - var err error + var matches, hasLastValidActual bool + var actual, lastValidActual interface{} + var actualErr, matcherErr error var oracleMatcherSaysStop bool assertion.g.THelper() - pollActual, err := assertion.buildActualPoller() - if err != nil { - assertion.g.Fail(err.Error(), 2+assertion.offset) + pollActual, buildActualPollerErr := assertion.buildActualPoller() + if buildActualPollerErr != nil { + assertion.g.Fail(buildActualPollerErr.Error(), 2+assertion.offset) return false } - value, err := pollActual() - if err == nil { - oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, value) - matches, err = assertion.pollMatcher(matcher, value) + actual, actualErr = pollActual() + if actualErr == nil { + lastValidActual = actual + hasLastValidActual = true + oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual) + matches, matcherErr = assertion.pollMatcher(matcher, actual) + } + + renderError := func(preamble string, err error) string { + message := "" + if pollingSignalErr, ok := AsPollingSignalError(err); ok { + message = err.Error() + for _, attachment := range pollingSignalErr.Attachments { + message += fmt.Sprintf("\n%s:\n", attachment.Description) + message += format.Object(attachment.Object, 1) + } + } else { + message = preamble + "\n" + err.Error() + "\n" + format.Object(err, 1) + } + return message } messageGenerator := func() string { @@ -382,23 +422,45 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch lock.Lock() defer lock.Unlock() message := "" - if err != nil { - if pollingSignalErr, ok := AsPollingSignalError(err); ok && pollingSignalErr.IsStopTrying() { - message = err.Error() - for _, attachment := range pollingSignalErr.Attachments { - message += fmt.Sprintf("\n%s:\n", attachment.Description) - message += format.Object(attachment.Object, 1) + + if actualErr == nil { + if matcherErr == nil { + if desiredMatch { + message += matcher.FailureMessage(actual) + } else { + message += matcher.NegatedFailureMessage(actual) } } else { - message = "Error: " + err.Error() + "\n" + format.Object(err, 1) + var fgErr formattedGomegaError + if errors.As(actualErr, &fgErr) { + message += fgErr.FormattedGomegaError() + "\n" + } else { + message += renderError(fmt.Sprintf("The matcher passed to %s returned the following error:", assertion.asyncType), matcherErr) + } } } else { - if desiredMatch { - message = matcher.FailureMessage(value) + var fgErr formattedGomegaError + if errors.As(actualErr, &fgErr) { + message += fgErr.FormattedGomegaError() + "\n" } else { - message = matcher.NegatedFailureMessage(value) + message += renderError(fmt.Sprintf("The function passed to %s returned the following error:", assertion.asyncType), actualErr) + } + if hasLastValidActual { + message += fmt.Sprintf("\nAt one point, however, the function did return successfully. But %s failed because", assertion.asyncType) + _, e := matcher.Match(lastValidActual) + if e != nil { + message += renderError(" the matcher returned the following error:", e) + } else { + message += " the matcher was not satisfied:\n" + if desiredMatch { + message += matcher.FailureMessage(lastValidActual) + } else { + message += matcher.NegatedFailureMessage(lastValidActual) + } + } } } + description := assertion.buildDescription(optionalDescription...) return fmt.Sprintf("%s%s", description, message) } @@ -423,18 +485,20 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch var nextPoll <-chan time.Time = nil var isTryAgainAfterError = false - if pollingSignalErr, ok := AsPollingSignalError(err); ok { - if pollingSignalErr.IsStopTrying() { - fail("Told to stop trying") - return false - } - if pollingSignalErr.IsTryAgainAfter() { - nextPoll = time.After(pollingSignalErr.TryAgainDuration()) - isTryAgainAfterError = true + for _, err := range []error{actualErr, matcherErr} { + if pollingSignalErr, ok := AsPollingSignalError(err); ok { + if pollingSignalErr.IsStopTrying() { + fail("Told to stop trying") + return false + } + if pollingSignalErr.IsTryAgainAfter() { + nextPoll = time.After(pollingSignalErr.TryAgainDuration()) + isTryAgainAfterError = true + } } } - if err == nil && matches == desiredMatch { + if actualErr == nil && matcherErr == nil && matches == desiredMatch { if assertion.asyncType == AsyncAssertionTypeEventually { passedRepeatedlyCount += 1 if passedRepeatedlyCount == assertion.mustPassRepeatedly { @@ -465,15 +529,19 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch select { case <-nextPoll: - v, e := pollActual() + a, e := pollActual() lock.Lock() - value, err = v, e + actual, actualErr = a, e lock.Unlock() - if err == nil { - oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, value) - m, e := assertion.pollMatcher(matcher, value) + if actualErr == nil { + lock.Lock() + lastValidActual = actual + hasLastValidActual = true + lock.Unlock() + oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual) + m, e := assertion.pollMatcher(matcher, actual) lock.Lock() - matches, err = m, e + matches, matcherErr = m, e lock.Unlock() } case <-contextDone: diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go index e1c0cf079..b12720fec 100644 --- a/internal/async_assertion_test.go +++ b/internal/async_assertion_test.go @@ -23,11 +23,11 @@ func (q quickMatcher) Match(actual any) (bool, error) { } func (q quickMatcher) FailureMessage(actual any) (message string) { - return "QM failure message" + return fmt.Sprintf("QM failure message: %v", actual) } func (q quickMatcher) NegatedFailureMessage(actual any) (message string) { - return "QM negated failure message" + return fmt.Sprintf("QM negated failure message: %v", actual) } func (q quickMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { @@ -183,7 +183,7 @@ var _ = Describe("Asynchronous Assertions", func() { It("renders the matcher's error if an error occured", func() { ig.G.Eventually(ERR_MATCH).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch()) Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + Ω(ig.FailureMessage).Should(ContainSubstring("The matcher passed to Eventually returned the following error:\nspec matcher error")) }) It("renders the optional description", func() { @@ -355,7 +355,7 @@ var _ = Describe("Asynchronous Assertions", func() { }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch()) Ω(counter).Should(Equal(3)) Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + Ω(ig.FailureMessage).Should(ContainSubstring("The matcher passed to Consistently returned the following error:\nspec matcher error")) }) It("fails if the matcher doesn't match at any point", func() { @@ -396,7 +396,7 @@ var _ = Describe("Asynchronous Assertions", func() { }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(SpecMatch()) Ω(counter).Should(Equal(3)) Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + Ω(ig.FailureMessage).Should(ContainSubstring("spec matcher error")) }) It("fails if the matcher matches at any point", func() { @@ -427,7 +427,7 @@ var _ = Describe("Asynchronous Assertions", func() { It("renders the matcher's error if an error occured", func() { ig.G.Consistently(ERR_MATCH).Should(SpecMatch()) Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + Ω(ig.FailureMessage).Should(ContainSubstring("The matcher passed to Consistently returned the following error:\nspec matcher error")) }) It("renders the optional description", func() { @@ -563,15 +563,15 @@ var _ = Describe("Asynchronous Assertions", func() { ig.G.Eventually(func() (int, string, Foo, error) { return 1, "", Foo{Bar: "hi"}, nil }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero return value at index 2:")) - Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"hi"}`)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually had an unexpected non-nil/non-zero return value at index 2:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`: {Bar: "hi"}`)) }) It("has a meaningful message if all the return values are zero except the final return value, and it is an error", func() { ig.G.Eventually(func() (int, string, Foo, error) { return 1, "", Foo{}, errors.New("welp!") }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: function returned error: welp!")) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually returned the following error:\nwelp!")) }) Context("when making a ShouldNot assertion", func() { @@ -616,8 +616,8 @@ var _ = Describe("Asynchronous Assertions", func() { } return counter, s, f, err }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero return value at index 2:")) - Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"welp"}`)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently had an unexpected non-nil/non-zero return value at index 2:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`: {Bar: "welp"}`)) Ω(counter).Should(Equal(3)) }) @@ -643,8 +643,8 @@ var _ = Describe("Asynchronous Assertions", func() { } return counter, s, f, err }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(BeNumerically(">", 100)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero return value at index 1:")) - Ω(ig.FailureMessage).Should(ContainSubstring(`: "welp"`)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently had an unexpected non-nil/non-zero return value at index 1:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`: welp`)) Ω(counter).Should(Equal(3)) }) }) @@ -682,7 +682,7 @@ var _ = Describe("Asynchronous Assertions", func() { g.Expect(false).To(BeTrue()) return 10 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Equal(10)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually failed at %s:%d with:", file, line+2)) Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) }) @@ -742,9 +742,22 @@ var _ = Describe("Asynchronous Assertions", func() { g.Expect(false).To(BeTrue()) return 9 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Equal(10)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually failed at %s:%d with:", file, line+2)) Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) }) + + It("shows the state of the last match if there was a non-failing funciton at some point", func() { + counter := 0 + _, file, line, _ := runtime.Caller(0) + ig.G.Eventually(func(g Gomega) int { + counter += 1 + g.Expect(counter).To(BeNumerically("<", 3)) + return counter + }).WithTimeout(100 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Equal(10)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually failed at %s:%d with:\nExpected\n : ", file, line+3)) + Ω(ig.FailureMessage).Should(ContainSubstring("to be <\n : 3")) + Ω(ig.FailureMessage).Should(ContainSubstring("At one point, however, the function did return successfully. But Eventually failed because the matcher was not satisfied:\nExpected\n : 2\nto equal\n : 10")) + }) }) Context("with Consistently", func() { @@ -773,7 +786,7 @@ var _ = Describe("Asynchronous Assertions", func() { } return counter, s, f, err }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently failed at %s:%d with:", file, line+5)) Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) Ω(counter).Should(Equal(3)) }) @@ -809,7 +822,7 @@ var _ = Describe("Asynchronous Assertions", func() { } return 9 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Equal(10)) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5)) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently failed at %s:%d with:", file, line+5)) Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) }) }) @@ -841,7 +854,7 @@ var _ = Describe("Asynchronous Assertions", func() { } }).WithTimeout(100 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Succeed()) Ω(counter).Should(BeNumerically(">", 1)) - Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error")) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Eventually failed at")) Ω(ig.FailureMessage).Should(ContainSubstring(": false")) Ω(ig.FailureMessage).Should(ContainSubstring("to be true")) Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop")) @@ -890,7 +903,7 @@ var _ = Describe("Asynchronous Assertions", func() { g.Expect("bloop").To(Equal("blarp")) } }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Succeed()) - Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error")) + Ω(ig.FailureMessage).Should(ContainSubstring("The function passed to Consistently failed at")) Ω(ig.FailureMessage).Should(ContainSubstring(": false")) Ω(ig.FailureMessage).Should(ContainSubstring("to be true")) Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop")) @@ -1261,6 +1274,7 @@ sprocket: Attach("sprocket", 17)) }).Should(Succeed()) Ω(i).Should(Equal(1)) + Ω(ig.FailureMessage).ShouldNot(ContainSubstring("The function passed to")) Ω(ig.FailureMessage).Should(ContainSubstring("Told to stop trying after")) Ω(ig.FailureMessage).Should(ContainSubstring(`wizz: bam: boom widget: @@ -1276,7 +1290,7 @@ sprocket: ig.G.Eventually(func() (int, error) { return 0, fmt.Errorf("bam") }).WithTimeout(10 * time.Millisecond).Should(Equal(1)) - Ω(ig.FailureMessage).Should(ContainSubstring("*fmt.wrapError")) + Ω(ig.FailureMessage).Should(ContainSubstring(`{s: "bam"}`)) }) }) }) @@ -1430,7 +1444,8 @@ sprocket: Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10)) Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: told to try again after 10s: bam")) + Ω(ig.FailureMessage).Should(ContainSubstring("told to try again after 10s: bam")) + Ω(ig.FailureMessage).Should(ContainSubstring("At one point, however, the function did return successfully. But Eventually failed because the matcher was not satisfied:\nExpected\n : 2\nto equal\n : 4")) }) }) @@ -1466,7 +1481,140 @@ sprocket: Ω(times[1]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10)) Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10)) Ω(ig.FailureMessage).Should(ContainSubstring("Timed out while waiting on TryAgainAfter after")) - Ω(ig.FailureMessage).Should(ContainSubstring("Error: told to try again after 10s: bam")) + Ω(ig.FailureMessage).Should(ContainSubstring("told to try again after 10s: bam")) + }) + }) + }) + + Describe("reporting on failures in the presence of either matcher errors or actual errors", func() { + When("there is no actual error or matcher error", func() { + It("simply emits the correct matcher failure message", func() { + ig.G.Eventually(func() (int, error) { + return 5, nil + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return false, nil + }), "My Description") + Ω(ig.FailureMessage).Should(HaveSuffix("My Description\nQM failure message: 5")) + + ig.G.Eventually(func() (int, error) { + return 5, nil + }).WithTimeout(time.Millisecond*10).ShouldNot(QuickMatcher(func(actual any) (bool, error) { + return true, nil + }), "My Description") + Ω(ig.FailureMessage).Should(HaveSuffix("My Description\nQM negated failure message: 5")) + }) + }) + + When("there is no actual error, but there is a matcher error", func() { + It("emits the matcher error", func() { + ig.G.Eventually(func() (int, error) { + return 5, nil + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return false, fmt.Errorf("matcher-error") + }), "My Description") + Ω(ig.FailureMessage).Should(ContainSubstring("My Description\nThe matcher passed to Eventually returned the following error:\nmatcher-error\n <*errors.errorString")) + }) + + When("the matcher error is a StopTrying with attachments", func() { + It("emits the error along with its attachments", func() { + ig.G.Eventually(func() (int, error) { + return 5, nil + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return false, StopTrying("stop-trying").Attach("now, please", 17) + }), "My Description") + Ω(ig.FailureMessage).Should(HavePrefix("Told to stop trying")) + Ω(ig.FailureMessage).Should(HaveSuffix("My Description\nstop-trying\nnow, please:\n : 17")) + }) + }) + }) + + When("there is an actual error", func() { + When("it never manages to get an actual", func() { + It("simply emits the actual error", func() { + ig.G.Eventually(func() (int, error) { + return 0, fmt.Errorf("actual-err") + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return true, nil + }), "My Description") + Ω(ig.FailureMessage).Should(ContainSubstring("My Description\nThe function passed to Eventually returned the following error:\nactual-err\n <*errors.errorString")) + }) + }) + + When("the actual error is because there was a non-nil/non-zero return value", func() { + It("emites a clear message about the non-nil/non-zero return value", func() { + ig.G.Eventually(func() (int, int, error) { + return 0, 1, nil + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return true, nil + }), "My Description") + Ω(ig.FailureMessage).Should(ContainSubstring("My Description\nThe function passed to Eventually had an unexpected non-nil/non-zero return value at index 1:\n : 1")) + }) + }) + + When("the actual error is because there was an assertion failure in the function, and there are return values", func() { + It("emits a clear message about the error having occured", func() { + _, file, line, _ := runtime.Caller(0) + ig.G.Eventually(func(g Gomega) int { + g.Expect(true).To(BeFalse()) + return 1 + }).WithTimeout(time.Millisecond*10).Should(QuickMatcher(func(actual any) (bool, error) { + return true, nil + }), "My Description") + Ω(ig.FailureMessage).Should(HaveSuffix("My Description\nThe function passed to Eventually failed at %s:%d with:\nExpected\n : true\nto be false\n", file, line+2)) + }) + }) + + When("the actual error is because there was an assertion failure in the function, and there are no return values", func() { + It("emits a clear message about the error having occurred", func() { + _, file, line, _ := runtime.Caller(0) + ig.G.Eventually(func(g Gomega) { + g.Expect(true).To(BeFalse()) + }).WithTimeout(time.Millisecond*10).Should(Succeed(), "My Description") + Ω(ig.FailureMessage).Should(HaveSuffix("My Description\nThe function passed to Eventually failed at %s:%d with:\nExpected\n : true\nto be false", file, line+2)) + }) + }) + + When("it did manage to get an actual", func() { + When("that actual generates a matcher error", func() { + It("emits the actual error, and then emits the matcher error", func() { + counter := 0 + ig.G.Eventually(func() (int, error) { + counter += 1 + if counter > 3 { + return counter, fmt.Errorf("actual-err") + } else { + return counter, nil + } + }).WithTimeout(time.Millisecond*100).Should(QuickMatcher(func(actual any) (bool, error) { + if actual.(int) == 3 { + return true, fmt.Errorf("matcher-err") + } + return false, nil + }), "My Description") + Ω(ig.FailureMessage).Should(ContainSubstring("My Description\nThe function passed to Eventually returned the following error:\nactual-err\n <*errors.errorString")) + Ω(ig.FailureMessage).Should(ContainSubstring("At one point, however, the function did return successfully. But Eventually failed because the matcher returned the following error:\nmatcher-err")) + }) + }) + + When("that actual simply didn't match", func() { + It("emits the matcher's failure message", func() { + counter := 0 + ig.G.Eventually(func() (int, error) { + counter += 1 + if counter > 3 { + return counter, fmt.Errorf("actual-err") + } else { + return counter, nil + } + }).WithTimeout(time.Millisecond*100).Should(QuickMatcher(func(actual any) (bool, error) { + actualInt := actual.(int) + return actualInt > 3, nil + }), "My Description") + Ω(ig.FailureMessage).Should(ContainSubstring("My Description\nThe function passed to Eventually returned the following error:\nactual-err\n <*errors.errorString")) + Ω(ig.FailureMessage).Should(ContainSubstring("At one point, however, the function did return successfully. But Eventually failed because the matcher was not satisfied:\nQM failure message: 3")) + + }) + }) }) }) }) diff --git a/matchers/succeed_matcher.go b/matchers/succeed_matcher.go index 721ed5529..da5a39594 100644 --- a/matchers/succeed_matcher.go +++ b/matchers/succeed_matcher.go @@ -1,11 +1,16 @@ package matchers import ( + "errors" "fmt" "github.com/onsi/gomega/format" ) +type formattedGomegaError interface { + FormattedGomegaError() string +} + type SucceedMatcher struct { } @@ -25,6 +30,10 @@ func (matcher *SucceedMatcher) Match(actual interface{}) (success bool, err erro } func (matcher *SucceedMatcher) FailureMessage(actual interface{}) (message string) { + var fgErr formattedGomegaError + if errors.As(actual.(error), &fgErr) { + return fgErr.FormattedGomegaError() + } return fmt.Sprintf("Expected success, but got an error:\n%s\n%s", format.Object(actual, 1), format.IndentString(actual.(error).Error(), 1)) } diff --git a/matchers/succeed_matcher_test.go b/matchers/succeed_matcher_test.go index a15f49ca8..72b7cd5da 100644 --- a/matchers/succeed_matcher_test.go +++ b/matchers/succeed_matcher_test.go @@ -3,6 +3,8 @@ package matchers_test import ( "errors" "regexp" + "runtime" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -23,6 +25,18 @@ func Invalid() *AnyType { return nil } +type formattedGomegaError struct { + message string +} + +func (e formattedGomegaError) Error() string { + return "NOT THIS ERROR" +} + +func (e formattedGomegaError) FormattedGomegaError() string { + return e.message +} + var _ = Describe("Succeed", func() { It("should succeed if the function succeeds", func() { Expect(NotErroring()).Should(Succeed()) @@ -66,6 +80,22 @@ var _ = Describe("Succeed", func() { Expect(actual).To(Equal("Expected success, but got an error:\n <*errors.errorString | 0x00000000>: {s: \"oops\"}\n oops")) }) + It("simply returns .Error() for the failure message if the error is an AsyncPolledActualError", func() { + actual := Succeed().FailureMessage(formattedGomegaError{message: "this is already formatted appropriately"}) + Expect(actual).To(Equal("this is already formatted appropriately")) + }) + + It("operates correctly when paired with an Eventually that receives a Gomega", func() { + _, file, line, _ := runtime.Caller(0) + failureMessage := InterceptGomegaFailure(func() { + Eventually(func(g Gomega) { + g.Expect(true).To(BeFalse()) + }).WithTimeout(time.Millisecond * 10).Should(Succeed()) + }).Error() + Ω(failureMessage).Should(HavePrefix("Timed out after")) + Ω(failureMessage).Should(HaveSuffix("The function passed to Eventually failed at %s:%d with:\nExpected\n : true\nto be false", file, line+3)) + }) + It("builds negated failure message", func() { actual := Succeed().NegatedFailureMessage(123) Expect(actual).To(Equal("Expected failure, but got no error."))