Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Error() assertions on the final error value of multi-return value functions #480

Merged
merged 1 commit into from
Nov 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}