rebase: bump github.com/onsi/gomega from 1.22.1 to 1.23.0

Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.22.1 to 1.23.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.22.1...v1.23.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2022-11-01 15:23:27 +00:00
committed by mergify[bot]
parent 7b663279bf
commit 7ca2468d80
22 changed files with 570 additions and 410 deletions

View File

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

View File

@ -1,3 +1,23 @@
## 1.23.0
### Features
- Custom formatting on a per-type basis can be provided using `format.RegisterCustomFormatter()` -- see the docs [here](https://onsi.github.io/gomega/#adjusting-output)
- Substantial improvement have been made to `StopTrying()`:
- Users can now use `StopTrying().Wrap(err)` to wrap errors and `StopTrying().Attach(description, object)` to attach arbitrary objects to the `StopTrying()` error
- `StopTrying()` is now always interpreted as a failure. If you are an early adopter of `StopTrying()` you may need to change your code as the prior version would match against the returned value even if `StopTrying()` was returned. Going forward the `StopTrying()` api should remain stable.
- `StopTrying()` and `StopTrying().Now()` can both be used in matchers - not just polled functions.
- `TryAgainAfter(duration)` is used like `StopTrying()` but instructs `Eventually` and `Consistently` that the poll should be tried again after the specified duration. This allows you to dynamically adjust the polling duration.
- `ctx` can now be passed-in as the first argument to `Eventually` and `Consistently`.
## Maintenance
- Bump github.com/onsi/ginkgo/v2 from 2.3.0 to 2.3.1 (#597) [afed901]
- Bump nokogiri from 1.13.8 to 1.13.9 in /docs (#599) [7c691b3]
- Bump github.com/google/go-cmp from 0.5.8 to 0.5.9 (#587) [ff22665]
## 1.22.1
## Fixes

View File

@ -65,6 +65,52 @@ type GomegaStringer interface {
GomegaString() string
}
/*
CustomFormatters can be registered with Gomega via RegisterCustomFormatter()
Any value to be rendered by Gomega is passed to each registered CustomFormatters.
The CustomFormatter signals that it will handle formatting the value by returning (formatted-string, true)
If the CustomFormatter does not want to handle the object it should return ("", false)
Strings returned by CustomFormatters are not truncated
*/
type CustomFormatter func(value interface{}) (string, bool)
type CustomFormatterKey uint
var customFormatterKey CustomFormatterKey = 1
type customFormatterKeyPair struct {
CustomFormatter
CustomFormatterKey
}
/*
RegisterCustomFormatter registers a CustomFormatter and returns a CustomFormatterKey
You can call UnregisterCustomFormatter with the returned key to unregister the associated CustomFormatter
*/
func RegisterCustomFormatter(customFormatter CustomFormatter) CustomFormatterKey {
key := customFormatterKey
customFormatterKey += 1
customFormatters = append(customFormatters, customFormatterKeyPair{customFormatter, key})
return key
}
/*
UnregisterCustomFormatter unregisters a previously registered CustomFormatter. You should pass in the key returned by RegisterCustomFormatter
*/
func UnregisterCustomFormatter(key CustomFormatterKey) {
formatters := []customFormatterKeyPair{}
for _, f := range customFormatters {
if f.CustomFormatterKey == key {
continue
}
formatters = append(formatters, f)
}
customFormatters = formatters
}
var customFormatters = []customFormatterKeyPair{}
/*
Generates a formatted matcher success/failure message of the form:
@ -219,17 +265,24 @@ func Object(object interface{}, indentation uint) string {
IndentString takes a string and indents each line by the specified amount.
*/
func IndentString(s string, indentation uint) string {
return indentString(s, indentation, true)
}
func indentString(s string, indentation uint, indentFirstLine bool) string {
result := &strings.Builder{}
components := strings.Split(s, "\n")
result := ""
indent := strings.Repeat(Indent, int(indentation))
for i, component := range components {
result += indent + component
if i > 0 || indentFirstLine {
result.WriteString(indent)
}
result.WriteString(component)
if i < len(components)-1 {
result += "\n"
result.WriteString("\n")
}
}
return result
return result.String()
}
func formatType(v reflect.Value) string {
@ -261,18 +314,27 @@ func formatValue(value reflect.Value, indentation uint) string {
if value.CanInterface() {
obj := value.Interface()
// if a CustomFormatter handles this values, we'll go with that
for _, customFormatter := range customFormatters {
formatted, handled := customFormatter.CustomFormatter(obj)
// do not truncate a user-provided CustomFormatter()
if handled {
return indentString(formatted, indentation+1, false)
}
}
// GomegaStringer will take precedence to other representations and disregards UseStringerRepresentation
if x, ok := obj.(GomegaStringer); ok {
// do not truncate a user-defined GoMegaString() value
return x.GomegaString()
// do not truncate a user-defined GomegaString() value
return indentString(x.GomegaString(), indentation+1, false)
}
if UseStringerRepresentation {
switch x := obj.(type) {
case fmt.GoStringer:
return truncateLongStrings(x.GoString())
return indentString(truncateLongStrings(x.GoString()), indentation+1, false)
case fmt.Stringer:
return truncateLongStrings(x.String())
return indentString(truncateLongStrings(x.String()), indentation+1, false)
}
}
}

View File

@ -22,7 +22,7 @@ import (
"github.com/onsi/gomega/types"
)
const GOMEGA_VERSION = "1.22.1"
const GOMEGA_VERSION = "1.23.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().
@ -86,12 +86,12 @@ func internalGomega(g Gomega) *internal.Gomega {
// NewWithT takes a *testing.T and returns a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with
// Gomega's rich ecosystem of matchers in standard `testing` test suits.
//
// func TestFarmHasCow(t *testing.T) {
// g := gomega.NewWithT(t)
// func TestFarmHasCow(t *testing.T) {
// g := gomega.NewWithT(t)
//
// f := farm.New([]string{"Cow", "Horse"})
// g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
// }
// f := farm.New([]string{"Cow", "Horse"})
// g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
// }
func NewWithT(t types.GomegaTestingT) *WithT {
return internal.NewGomega(internalGomega(Default).DurationBundle).ConfigureWithT(t)
}
@ -171,7 +171,8 @@ func ensureDefaultGomegaIsConfigured() {
}
// Ω wraps an actual value allowing assertions to be made on it:
// Ω("foo").Should(Equal("foo"))
//
// Ω("foo").Should(Equal("foo"))
//
// If Ω is passed more than one argument it will pass the *first* argument to the matcher.
// All subsequent arguments will be required to be nil/zero.
@ -180,10 +181,13 @@ func ensureDefaultGomegaIsConfigured() {
// a value and an error - a common patter in Go.
//
// For example, given a function with signature:
// func MyAmazingThing() (int, error)
//
// func MyAmazingThing() (int, error)
//
// Then:
// Ω(MyAmazingThing()).Should(Equal(3))
//
// Ω(MyAmazingThing()).Should(Equal(3))
//
// Will succeed only if `MyAmazingThing()` returns `(3, nil)`
//
// Ω and Expect are identical
@ -193,7 +197,8 @@ func Ω(actual interface{}, extra ...interface{}) Assertion {
}
// Expect wraps an actual value allowing assertions to be made on it:
// Expect("foo").To(Equal("foo"))
//
// Expect("foo").To(Equal("foo"))
//
// If Expect is passed more than one argument it will pass the *first* argument to the matcher.
// All subsequent arguments will be required to be nil/zero.
@ -202,10 +207,13 @@ func Ω(actual interface{}, extra ...interface{}) Assertion {
// a value and an error - a common patter in Go.
//
// For example, given a function with signature:
// func MyAmazingThing() (int, error)
//
// func MyAmazingThing() (int, error)
//
// Then:
// Expect(MyAmazingThing()).Should(Equal(3))
//
// Expect(MyAmazingThing()).Should(Equal(3))
//
// Will succeed only if `MyAmazingThing()` returns `(3, nil)`
//
// Expect and Ω are identical
@ -215,7 +223,8 @@ func Expect(actual interface{}, extra ...interface{}) Assertion {
}
// ExpectWithOffset wraps an actual value allowing assertions to be made on it:
// ExpectWithOffset(1, "foo").To(Equal("foo"))
//
// ExpectWithOffset(1, "foo").To(Equal("foo"))
//
// Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument
// that is used to modify the call-stack offset when computing line numbers. It is
@ -241,15 +250,15 @@ Eventually works with any Gomega compatible matcher and supports making assertio
There are several examples of values that can change over time. These can be passed in to Eventually and will be passed to the matcher repeatedly until a match occurs. For example:
c := make(chan bool)
go DoStuff(c)
Eventually(c, "50ms").Should(BeClosed())
c := make(chan bool)
go DoStuff(c)
Eventually(c, "50ms").Should(BeClosed())
will poll the channel repeatedly until it is closed. In this example `Eventually` will block until either the specified timeout of 50ms has elapsed or the channel is closed, whichever comes first.
Several Gomega libraries allow you to use Eventually in this way. For example, the gomega/gexec package allows you to block until a *gexec.Session exits successfully via:
Eventually(session).Should(gexec.Exit(0))
Eventually(session).Should(gexec.Exit(0))
And the gomega/gbytes package allows you to monitor a streaming *gbytes.Buffer until a given string is seen:
@ -270,34 +279,38 @@ Eventually can be passed functions that **return at least one value**. When con
For example:
Eventually(func() int {
return client.FetchCount()
}).Should(BeNumerically(">=", 17))
Eventually(func() int {
return client.FetchCount()
}).Should(BeNumerically(">=", 17))
will repeatedly poll client.FetchCount until the BeNumerically matcher is satisfied. (Note that this example could have been written as Eventually(client.FetchCount).Should(BeNumerically(">=", 17)))
will repeatedly poll client.FetchCount until the BeNumerically matcher is satisfied. (Note that this example could have been written as Eventually(client.FetchCount).Should(BeNumerically(">=", 17)))
If multiple values are returned by the function, Eventually will pass the first value to the matcher and require that all others are zero-valued. This allows you to pass Eventually a function that returns a value and an error - a common pattern in Go.
For example, consider a method that returns a value and an error:
func FetchFromDB() (string, error)
func FetchFromDB() (string, error)
Then
Eventually(FetchFromDB).Should(Equal("got it"))
Eventually(FetchFromDB).Should(Equal("got it"))
will pass only if and when the returned error is nil *and* the returned string satisfies the matcher.
Eventually can also accept functions that take arguments, however you must provide those arguments using .WithArguments(). For example, consider a function that takes a user-id and makes a network request to fetch a full name:
func FetchFullName(userId int) (string, error)
You can poll this function like so:
Eventually(FetchFullName).WithArguments(1138).Should(Equal("Wookie"))
It is important to note that the function passed into Eventually is invoked *synchronously* when polled. Eventually does not (in fact, it cannot) kill the function if it takes longer to return than Eventually's configured timeout. A common practice here is to use a context. Here's an example that combines Ginkgo's spec timeout support with Eventually:
It("fetches the correct count", func(ctx SpecContext) {
Eventually(func() int {
Eventually(ctx, func() int {
return client.FetchCount(ctx, "/users")
}, ctx).Should(BeNumerically(">=", 17))
}).Should(BeNumerically(">=", 17))
}, SpecTimeout(time.Second))
you an also use Eventually().WithContext(ctx) to pass in the context. Passed-in contexts play nicely with paseed-in arguments as long as the context appears first. You can rewrite the above example as:
@ -326,13 +339,13 @@ will pass only if all the assertions in the polled function pass and the return
Eventually also supports a special case polling function that takes a single Gomega argument and returns no values. Eventually assumes such a function is making assertions and is designed to work with the Succeed matcher to validate that all assertions have passed.
For example:
Eventually(func(g Gomega) {
model, err := client.Find(1138)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(model.Reticulate()).To(Succeed())
g.Expect(model.IsReticulated()).To(BeTrue())
g.Expect(model.Save()).To(Succeed())
}).Should(Succeed())
Eventually(func(g Gomega) {
model, err := client.Find(1138)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(model.Reticulate()).To(Succeed())
g.Expect(model.IsReticulated()).To(BeTrue())
g.Expect(model.Save()).To(Succeed())
}).Should(Succeed())
will rerun the function until all assertions pass.
@ -353,11 +366,11 @@ Finally, in addition to passing timeouts and a context to Eventually you can be
is equivalent to
Eventually(...).WithTimeout(time.Second).WithPolling(2*time.Second).WithContext(ctx).Should(...)
Eventually(...).WithTimeout(time.Second).WithPolling(2*time.Second).WithContext(ctx).Should(...)
*/
func Eventually(actual interface{}, args ...interface{}) AsyncAssertion {
func Eventually(args ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured()
return Default.Eventually(actual, args...)
return Default.Eventually(args...)
}
// EventuallyWithOffset operates like Eventually but takes an additional
@ -369,9 +382,9 @@ func Eventually(actual interface{}, args ...interface{}) AsyncAssertion {
// `EventuallyWithOffset` specifying a timeout interval (and an optional polling interval) are
// the same as `Eventually(...).WithOffset(...).WithTimeout` or
// `Eventually(...).WithOffset(...).WithTimeout(...).WithPolling`.
func EventuallyWithOffset(offset int, actual interface{}, args ...interface{}) AsyncAssertion {
func EventuallyWithOffset(offset int, args ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured()
return Default.EventuallyWithOffset(offset, actual, args...)
return Default.EventuallyWithOffset(offset, args...)
}
/*
@ -385,13 +398,13 @@ Consistently accepts the same three categories of actual as Eventually, check th
Consistently is useful in cases where you want to assert that something *does not happen* for a period of time. For example, you may want to assert that a goroutine does *not* send data down a channel. In this case you could write:
Consistently(channel, "200ms").ShouldNot(Receive())
Consistently(channel, "200ms").ShouldNot(Receive())
This will block for 200 milliseconds and repeatedly check the channel and ensure nothing has been received.
*/
func Consistently(actual interface{}, args ...interface{}) AsyncAssertion {
func Consistently(args ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured()
return Default.Consistently(actual, args...)
return Default.Consistently(args...)
}
// ConsistentlyWithOffset operates like Consistently but takes an additional
@ -400,44 +413,54 @@ func Consistently(actual interface{}, args ...interface{}) AsyncAssertion {
//
// `ConsistentlyWithOffset` is the same as `Consistently(...).WithOffset` and
// optional `WithTimeout` and `WithPolling`.
func ConsistentlyWithOffset(offset int, actual interface{}, args ...interface{}) AsyncAssertion {
func ConsistentlyWithOffset(offset int, args ...interface{}) AsyncAssertion {
ensureDefaultGomegaIsConfigured()
return Default.ConsistentlyWithOffset(offset, actual, args...)
return Default.ConsistentlyWithOffset(offset, args...)
}
/*
StopTrying can be used to signal to Eventually and Consistently that the polled function will not change
and that they should stop trying. In the case of Eventually, if a match does not occur in this, final, iteration then a failure will result. In the case of Consistently, as long as this last iteration satisfies the match, the assertion will be considered successful.
StopTrying can be used to signal to Eventually and Consistentlythat they should abort and stop trying. This always results in a failure of the assertion - and the failure message is the content of the StopTrying signal.
You can send the StopTrying signal by either returning a StopTrying("message") messages as an error from your passed-in function _or_ by calling StopTrying("message").Now() to trigger a panic and end execution.
You can send the StopTrying signal by either returning StopTrying("message") as an error from your passed-in function _or_ by calling StopTrying("message").Now() to trigger a panic and end execution.
You can also wrap StopTrying around an error with `StopTrying("message").Wrap(err)` and can attach additional objects via `StopTrying("message").Attach("description", object). When rendered, the signal will include the wrapped error and any attached objects rendered using Gomega's default formatting.
Here are a couple of examples. This is how you might use StopTrying() as an error to signal that Eventually should stop:
playerIndex, numPlayers := 0, 11
Eventually(func() (string, error) {
name := client.FetchPlayer(playerIndex)
playerIndex += 1
if playerIndex == numPlayers {
return name, StopTrying("No more players left")
} else {
return name, nil
}
if playerIndex == numPlayers {
return "", StopTrying("no more players left")
}
name := client.FetchPlayer(playerIndex)
playerIndex += 1
return name, nil
}).Should(Equal("Patrick Mahomes"))
note that the final `name` returned alongside `StopTrying()` will be processed.
And here's an example where `StopTrying().Now()` is called to halt execution immediately:
Eventually(func() []string {
names, err := client.FetchAllPlayers()
if err == client.IRRECOVERABLE_ERROR {
StopTrying("Irrecoverable error occurred").Now()
StopTrying("Irrecoverable error occurred").Wrap(err).Now()
}
return names
}).Should(ContainElement("Patrick Mahomes"))
*/
var StopTrying = internal.StopTrying
/*
TryAgainAfter(<duration>) allows you to adjust the polling interval for the _next_ iteration of `Eventually` or `Consistently`. Like `StopTrying` you can either return `TryAgainAfter` as an error or trigger it immedieately with `.Now()`
When `TryAgainAfter(<duration>` is triggered `Eventually` and `Consistently` will wait for that duration. If a timeout occurs before the next poll is triggered both `Eventually` and `Consistently` will always fail with the content of the TryAgainAfter message. As with StopTrying you can `.Wrap()` and error and `.Attach()` additional objects to `TryAgainAfter`.
*/
var TryAgainAfter = internal.TryAgainAfter
/*
PollingSignalError is the error returned by StopTrying() and TryAgainAfter()
*/
type PollingSignalError = internal.PollingSignalError
// SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses.
func SetDefaultEventuallyTimeout(t time.Duration) {
Default.SetDefaultEventuallyTimeout(t)
@ -471,8 +494,8 @@ func SetDefaultConsistentlyPollingInterval(t time.Duration) {
//
// Example:
//
// Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.")
// Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." })
// Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.")
// Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." })
type AsyncAssertion = types.AsyncAssertion
// GomegaAsyncAssertion is deprecated in favor of AsyncAssertion, which does not stutter.
@ -494,7 +517,7 @@ type GomegaAsyncAssertion = types.AsyncAssertion
//
// Example:
//
// Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm)
// Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm)
type Assertion = types.Assertion
// GomegaAssertion is deprecated in favor of Assertion, which does not stutter.

View File

@ -4,6 +4,7 @@ import (
"fmt"
"reflect"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)
@ -146,7 +147,12 @@ func vetActuals(actuals []interface{}, skipIndex int) (bool, string) {
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)
var message string
if err, ok := actual.(error); ok {
message = fmt.Sprintf("Unexpected error: %s\n%s", err, format.Object(err, 1))
} else {
message = fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual)
}
return false, message
}
}

View File

@ -2,58 +2,22 @@ package internal
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
"sync"
"time"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)
type StopTryingError interface {
error
Now()
wasViaPanic() bool
}
var errInterface = reflect.TypeOf((*error)(nil)).Elem()
var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem()
var contextType = reflect.TypeOf(new(context.Context)).Elem()
func asStopTryingError(actual interface{}) (StopTryingError, bool) {
if actual == nil {
return nil, false
}
if actualErr, ok := actual.(error); ok {
var target *stopTryingError
if errors.As(actualErr, &target) {
return target, true
} else {
return nil, false
}
}
return nil, false
}
type stopTryingError struct {
message string
viaPanic bool
}
func (s *stopTryingError) Error() string {
return s.message
}
func (s *stopTryingError) Now() {
s.viaPanic = true
panic(s)
}
func (s *stopTryingError) wasViaPanic() bool {
return s.viaPanic
}
var StopTrying = func(message string) StopTryingError {
return &stopTryingError{message: message}
type contextWithAttachProgressReporter interface {
AttachProgressReporter(func() string) func()
}
type AsyncAssertionType uint
@ -164,39 +128,40 @@ func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interfa
return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
}
func (assertion *AsyncAssertion) processReturnValues(values []reflect.Value) (interface{}, error, StopTryingError) {
var err error
var stopTrying StopTryingError
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"), stopTrying
return nil, fmt.Errorf("No values were returned by the function passed to Gomega")
}
actual := values[0].Interface()
if stopTryingErr, ok := asStopTryingError(actual); ok {
stopTrying = stopTryingErr
if _, ok := AsPollingSignalError(actual); ok {
return actual, actual.(error)
}
var err error
for i, extraValue := range values[1:] {
extra := extraValue.Interface()
if extra == nil {
continue
}
if stopTryingErr, ok := asStopTryingError(extra); ok {
stopTrying = stopTryingErr
continue
if _, ok := AsPollingSignalError(extra); ok {
return actual, extra.(error)
}
zero := reflect.Zero(reflect.TypeOf(extra)).Interface()
extraType := reflect.TypeOf(extra)
zero := reflect.Zero(extraType).Interface()
if reflect.DeepEqual(extra, zero) {
continue
}
if i == len(values)-2 && extraType.Implements(errInterface) {
err = fmt.Errorf("function returned error: %w", extra.(error))
}
if err == nil {
err = fmt.Errorf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i+1, extra, extra)
err = fmt.Errorf("Unexpected non-nil/non-zero return value at index %d:\n\t<%T>: %#v", i+1, extra, extra)
}
}
return actual, err, stopTrying
}
var gomegaType = reflect.TypeOf((*types.Gomega)(nil)).Elem()
var contextType = reflect.TypeOf(new(context.Context)).Elem()
return actual, err
}
func (assertion *AsyncAssertion) invalidFunctionError(t reflect.Type) error {
return fmt.Errorf(`The function passed to %s had an invalid signature of %s. Functions passed to %s must either:
@ -226,9 +191,9 @@ You can learn more at https://onsi.github.io/gomega/#eventually
`, assertion.asyncType, t, t.NumIn(), numProvided, have, assertion.asyncType)
}
func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error, StopTryingError), error) {
func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error), error) {
if !assertion.actualIsFunc {
return func() (interface{}, error, StopTryingError) { return assertion.actual, nil, nil }, nil
return func() (interface{}, error) { return assertion.actual, nil }, nil
}
actualValue := reflect.ValueOf(assertion.actual)
actualType := reflect.TypeOf(assertion.actual)
@ -236,23 +201,11 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
if numIn == 0 && numOut == 0 {
return nil, assertion.invalidFunctionError(actualType)
} else if numIn == 0 {
return func() (actual interface{}, err error, stopTrying StopTryingError) {
defer func() {
if e := recover(); e != nil {
if stopTryingErr, ok := asStopTryingError(e); ok {
stopTrying = stopTryingErr
} else {
panic(e)
}
}
}()
actual, err, stopTrying = assertion.processReturnValues(actualValue.Call([]reflect.Value{}))
return
}, nil
}
takesGomega, takesContext := actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType)
takesGomega, takesContext := false, false
if numIn > 0 {
takesGomega, takesContext = actualType.In(0).Implements(gomegaType), actualType.In(0).Implements(contextType)
}
if takesGomega && numIn > 1 && actualType.In(1).Implements(contextType) {
takesContext = true
}
@ -292,21 +245,22 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
return nil, assertion.argumentMismatchError(actualType, len(inValues))
}
return func() (actual interface{}, err error, stopTrying StopTryingError) {
return func() (actual interface{}, err error) {
var values []reflect.Value
assertionFailure = nil
defer func() {
if numOut == 0 {
if numOut == 0 && takesGomega {
actual = assertionFailure
} else {
actual, err, stopTrying = assertion.processReturnValues(values)
if assertionFailure != nil {
actual, err = assertion.processReturnValues(values)
_, isAsyncError := AsPollingSignalError(err)
if assertionFailure != nil && !isAsyncError {
err = assertionFailure
}
}
if e := recover(); e != nil {
if stopTryingErr, ok := asStopTryingError(e); ok {
stopTrying = stopTryingErr
if _, isAsyncError := AsPollingSignalError(e); isAsyncError {
err = e.(error)
} else if assertionFailure == nil {
panic(e)
}
@ -317,13 +271,6 @@ func (assertion *AsyncAssertion) buildActualPoller() (func() (interface{}, error
}, nil
}
func (assertion *AsyncAssertion) matcherSaysStopTrying(matcher types.GomegaMatcher, value interface{}) StopTryingError {
if assertion.actualIsFunc || types.MatchMayChangeInTheFuture(matcher, value) {
return nil
}
return StopTrying("No future change is possible. Bailing out early")
}
func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time {
if assertion.timeoutInterval >= 0 {
return time.After(assertion.timeoutInterval)
@ -351,8 +298,27 @@ func (assertion *AsyncAssertion) afterPolling() <-chan time.Time {
}
}
type contextWithAttachProgressReporter interface {
AttachProgressReporter(func() string) func()
func (assertion *AsyncAssertion) matcherSaysStopTrying(matcher types.GomegaMatcher, value interface{}) bool {
if assertion.actualIsFunc || types.MatchMayChangeInTheFuture(matcher, value) {
return false
}
return true
}
func (assertion *AsyncAssertion) pollMatcher(matcher types.GomegaMatcher, value interface{}) (matches bool, err error) {
defer func() {
if e := recover(); e != nil {
if _, isAsyncError := AsPollingSignalError(e); isAsyncError {
err = e.(error)
} else {
panic(e)
}
}
}()
matches, err = matcher.Match(value)
return
}
func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
@ -362,6 +328,7 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
var matches bool
var err error
var oracleMatcherSaysStop bool
assertion.g.THelper()
@ -371,22 +338,27 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
return false
}
value, err, stopTrying := pollActual()
value, err := pollActual()
if err == nil {
if stopTrying == nil {
stopTrying = assertion.matcherSaysStopTrying(matcher, value)
}
matches, err = matcher.Match(value)
oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, value)
matches, err = assertion.pollMatcher(matcher, value)
}
messageGenerator := func() string {
// can be called out of band by Ginkgo if the user requests a progress report
lock.Lock()
defer lock.Unlock()
errMsg := ""
message := ""
if err != nil {
errMsg = "Error: " + err.Error()
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)
}
} else {
message = "Error: " + err.Error() + "\n" + format.Object(err, 1)
}
} else {
if desiredMatch {
message = matcher.FailureMessage(value)
@ -395,7 +367,7 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
}
}
description := assertion.buildDescription(optionalDescription...)
return fmt.Sprintf("%s%s%s", description, message, errMsg)
return fmt.Sprintf("%s%s", description, message)
}
fail := func(preamble string) {
@ -412,84 +384,72 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch
}
}
if assertion.asyncType == AsyncAssertionTypeEventually {
for {
if err == nil && matches == desiredMatch {
return true
}
for {
var nextPoll <-chan time.Time = nil
var isTryAgainAfterError = false
if stopTrying != nil {
fail(stopTrying.Error() + " -")
if pollingSignalErr, ok := AsPollingSignalError(err); ok {
if pollingSignalErr.IsStopTrying() {
fail("Told to stop trying")
return false
}
select {
case <-assertion.afterPolling():
v, e, st := pollActual()
if st != nil && st.wasViaPanic() {
// we were told to stop trying via panic - which means we dont' have reasonable new values
// we should simply use the old values and exit now
fail(st.Error() + " -")
return false
}
lock.Lock()
value, err, stopTrying = v, e, st
lock.Unlock()
if err == nil {
if stopTrying == nil {
stopTrying = assertion.matcherSaysStopTrying(matcher, value)
}
matches, e = matcher.Match(value)
lock.Lock()
err = e
lock.Unlock()
}
case <-contextDone:
fail("Context was cancelled")
return false
case <-timeout:
fail("Timed out")
return false
if pollingSignalErr.IsTryAgainAfter() {
nextPoll = time.After(pollingSignalErr.TryAgainDuration())
isTryAgainAfterError = true
}
}
} else if assertion.asyncType == AsyncAssertionTypeConsistently {
for {
if !(err == nil && matches == desiredMatch) {
if err == nil && matches == desiredMatch {
if assertion.asyncType == AsyncAssertionTypeEventually {
return true
}
} else if !isTryAgainAfterError {
if assertion.asyncType == AsyncAssertionTypeConsistently {
fail("Failed")
return false
}
}
if stopTrying != nil {
if oracleMatcherSaysStop {
if assertion.asyncType == AsyncAssertionTypeEventually {
fail("No future change is possible. Bailing out early")
return false
} else {
return true
}
}
select {
case <-assertion.afterPolling():
v, e, st := pollActual()
if st != nil && st.wasViaPanic() {
// we were told to stop trying via panic - which means we made it this far and should return successfully
return true
}
if nextPoll == nil {
nextPoll = assertion.afterPolling()
}
select {
case <-nextPoll:
v, e := pollActual()
lock.Lock()
value, err = v, e
lock.Unlock()
if err == nil {
oracleMatcherSaysStop = assertion.matcherSaysStopTrying(matcher, value)
m, e := assertion.pollMatcher(matcher, value)
lock.Lock()
value, err, stopTrying = v, e, st
matches, err = m, e
lock.Unlock()
if err == nil {
if stopTrying == nil {
stopTrying = assertion.matcherSaysStopTrying(matcher, value)
}
matches, e = matcher.Match(value)
lock.Lock()
err = e
lock.Unlock()
}
case <-contextDone:
fail("Context was cancelled")
}
case <-contextDone:
fail("Context was cancelled")
return false
case <-timeout:
if assertion.asyncType == AsyncAssertionTypeEventually {
fail("Timed out")
return false
case <-timeout:
} else {
if isTryAgainAfterError {
fail("Timed out while waiting on TryAgainAfter")
return false
}
return true
}
}
}
return false
}

View File

@ -44,28 +44,28 @@ func durationFromEnv(key string, defaultDuration time.Duration) time.Duration {
return duration
}
func toDuration(input interface{}) time.Duration {
func toDuration(input interface{}) (time.Duration, error) {
duration, ok := input.(time.Duration)
if ok {
return duration
return duration, nil
}
value := reflect.ValueOf(input)
kind := reflect.TypeOf(input).Kind()
if reflect.Int <= kind && kind <= reflect.Int64 {
return time.Duration(value.Int()) * time.Second
return time.Duration(value.Int()) * time.Second, nil
} else if reflect.Uint <= kind && kind <= reflect.Uint64 {
return time.Duration(value.Uint()) * time.Second
return time.Duration(value.Uint()) * time.Second, nil
} else if reflect.Float32 <= kind && kind <= reflect.Float64 {
return time.Duration(value.Float() * float64(time.Second))
return time.Duration(value.Float() * float64(time.Second)), nil
} else if reflect.String == kind {
duration, err := time.ParseDuration(value.String())
if err != nil {
panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input))
return 0, fmt.Errorf("%#v is not a valid parsable duration string: %w", input, err)
}
return duration
return duration, nil
}
panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input))
return 0, fmt.Errorf("%#v is not a valid interval. Must be a time.Duration, a parsable duration string, or a number.", input)
}

View File

@ -2,6 +2,7 @@ package internal
import (
"context"
"fmt"
"time"
"github.com/onsi/gomega/types"
@ -52,16 +53,46 @@ func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...inter
return NewAssertion(actual, g, offset, extra...)
}
func (g *Gomega) Eventually(actual interface{}, intervals ...interface{}) types.AsyncAssertion {
return g.EventuallyWithOffset(0, actual, intervals...)
func (g *Gomega) Eventually(args ...interface{}) types.AsyncAssertion {
return g.makeAsyncAssertion(AsyncAssertionTypeEventually, 0, args...)
}
func (g *Gomega) EventuallyWithOffset(offset int, actual interface{}, args ...interface{}) types.AsyncAssertion {
func (g *Gomega) EventuallyWithOffset(offset int, args ...interface{}) types.AsyncAssertion {
return g.makeAsyncAssertion(AsyncAssertionTypeEventually, offset, args...)
}
func (g *Gomega) Consistently(args ...interface{}) types.AsyncAssertion {
return g.makeAsyncAssertion(AsyncAssertionTypeConsistently, 0, args...)
}
func (g *Gomega) ConsistentlyWithOffset(offset int, args ...interface{}) types.AsyncAssertion {
return g.makeAsyncAssertion(AsyncAssertionTypeConsistently, offset, args...)
}
func (g *Gomega) makeAsyncAssertion(asyncAssertionType AsyncAssertionType, offset int, args ...interface{}) types.AsyncAssertion {
baseOffset := 3
timeoutInterval := -time.Duration(1)
pollingInterval := -time.Duration(1)
intervals := []interface{}{}
var ctx context.Context
for _, arg := range args {
if len(args) == 0 {
g.Fail(fmt.Sprintf("Call to %s is missing a value or function to poll", asyncAssertionType), offset+baseOffset)
return nil
}
actual := args[0]
startingIndex := 1
if _, isCtx := args[0].(context.Context); isCtx && len(args) > 1 {
// the first argument is a context, we should accept it as the context _only if_ it is **not** the only argumnent **and** the second argument is not a parseable duration
// this is due to an unfortunate ambiguity in early version of Gomega in which multi-type durations are allowed after the actual
if _, err := toDuration(args[1]); err != nil {
ctx = args[0].(context.Context)
actual = args[1]
startingIndex = 2
}
}
for _, arg := range args[startingIndex:] {
switch v := arg.(type) {
case context.Context:
ctx = v
@ -69,41 +100,21 @@ func (g *Gomega) EventuallyWithOffset(offset int, actual interface{}, args ...in
intervals = append(intervals, arg)
}
}
var err error
if len(intervals) > 0 {
timeoutInterval = toDuration(intervals[0])
}
if len(intervals) > 1 {
pollingInterval = toDuration(intervals[1])
}
return NewAsyncAssertion(AsyncAssertionTypeEventually, actual, g, timeoutInterval, pollingInterval, ctx, offset)
}
func (g *Gomega) Consistently(actual interface{}, intervals ...interface{}) types.AsyncAssertion {
return g.ConsistentlyWithOffset(0, actual, intervals...)
}
func (g *Gomega) ConsistentlyWithOffset(offset int, actual interface{}, args ...interface{}) types.AsyncAssertion {
timeoutInterval := -time.Duration(1)
pollingInterval := -time.Duration(1)
intervals := []interface{}{}
var ctx context.Context
for _, arg := range args {
switch v := arg.(type) {
case context.Context:
ctx = v
default:
intervals = append(intervals, arg)
timeoutInterval, err = toDuration(intervals[0])
if err != nil {
g.Fail(err.Error(), offset+baseOffset)
}
}
if len(intervals) > 0 {
timeoutInterval = toDuration(intervals[0])
}
if len(intervals) > 1 {
pollingInterval = toDuration(intervals[1])
pollingInterval, err = toDuration(intervals[1])
if err != nil {
g.Fail(err.Error(), offset+baseOffset)
}
}
return NewAsyncAssertion(AsyncAssertionTypeConsistently, actual, g, timeoutInterval, pollingInterval, ctx, offset)
return NewAsyncAssertion(asyncAssertionType, actual, g, timeoutInterval, pollingInterval, ctx, offset)
}
func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) {

View File

@ -0,0 +1,106 @@
package internal
import (
"errors"
"fmt"
"time"
)
type PollingSignalErrorType int
const (
PollingSignalErrorTypeStopTrying PollingSignalErrorType = iota
PollingSignalErrorTypeTryAgainAfter
)
type PollingSignalError interface {
error
Wrap(err error) PollingSignalError
Attach(description string, obj any) PollingSignalError
Now()
}
var StopTrying = func(message string) PollingSignalError {
return &PollingSignalErrorImpl{
message: message,
pollingSignalErrorType: PollingSignalErrorTypeStopTrying,
}
}
var TryAgainAfter = func(duration time.Duration) PollingSignalError {
return &PollingSignalErrorImpl{
message: fmt.Sprintf("told to try again after %s", duration),
duration: duration,
pollingSignalErrorType: PollingSignalErrorTypeTryAgainAfter,
}
}
type PollingSignalErrorAttachment struct {
Description string
Object any
}
type PollingSignalErrorImpl struct {
message string
wrappedErr error
pollingSignalErrorType PollingSignalErrorType
duration time.Duration
Attachments []PollingSignalErrorAttachment
}
func (s *PollingSignalErrorImpl) Wrap(err error) PollingSignalError {
s.wrappedErr = err
return s
}
func (s *PollingSignalErrorImpl) Attach(description string, obj any) PollingSignalError {
s.Attachments = append(s.Attachments, PollingSignalErrorAttachment{description, obj})
return s
}
func (s *PollingSignalErrorImpl) Error() string {
if s.wrappedErr == nil {
return s.message
} else {
return s.message + ": " + s.wrappedErr.Error()
}
}
func (s *PollingSignalErrorImpl) Unwrap() error {
if s == nil {
return nil
}
return s.wrappedErr
}
func (s *PollingSignalErrorImpl) Now() {
panic(s)
}
func (s *PollingSignalErrorImpl) IsStopTrying() bool {
return s.pollingSignalErrorType == PollingSignalErrorTypeStopTrying
}
func (s *PollingSignalErrorImpl) IsTryAgainAfter() bool {
return s.pollingSignalErrorType == PollingSignalErrorTypeTryAgainAfter
}
func (s *PollingSignalErrorImpl) TryAgainDuration() time.Duration {
return s.duration
}
func AsPollingSignalError(actual interface{}) (*PollingSignalErrorImpl, bool) {
if actual == nil {
return nil, false
}
if actualErr, ok := actual.(error); ok {
var target *PollingSignalErrorImpl
if errors.As(actualErr, &target) {
return target, true
} else {
return nil, false
}
}
return nil, false
}

View File

@ -19,11 +19,11 @@ type Gomega interface {
Expect(actual interface{}, extra ...interface{}) Assertion
ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion
Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion
EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion
Eventually(args ...interface{}) AsyncAssertion
EventuallyWithOffset(offset int, args ...interface{}) AsyncAssertion
Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion
ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion
Consistently(args ...interface{}) AsyncAssertion
ConsistentlyWithOffset(offset int, args ...interface{}) AsyncAssertion
SetDefaultEventuallyTimeout(time.Duration)
SetDefaultEventuallyPollingInterval(time.Duration)