
Ginkgo v2.1.6 adds ginkgo.SuppressProgressReporting which is needed to suppress too verbose output each time the ReportAfterEach of the custom progress reporter is invoked.
238 lines
6.6 KiB
Go
238 lines
6.6 KiB
Go
package internal
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"time"
|
|
|
|
"github.com/onsi/gomega/types"
|
|
)
|
|
|
|
type AsyncAssertionType uint
|
|
|
|
const (
|
|
AsyncAssertionTypeEventually AsyncAssertionType = iota
|
|
AsyncAssertionTypeConsistently
|
|
)
|
|
|
|
type AsyncAssertion struct {
|
|
asyncType AsyncAssertionType
|
|
|
|
actualIsFunc bool
|
|
actualValue interface{}
|
|
actualFunc func() ([]reflect.Value, error)
|
|
|
|
timeoutInterval time.Duration
|
|
pollingInterval time.Duration
|
|
offset int
|
|
g *Gomega
|
|
}
|
|
|
|
func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion {
|
|
out := &AsyncAssertion{
|
|
asyncType: asyncType,
|
|
timeoutInterval: timeoutInterval,
|
|
pollingInterval: pollingInterval,
|
|
offset: offset,
|
|
g: g,
|
|
}
|
|
|
|
switch actualType := reflect.TypeOf(actualInput); {
|
|
case actualInput == nil || actualType.Kind() != reflect.Func:
|
|
out.actualValue = actualInput
|
|
case actualType.NumIn() == 0 && actualType.NumOut() > 0:
|
|
out.actualIsFunc = true
|
|
out.actualFunc = func() ([]reflect.Value, error) {
|
|
return reflect.ValueOf(actualInput).Call([]reflect.Value{}), nil
|
|
}
|
|
case actualType.NumIn() == 1 && actualType.In(0).Implements(reflect.TypeOf((*types.Gomega)(nil)).Elem()):
|
|
out.actualIsFunc = true
|
|
out.actualFunc = func() (values []reflect.Value, err error) {
|
|
var assertionFailure error
|
|
assertionCapturingGomega := NewGomega(g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) {
|
|
skip := 0
|
|
if len(callerSkip) > 0 {
|
|
skip = callerSkip[0]
|
|
}
|
|
_, file, line, _ := runtime.Caller(skip + 1)
|
|
assertionFailure = fmt.Errorf("Assertion in callback at %s:%d failed:\n%s", file, line, message)
|
|
panic("stop execution")
|
|
})
|
|
|
|
defer func() {
|
|
if actualType.NumOut() == 0 {
|
|
if assertionFailure == nil {
|
|
values = []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())}
|
|
} else {
|
|
values = []reflect.Value{reflect.ValueOf(assertionFailure)}
|
|
}
|
|
} else {
|
|
err = assertionFailure
|
|
}
|
|
if e := recover(); e != nil && assertionFailure == nil {
|
|
panic(e)
|
|
}
|
|
}()
|
|
|
|
values = reflect.ValueOf(actualInput).Call([]reflect.Value{reflect.ValueOf(assertionCapturingGomega)})
|
|
return
|
|
}
|
|
default:
|
|
msg := fmt.Sprintf("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes %d arguments and returns %d values.", actualType.NumIn(), actualType.NumOut())
|
|
g.Fail(msg, offset+4)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) WithOffset(offset int) types.AsyncAssertion {
|
|
assertion.offset = offset
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) WithTimeout(interval time.Duration) types.AsyncAssertion {
|
|
assertion.timeoutInterval = interval
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) WithPolling(interval time.Duration) types.AsyncAssertion {
|
|
assertion.pollingInterval = interval
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
|
|
assertion.g.THelper()
|
|
vetOptionalDescription("Asynchronous assertion", optionalDescription...)
|
|
return assertion.match(matcher, true, optionalDescription...)
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
|
|
assertion.g.THelper()
|
|
vetOptionalDescription("Asynchronous assertion", optionalDescription...)
|
|
return assertion.match(matcher, false, optionalDescription...)
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interface{}) string {
|
|
switch len(optionalDescription) {
|
|
case 0:
|
|
return ""
|
|
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) pollActual() (interface{}, error) {
|
|
if !assertion.actualIsFunc {
|
|
return assertion.actualValue, nil
|
|
}
|
|
|
|
values, err := assertion.actualFunc()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
extras := []interface{}{nil}
|
|
for _, value := range values[1:] {
|
|
extras = append(extras, value.Interface())
|
|
}
|
|
success, message := vetActuals(extras, 0)
|
|
if !success {
|
|
return nil, errors.New(message)
|
|
}
|
|
|
|
return values[0].Interface(), nil
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool {
|
|
if assertion.actualIsFunc {
|
|
return true
|
|
}
|
|
return types.MatchMayChangeInTheFuture(matcher, value)
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
|
|
timer := time.Now()
|
|
timeout := time.After(assertion.timeoutInterval)
|
|
|
|
var matches bool
|
|
var err error
|
|
mayChange := true
|
|
value, err := assertion.pollActual()
|
|
if err == nil {
|
|
mayChange = assertion.matcherMayChange(matcher, value)
|
|
matches, err = matcher.Match(value)
|
|
}
|
|
|
|
assertion.g.THelper()
|
|
|
|
fail := func(preamble string) {
|
|
errMsg := ""
|
|
message := ""
|
|
if err != nil {
|
|
errMsg = "Error: " + err.Error()
|
|
} else {
|
|
if desiredMatch {
|
|
message = matcher.FailureMessage(value)
|
|
} else {
|
|
message = matcher.NegatedFailureMessage(value)
|
|
}
|
|
}
|
|
assertion.g.THelper()
|
|
description := assertion.buildDescription(optionalDescription...)
|
|
assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset)
|
|
}
|
|
|
|
if assertion.asyncType == AsyncAssertionTypeEventually {
|
|
for {
|
|
if err == nil && matches == desiredMatch {
|
|
return true
|
|
}
|
|
|
|
if !mayChange {
|
|
fail("No future change is possible. Bailing out early")
|
|
return false
|
|
}
|
|
|
|
select {
|
|
case <-time.After(assertion.pollingInterval):
|
|
value, err = assertion.pollActual()
|
|
if err == nil {
|
|
mayChange = assertion.matcherMayChange(matcher, value)
|
|
matches, err = matcher.Match(value)
|
|
}
|
|
case <-timeout:
|
|
fail("Timed out")
|
|
return false
|
|
}
|
|
}
|
|
} else if assertion.asyncType == AsyncAssertionTypeConsistently {
|
|
for {
|
|
if !(err == nil && matches == desiredMatch) {
|
|
fail("Failed")
|
|
return false
|
|
}
|
|
|
|
if !mayChange {
|
|
return true
|
|
}
|
|
|
|
select {
|
|
case <-time.After(assertion.pollingInterval):
|
|
value, err = assertion.pollActual()
|
|
if err == nil {
|
|
mayChange = assertion.matcherMayChange(matcher, value)
|
|
matches, err = matcher.Match(value)
|
|
}
|
|
case <-timeout:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|