dependencies: update gomega to v1.26.0

If gomega.Eventually/Consistently run into a situation where it observes some
state of e.g. a pod which does not satisfy the condition and then further
polling fails with API server errors, gomega will report both the most recent
pod state and API error instead of just the API error.
This commit is contained in:
Patrick Ohly
2023-01-23 15:19:38 +01:00
parent d29e3bd7aa
commit aa1279b5eb
139 changed files with 4013 additions and 1680 deletions

View File

@@ -3,4 +3,5 @@
.
.idea
gomega.iml
TODO.md
TODO.md
.vscode

View File

@@ -1,3 +1,25 @@
## 1.26.0
### Features
- When a polled function returns an error, keep track of the actual and report on the matcher state of the last non-errored actual [21f3090]
- improve eventually failure message output [c530fb3]
### Fixes
- fix several documentation spelling issues [e2eff1f]
## 1.25.0
### Features
- add `MustPassRepeatedly(int)` to asyncAssertion (#619) [4509f72]
- compare unwrapped errors using DeepEqual (#617) [aaeaa5d]
### Maintenance
- Bump golang.org/x/net from 0.4.0 to 0.5.0 (#614) [c7cfea4]
- Bump github.com/onsi/ginkgo/v2 from 2.6.1 to 2.7.0 (#615) [71b8adb]
- Docs: Fix typo "MUltiple" -> "Multiple" (#616) [9351dda]
- clean up go.sum [cd1dc1d]
## 1.24.2
### Fixes

View File

@@ -41,7 +41,7 @@ func ParseTemplate(templ string) (*template.Template, error) {
/*
MakeMatcher builds a Gomega-compatible matcher from a function (the matchFunc).
matchFunc must return (bool, error) and take a single argument. If you want to perform type-checking yourself pass in a matchFunc of type `func(actual any) (bool, error)`. If you want to only operate on a specific type, pass in `func(actual DesiredType) (bool, error)`; MakeMatcher will take care of checking typues for you and notifying the user if they use the matcher with an invalid type.
matchFunc must return (bool, error) and take a single argument. If you want to perform type-checking yourself pass in a matchFunc of type `func(actual any) (bool, error)`. If you want to only operate on a specific type, pass in `func(actual DesiredType) (bool, error)`; MakeMatcher will take care of checking types for you and notifying the user if they use the matcher with an invalid type.
MakeMatcher(matchFunc) builds a matcher with generic failure messages that look like:
@@ -68,7 +68,7 @@ where templateString is a string that is compiled by WithTemplate into a matcher
template, err = gcustom.ParseTemplate(templateString) //use gcustom's ParseTemplate to get some additional functions mixed in
matcher := MakeMatcher(matchFunc, template)
matcher := MakeMatcher(matchFunc).WithPrcompiled(template)
matcher := MakeMatcher(matchFunc).WithPrecompiled(template)
When a simple message string is provided the positive failure message will look like:
@@ -161,7 +161,7 @@ Templates are provided the following variables and functions:
{{.To}} - is set to "to" if this is a positive failure message and "not to" if this is a negated failure message
{{.Actual}} - the actual passed in to the matcher
{{.FormattedActual}} - a string representing the formatted actual. This can be multiple lines and is always generated with an indentation of 1
{{format <object> <optional-indentaiton}} - a function that allows you to use Gomega's default formatting from within the template. The passed-in <object> is formatted and <optional-indentation> can be set to an integer to control indentation.
{{format <object> <optional-indentation}} - a function that allows you to use Gomega's default formatting from within the template. The passed-in <object> is formatted and <optional-indentation> can be set to an integer to control indentation.
In addition, you can provide custom data to the template by calling WithTemplate(templateString, data) (where data can be anything). This is provided to the template as {{.Data}}.
@@ -169,17 +169,17 @@ Here's a simple example of all these pieces working together:
func HaveWidget(widget Widget) OmegaMatcher {
return MakeMatcher(func(machine Machine) (bool, error) {
return maching.HasWidget(widget), nil
return machine.HasWidget(widget), nil
}).WithTemplate("Expected:\n{{.FormattedActual}}\n{{.To}} have widget named {{.Data.Name}}:\n{{format .Data 1}}", widget)
}
Expect(maching).To(HaveWidget(Widget{Name: "sprocket", Version: 2}))
Expect(machine).To(HaveWidget(Widget{Name: "sprocket", Version: 2}))
Would generate a failure message that looks like:
Expected:
<formatted machine>
to have widget named sprocker:
to have widget named sprocket:
<formatted sprocket>
*/
func (c CustomGomegaMatcher) WithTemplate(templ string, data ...any) CustomGomegaMatcher {
@@ -205,7 +205,6 @@ WithTemplateData() returns a CustomGomegaMatcher configured to provide it's temp
MakeMatcher(matchFunc).WithTemplate(templateString, data)
MakeMatcher(matchFunc).WithTemplate(templateString).WithTemplateData(data)
*/
func (c CustomGomegaMatcher) WithTemplateData(data any) CustomGomegaMatcher {
c.templateData = data

View File

@@ -22,7 +22,7 @@ import (
"github.com/onsi/gomega/types"
)
const GOMEGA_VERSION = "1.24.2"
const GOMEGA_VERSION = "1.26.0"
const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler.
If you're using Ginkgo then you probably forgot to put your assertion in an It().
@@ -360,6 +360,16 @@ You can also pass additional arugments to functions that take a Gomega. The onl
g.Expect(elements).To(ConsistOf(expected))
}).WithContext(ctx).WithArguments("/names", "Joe", "Jane", "Sam").Should(Succeed())
You can ensure that you get a number of consecutive successful tries before succeeding using `MustPassRepeatedly(int)`. For Example:
int count := 0
Eventually(func() bool {
count++
return count > 2
}).MustPassRepeatedly(2).Should(BeTrue())
// Because we had to wait for 2 calls that returned true
Expect(count).To(Equal(3))
Finally, in addition to passing timeouts and a context to Eventually you can be more explicit with Eventually's chaining configuration methods:
Eventually(..., "1s", "2s", ctx).Should(...)

View File

@@ -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()
}
@@ -55,21 +72,23 @@ type AsyncAssertion struct {
actual interface{}
argsToForward []interface{}
timeoutInterval time.Duration
pollingInterval time.Duration
ctx context.Context
offset int
g *Gomega
timeoutInterval time.Duration
pollingInterval time.Duration
mustPassRepeatedly int
ctx context.Context
offset int
g *Gomega
}
func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, ctx context.Context, offset int) *AsyncAssertion {
func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, mustPassRepeatedly int, ctx context.Context, offset int) *AsyncAssertion {
out := &AsyncAssertion{
asyncType: asyncType,
timeoutInterval: timeoutInterval,
pollingInterval: pollingInterval,
offset: offset,
ctx: ctx,
g: g,
asyncType: asyncType,
timeoutInterval: timeoutInterval,
pollingInterval: pollingInterval,
mustPassRepeatedly: mustPassRepeatedly,
offset: offset,
ctx: ctx,
g: g,
}
out.actual = actualInput
@@ -115,6 +134,11 @@ func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) typ
return assertion
}
func (assertion *AsyncAssertion) MustPassRepeatedly(count int) types.AsyncAssertion {
assertion.mustPassRepeatedly = count
return assertion
}
func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
assertion.g.THelper()
vetOptionalDescription("Asynchronous assertion", optionalDescription...)
@@ -141,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()
@@ -164,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)),
}
}
}
@@ -202,6 +230,13 @@ You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
}
func (assertion *AsyncAssertion) invalidMustPassRepeatedlyError(reason string) error {
return fmt.Errorf(`Invalid use of MustPassRepeatedly with %s %s
You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, reason)
}
func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) {
if !assertion.actualIsFunc {
return func() (interface{}, error) { return assertion.actual, nil }, nil
@@ -239,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{})
})))
@@ -257,6 +294,13 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
return nil, assertion.argumentMismatchError(actualType, len(inValues))
}
if assertion.mustPassRepeatedly != 1 && assertion.asyncType != AsyncAssertionTypeEventually {
return nil, assertion.invalidMustPassRepeatedlyError("it can only be used with Eventually")
}
if assertion.mustPassRepeatedly < 1 {
return nil, assertion.invalidMustPassRepeatedlyError("parameter can't be < 1")
}
return func() (actual interface{}, err error) {
var values []reflect.Value
assertionFailure = nil
@@ -338,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 {
@@ -361,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.\nYet, %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)
}
@@ -396,30 +479,39 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
}
}
// Used to count the number of times in a row a step passed
passedRepeatedlyCount := 0
for {
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 {
return true
passedRepeatedlyCount += 1
if passedRepeatedlyCount == assertion.mustPassRepeatedly {
return true
}
}
} else if !isTryAgainAfterError {
if assertion.asyncType == AsyncAssertionTypeConsistently {
fail("Failed")
return false
}
// Reset the consecutive pass count
passedRepeatedlyCount = 0
}
if oracleMatcherSaysStop {
@@ -437,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()
matches, err = m, e
lastValidActual = actual
hasLastValidActual = true
lock.Unlock()
oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, actual)
m, e := assertion.pollMatcher(matcher, actual)
lock.Lock()
matches, matcherErr = m, e
lock.Unlock()
}
case <-contextDone:

View File

@@ -109,7 +109,7 @@ func (g *Gomega) makeAsyncAssertion(asyncAssertionType AsyncAssertionType, offse
}
}
return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, ctx, offset)
return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, 1, ctx, offset)
}
func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) {

View File

@@ -25,7 +25,17 @@ func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err e
expected := matcher.Expected
if isError(expected) {
return reflect.DeepEqual(actualErr, expected) || errors.Is(actualErr, expected.(error)), nil
// first try the built-in errors.Is
if errors.Is(actualErr, expected.(error)) {
return true, nil
}
// if not, try DeepEqual along the error chain
for unwrapped := actualErr; unwrapped != nil; unwrapped = errors.Unwrap(unwrapped) {
if reflect.DeepEqual(unwrapped, expected) {
return true, nil
}
}
return false, nil
}
if isString(expected) {

View File

@@ -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))
}

View File

@@ -75,6 +75,7 @@ type AsyncAssertion interface {
ProbeEvery(interval time.Duration) AsyncAssertion
WithContext(ctx context.Context) AsyncAssertion
WithArguments(argsToForward ...interface{}) AsyncAssertion
MustPassRepeatedly(count int) AsyncAssertion
}
// Assertions are returned by Ω and Expect and enable assertions against Gomega matchers