Skip to content

Commit

Permalink
Allow optional description to be lazily evaluated function (#364)
Browse files Browse the repository at this point in the history
* Allow optional description to be lazily evaluated function

Resolves #193
Resolves #351

* Refactor to use case statement for better performance

Co-authored-by: David Ansari <dansari@pivotal.io>
  • Loading branch information
ansd authored and blgm committed Nov 18, 2019
1 parent 0a981cb commit bf64010
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 13 deletions.
17 changes: 11 additions & 6 deletions gomega_dsl.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,16 +293,18 @@ func SetDefaultConsistentlyPollingInterval(t time.Duration) {
// AsyncAssertion is returned by Eventually and Consistently and polls the actual value passed into Eventually against
// the matcher passed to the Should and ShouldNot methods.
//
// Both Should and ShouldNot take a variadic optionalDescription argument. This is passed on to
// fmt.Sprintf() and is used to annotate failure messages. This allows you to make your failure messages more
// descriptive.
// Both Should and ShouldNot take a variadic optionalDescription argument.
// This argument allows you to make your failure messages more descriptive.
// If a single argument of type `func() string` is passed, this function will be lazily evaluated if a failure occurs
// and the returned string is used to annotate the failure message.
// Otherwise, this argument is passed on to fmt.Sprintf() and then used to annotate the failure message.
//
// Both Should and ShouldNot return a boolean that is true if the assertion passed and false if it failed.
//
// Example:
//
// Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.")
// Consistently(myChannel).ShouldNot(Receive(), "Nothing should have come down the pipe.")
// Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." })
type AsyncAssertion interface {
Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
Expand All @@ -317,8 +319,11 @@ type GomegaAsyncAssertion = AsyncAssertion
// Typically Should/ShouldNot are used with Ω and To/ToNot/NotTo are used with Expect
// though this is not enforced.
//
// All methods take a variadic optionalDescription argument. This is passed on to fmt.Sprintf()
// and is used to annotate failure messages.
// All methods take a variadic optionalDescription argument.
// This argument allows you to make your failure messages more descriptive.
// If a single argument of type `func() string` is passed, this function will be lazily evaluated if a failure occurs
// and the returned string is used to annotate the failure message.
// Otherwise, this argument is passed on to fmt.Sprintf() and then used to annotate the failure message.
//
// All methods return a bool that is true if the assertion passed and false if it failed.
//
Expand Down
10 changes: 7 additions & 3 deletions internal/assertion/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,19 @@ func (assertion *Assertion) buildDescription(optionalDescription ...interface{})
switch len(optionalDescription) {
case 0:
return ""
default:
return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
case 1:
if describe, ok := optionalDescription[0].(func() string); ok {
return describe() + "\n"
}
}
return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
}

func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
matches, err := matcher.Match(assertion.actualInput)
description := assertion.buildDescription(optionalDescription...)
assertion.failWrapper.TWithHelper.Helper()
if err != nil {
description := assertion.buildDescription(optionalDescription...)
assertion.failWrapper.Fail(description+err.Error(), 2+assertion.offset)
return false
}
Expand All @@ -72,6 +75,7 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool
} else {
message = matcher.NegatedFailureMessage(assertion.actualInput)
}
description := assertion.buildDescription(optionalDescription...)
assertion.failWrapper.Fail(description+message, 2+assertion.offset)
return false
}
Expand Down
19 changes: 19 additions & 0 deletions internal/assertion/assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ var _ = Describe("Assertion", func() {
Expect(a.ShouldNot(matcher)).Should(BeFalse())
})
})

Context("and the optional description is a function", func() {
It("should not evaluate that function", func() {
evaluated := false
a.Should(matcher, func() string {
evaluated = true
return "A description"
})
Expect(evaluated).Should(BeFalse())
})
})
})

Context("when the matcher fails", func() {
Expand Down Expand Up @@ -148,6 +159,14 @@ var _ = Describe("Assertion", func() {
Expect(failureCallerSkip).Should(Equal(3))
})
})

Context("and the optional description is a function", func() {
It("should append the description to the failure message", func() {
a.Should(matcher, func() string { return "A description" })
Expect(failureMessage).Should(Equal("A description\npositive: The thing I'm testing"))
Expect(failureCallerSkip).Should(Equal(3))
})
})
})

Context("When the matcher returns an error", func() {
Expand Down
10 changes: 6 additions & 4 deletions internal/asyncassertion/async_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interfa
switch len(optionalDescription) {
case 0:
return ""
default:
return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
case 1:
if describe, ok := optionalDescription[0].(func() string); ok {
return describe() + "\n"
}
}
return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
}

func (assertion *AsyncAssertion) actualInputIsAFunction() bool {
Expand Down Expand Up @@ -103,8 +106,6 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
timer := time.Now()
timeout := time.After(assertion.timeoutInterval)

description := assertion.buildDescription(optionalDescription...)

var matches bool
var err error
mayChange := true
Expand All @@ -129,6 +130,7 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
}
}
assertion.failWrapper.TWithHelper.Helper()
description := assertion.buildDescription(optionalDescription...)
assertion.failWrapper.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset)
}

Expand Down
38 changes: 38 additions & 0 deletions internal/asyncassertion/async_assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ var _ = Describe("Async Assertion", func() {
Expect(failureMessage).Should(ContainSubstring("My description 2"))
Expect(callerSkip).Should(Equal(4))
})

Context("when the optional description is a function", func() {
It("should append the description to the failure message", func() {
counter := 0
a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} {
counter++
if counter == 5 {
return "not-a-number" //this should cause the matcher to error
}
return counter
}, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)

a.Should(BeNumerically("==", 5), func() string { return "My description" })

Expect(failureMessage).Should(ContainSubstring("Timed out after"))
Expect(failureMessage).Should(ContainSubstring("My description"))
Expect(callerSkip).Should(Equal(4))
})

Context("and there is no failure", func() {
It("should not evaluate that function", func() {
counter := 0
a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
counter++
return counter
}, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)

evaluated := false
a.Should(BeNumerically("==", 5), func() string {
evaluated = true
return "A description"
})

Expect(failureMessage).Should(BeZero())
Expect(evaluated).Should(BeFalse())
})
})
})
})

Context("the negative case", func() {
Expand Down

0 comments on commit bf64010

Please sign in to comment.