Skip to content

Commit

Permalink
add Error() assertions on the final error value of multi-return values (
Browse files Browse the repository at this point in the history
  • Loading branch information
thediveo authored Nov 5, 2021
1 parent 18a4723 commit 2f96943
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 26 deletions.
76 changes: 56 additions & 20 deletions internal/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ import (
)

type Assertion struct {
actualInput interface{}
actuals []interface{} // actual value plus all extra values
actualIndex int // value to pass to the matcher
vet vetinari // the vet to call before calling Gomega matcher
offset int
extra []interface{}
g *Gomega
}

// ...obligatory discworld reference, as "vetineer" doesn't sound ... quite right.
type vetinari func(assertion *Assertion, optionalDescription ...interface{}) bool

func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion {
return &Assertion{
actualInput: actualInput,
actuals: append([]interface{}{actualInput}, extra...),
actualIndex: 0,
vet: (*Assertion).vetActuals,
offset: offset,
extra: extra,
g: g,
}
}
Expand All @@ -28,29 +33,39 @@ func (assertion *Assertion) WithOffset(offset int) types.Assertion {
return assertion
}

func (assertion *Assertion) Error() types.Assertion {
return &Assertion{
actuals: assertion.actuals,
actualIndex: len(assertion.actuals) - 1,
vet: (*Assertion).vetError,
offset: assertion.offset,
g: assertion.g,
}
}

func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
}

func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
}

func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
}

func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string {
Expand All @@ -66,7 +81,8 @@ func (assertion *Assertion) buildDescription(optionalDescription ...interface{})
}

func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
matches, err := matcher.Match(assertion.actualInput)
actualInput := assertion.actuals[assertion.actualIndex]
matches, err := matcher.Match(actualInput)
assertion.g.THelper()
if err != nil {
description := assertion.buildDescription(optionalDescription...)
Expand All @@ -76,9 +92,9 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool
if matches != desiredMatch {
var message string
if desiredMatch {
message = matcher.FailureMessage(assertion.actualInput)
message = matcher.FailureMessage(actualInput)
} else {
message = matcher.NegatedFailureMessage(assertion.actualInput)
message = matcher.NegatedFailureMessage(actualInput)
}
description := assertion.buildDescription(optionalDescription...)
assertion.g.Fail(description+message, 2+assertion.offset)
Expand All @@ -88,8 +104,11 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool
return true
}

func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool {
success, message := vetExtras(assertion.extra)
// vetActuals vets the actual values, with the (optional) exception of a
// specific value, such as the first value in case non-error assertions, or the
// last value in case of Error()-based assertions.
func (assertion *Assertion) vetActuals(optionalDescription ...interface{}) bool {
success, message := vetActuals(assertion.actuals, assertion.actualIndex)
if success {
return true
}
Expand All @@ -100,12 +119,29 @@ func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool {
return false
}

func vetExtras(extras []interface{}) (bool, string) {
for i, extra := range extras {
if extra != nil {
zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface()
if !reflect.DeepEqual(zeroValue, extra) {
message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra)
// vetError vets the actual values, except for the final error value, in case
// the final error value is non-zero. Otherwise, it doesn't vet the actual
// values, as these are allowed to take on any values unless there is a non-zero
// error value.
func (assertion *Assertion) vetError(optionalDescription ...interface{}) bool {
if err := assertion.actuals[assertion.actualIndex]; err != nil {
// Go error result idiom: all other actual values must be zero values.
return assertion.vetActuals(optionalDescription...)
}
return true
}

// vetActuals vets a slice of actual values, optionally skipping a particular
// value slice element, such as the first or last value slice element.
func vetActuals(actuals []interface{}, skipIndex int) (bool, string) {
for i, actual := range actuals {
if i == skipIndex {
continue
}
if actual != nil {
zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface()
if !reflect.DeepEqual(zeroValue, actual) {
message := fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual)
return false, message
}
}
Expand Down
48 changes: 47 additions & 1 deletion internal/assertion_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package internal_test

import (
"errors"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -141,7 +143,51 @@ var _ = Describe("Making Synchronous Assertions", func() {
Entry(
"when the matcher matches but a non-zero-valued extra parameter is included, it fails",
MATCH, Extras(1, "bam", struct{ Foo string }{Foo: "foo"}, nil), OptionalDescription(),
SHOULD_MATCH, "Unexpected non-nil/non-zero extra argument at index 1:\n\t<int>: 1", IT_FAILS,
SHOULD_MATCH, "Unexpected non-nil/non-zero argument at index 1:\n\t<int>: 1", IT_FAILS,
),
)

var SHOULD_OCCUR = true
var SHOULD_NOT_OCCUR = false

DescribeTable("error expectations",
func(a, b int, e error, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) {
abe := func(a, b int, e error) (int, int, error) {
return a, b, e
}
ig := NewInstrumentedGomega()
var returnValue bool
if isPositiveAssertion {
returnValue = ig.G.Expect(abe(a, b, e)).Error().To(HaveOccurred())
} else {
returnValue = ig.G.Expect(abe(a, b, e)).Error().NotTo(HaveOccurred())
}
Expect(returnValue).To(Equal(expectedReturnValue))
Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
if expectedFailureMessage != "" {
Expect(ig.FailureSkip).To(Equal([]int{2}))
}
},
Entry(
"when non-zero results without error",
1, 2, nil,
SHOULD_NOT_OCCUR, "", IT_PASSES,
),
Entry(
"when non-zero results with error",
1, 2, errors.New("D'oh!"),
SHOULD_NOT_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
),
Entry(
"when non-zero results without error",
0, 0, errors.New("D'oh!"),
SHOULD_OCCUR, "", IT_PASSES,
),
Entry(
"when non-zero results with error",
1, 2, errors.New("D'oh!"),
SHOULD_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
),
)

})
4 changes: 2 additions & 2 deletions internal/async_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ func (assertion *AsyncAssertion) pollActual() (interface{}, error) {
if err != nil {
return nil, err
}
extras := []interface{}{}
extras := []interface{}{nil}
for _, value := range values[1:] {
extras = append(extras, value.Interface())
}
success, message := vetExtras(extras)
success, message := vetActuals(extras, 0)
if !success {
return nil, errors.New(message)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/async_assertion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ 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 extra argument at index 2:"))
Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 2:"))
Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"hi"}`))
})

Expand Down Expand Up @@ -377,7 +377,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: Unexpected non-nil/non-zero extra argument at index 2:"))
Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 2:"))
Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"welp"}`))
Ω(counter).Should(Equal(3))
})
Expand All @@ -404,7 +404,7 @@ 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 extra argument at index 1:"))
Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 1:"))
Ω(ig.FailureMessage).Should(ContainSubstring(`<string>: "welp"`))
Ω(counter).Should(Equal(3))
})
Expand Down
2 changes: 2 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,6 @@ type Assertion interface {
NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool

WithOffset(offset int) Assertion

Error() Assertion
}

0 comments on commit 2f96943

Please sign in to comment.