mirror of
https://git.mirrors.martin98.com/https://github.com/ceph/ceph-csi.git
synced 2025-08-12 21:28:59 +08:00

Several packages are only used while running the e2e suite. These packages are less important to update, as the they can not influence the final executable that is part of the Ceph-CSI container-image. By moving these dependencies out of the main Ceph-CSI go.mod, it is easier to identify if a reported CVE affects Ceph-CSI, or only the testing (like most of the Kubernetes CVEs). Signed-off-by: Niels de Vos <ndevos@ibm.com>
585 lines
18 KiB
Go
585 lines
18 KiB
Go
package internal
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/onsi/gomega/format"
|
|
"github.com/onsi/gomega/types"
|
|
)
|
|
|
|
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()
|
|
}
|
|
|
|
type asyncGomegaHaltExecutionError struct{}
|
|
|
|
func (a asyncGomegaHaltExecutionError) GinkgoRecoverShouldIgnoreThisPanic() {}
|
|
func (a asyncGomegaHaltExecutionError) Error() string {
|
|
return `An assertion has failed in a goroutine. You should call
|
|
|
|
defer GinkgoRecover()
|
|
|
|
at the top of the goroutine that caused this panic. This will allow Ginkgo and Gomega to correctly capture and manage this panic.`
|
|
}
|
|
|
|
type AsyncAssertionType uint
|
|
|
|
const (
|
|
AsyncAssertionTypeEventually AsyncAssertionType = iota
|
|
AsyncAssertionTypeConsistently
|
|
)
|
|
|
|
func (at AsyncAssertionType) String() string {
|
|
switch at {
|
|
case AsyncAssertionTypeEventually:
|
|
return "Eventually"
|
|
case AsyncAssertionTypeConsistently:
|
|
return "Consistently"
|
|
}
|
|
return "INVALID ASYNC ASSERTION TYPE"
|
|
}
|
|
|
|
type AsyncAssertion struct {
|
|
asyncType AsyncAssertionType
|
|
|
|
actualIsFunc bool
|
|
actual interface{}
|
|
argsToForward []interface{}
|
|
|
|
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, mustPassRepeatedly int, ctx context.Context, offset int) *AsyncAssertion {
|
|
out := &AsyncAssertion{
|
|
asyncType: asyncType,
|
|
timeoutInterval: timeoutInterval,
|
|
pollingInterval: pollingInterval,
|
|
mustPassRepeatedly: mustPassRepeatedly,
|
|
offset: offset,
|
|
ctx: ctx,
|
|
g: g,
|
|
}
|
|
|
|
out.actual = actualInput
|
|
if actualInput != nil && reflect.TypeOf(actualInput).Kind() == reflect.Func {
|
|
out.actualIsFunc = true
|
|
}
|
|
|
|
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) Within(timeout time.Duration) types.AsyncAssertion {
|
|
assertion.timeoutInterval = timeout
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) ProbeEvery(interval time.Duration) types.AsyncAssertion {
|
|
assertion.pollingInterval = interval
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) WithContext(ctx context.Context) types.AsyncAssertion {
|
|
assertion.ctx = ctx
|
|
return assertion
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) WithArguments(argsToForward ...interface{}) types.AsyncAssertion {
|
|
assertion.argsToForward = argsToForward
|
|
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...)
|
|
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) processReturnValues(values []reflect.Value) (interface{}, error) {
|
|
if len(values) == 0 {
|
|
return nil, &asyncPolledActualError{
|
|
message: fmt.Sprintf("The function passed to %s did not return any values", assertion.asyncType),
|
|
}
|
|
}
|
|
|
|
actual := values[0].Interface()
|
|
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 _, ok := AsPollingSignalError(extra); ok {
|
|
return actual, extra.(error)
|
|
}
|
|
extraType := reflect.TypeOf(extra)
|
|
zero := reflect.Zero(extraType).Interface()
|
|
if reflect.DeepEqual(extra, zero) {
|
|
continue
|
|
}
|
|
if i == len(values)-2 && extraType.Implements(errInterface) {
|
|
err = extra.(error)
|
|
}
|
|
if err == nil {
|
|
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)),
|
|
}
|
|
}
|
|
}
|
|
|
|
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:
|
|
|
|
(a) have return values or
|
|
(b) take a Gomega interface as their first argument and use that Gomega instance to make assertions.
|
|
|
|
You can learn more at https://onsi.github.io/gomega/#eventually
|
|
`, assertion.asyncType, t, assertion.asyncType)
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) noConfiguredContextForFunctionError() error {
|
|
return fmt.Errorf(`The function passed to %s requested a context.Context, but no context has been provided. Please pass one in using %s().WithContext().
|
|
|
|
You can learn more at https://onsi.github.io/gomega/#eventually
|
|
`, assertion.asyncType, assertion.asyncType)
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) argumentMismatchError(t reflect.Type, numProvided int) error {
|
|
have := "have"
|
|
if numProvided == 1 {
|
|
have = "has"
|
|
}
|
|
return fmt.Errorf(`The function passed to %s has signature %s takes %d arguments but %d %s been provided. Please use %s().WithArguments() to pass the corect set of arguments.
|
|
|
|
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
|
|
}
|
|
actualValue := reflect.ValueOf(assertion.actual)
|
|
actualType := reflect.TypeOf(assertion.actual)
|
|
numIn, numOut, isVariadic := actualType.NumIn(), actualType.NumOut(), actualType.IsVariadic()
|
|
|
|
if numIn == 0 && numOut == 0 {
|
|
return nil, assertion.invalidFunctionError(actualType)
|
|
}
|
|
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
|
|
}
|
|
if takesContext && len(assertion.argsToForward) > 0 && reflect.TypeOf(assertion.argsToForward[0]).Implements(contextType) {
|
|
takesContext = false
|
|
}
|
|
if !takesGomega && numOut == 0 {
|
|
return nil, assertion.invalidFunctionError(actualType)
|
|
}
|
|
if takesContext && assertion.ctx == nil {
|
|
return nil, assertion.noConfiguredContextForFunctionError()
|
|
}
|
|
|
|
var assertionFailure error
|
|
inValues := []reflect.Value{}
|
|
if takesGomega {
|
|
inValues = append(inValues, reflect.ValueOf(NewGomega(assertion.g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) {
|
|
skip := 0
|
|
if len(callerSkip) > 0 {
|
|
skip = callerSkip[0]
|
|
}
|
|
_, file, line, _ := runtime.Caller(skip + 1)
|
|
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{})
|
|
})))
|
|
}
|
|
if takesContext {
|
|
inValues = append(inValues, reflect.ValueOf(assertion.ctx))
|
|
}
|
|
for _, arg := range assertion.argsToForward {
|
|
inValues = append(inValues, reflect.ValueOf(arg))
|
|
}
|
|
|
|
if !isVariadic && numIn != len(inValues) {
|
|
return nil, assertion.argumentMismatchError(actualType, len(inValues))
|
|
} else if isVariadic && len(inValues) < numIn-1 {
|
|
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
|
|
defer func() {
|
|
if numOut == 0 && takesGomega {
|
|
actual = assertionFailure
|
|
} else {
|
|
actual, err = assertion.processReturnValues(values)
|
|
_, isAsyncError := AsPollingSignalError(err)
|
|
if assertionFailure != nil && !isAsyncError {
|
|
err = assertionFailure
|
|
}
|
|
}
|
|
if e := recover(); e != nil {
|
|
if _, isAsyncError := AsPollingSignalError(e); isAsyncError {
|
|
err = e.(error)
|
|
} else if assertionFailure == nil {
|
|
panic(e)
|
|
}
|
|
}
|
|
}()
|
|
values = actualValue.Call(inValues)
|
|
return
|
|
}, nil
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) afterTimeout() <-chan time.Time {
|
|
if assertion.timeoutInterval >= 0 {
|
|
return time.After(assertion.timeoutInterval)
|
|
}
|
|
|
|
if assertion.asyncType == AsyncAssertionTypeConsistently {
|
|
return time.After(assertion.g.DurationBundle.ConsistentlyDuration)
|
|
} else {
|
|
if assertion.ctx == nil || assertion.g.DurationBundle.EnforceDefaultTimeoutsWhenUsingContexts {
|
|
return time.After(assertion.g.DurationBundle.EventuallyTimeout)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (assertion *AsyncAssertion) afterPolling() <-chan time.Time {
|
|
if assertion.pollingInterval >= 0 {
|
|
return time.After(assertion.pollingInterval)
|
|
}
|
|
if assertion.asyncType == AsyncAssertionTypeConsistently {
|
|
return time.After(assertion.g.DurationBundle.ConsistentlyPollingInterval)
|
|
} else {
|
|
return time.After(assertion.g.DurationBundle.EventuallyPollingInterval)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
timer := time.Now()
|
|
timeout := assertion.afterTimeout()
|
|
lock := sync.Mutex{}
|
|
|
|
var matches, hasLastValidActual bool
|
|
var actual, lastValidActual interface{}
|
|
var actualErr, matcherErr error
|
|
var oracleMatcherSaysStop bool
|
|
|
|
assertion.g.THelper()
|
|
|
|
pollActual, buildActualPollerErr := assertion.buildActualPoller()
|
|
if buildActualPollerErr != nil {
|
|
assertion.g.Fail(buildActualPollerErr.Error(), 2+assertion.offset)
|
|
return false
|
|
}
|
|
|
|
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" + format.Object(err, 1)
|
|
}
|
|
return message
|
|
}
|
|
|
|
messageGenerator := func() string {
|
|
// can be called out of band by Ginkgo if the user requests a progress report
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
message := ""
|
|
|
|
if actualErr == nil {
|
|
if matcherErr == nil {
|
|
if desiredMatch != matches {
|
|
if desiredMatch {
|
|
message += matcher.FailureMessage(actual)
|
|
} else {
|
|
message += matcher.NegatedFailureMessage(actual)
|
|
}
|
|
} else {
|
|
if assertion.asyncType == AsyncAssertionTypeConsistently {
|
|
message += "There is no failure as the matcher passed to Consistently has not yet failed"
|
|
} else {
|
|
message += "There is no failure as the matcher passed to Eventually succeeded on its most recent iteration"
|
|
}
|
|
}
|
|
} else {
|
|
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 {
|
|
var fgErr formattedGomegaError
|
|
if errors.As(actualErr, &fgErr) {
|
|
message += fgErr.FormattedGomegaError() + "\n"
|
|
} else {
|
|
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)
|
|
}
|
|
|
|
fail := func(preamble string) {
|
|
assertion.g.THelper()
|
|
assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s", preamble, time.Since(timer).Seconds(), messageGenerator()), 3+assertion.offset)
|
|
}
|
|
|
|
var contextDone <-chan struct{}
|
|
if assertion.ctx != nil {
|
|
contextDone = assertion.ctx.Done()
|
|
if v, ok := assertion.ctx.Value("GINKGO_SPEC_CONTEXT").(contextWithAttachProgressReporter); ok {
|
|
detach := v.AttachProgressReporter(messageGenerator)
|
|
defer detach()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
for _, err := range []error{actualErr, matcherErr} {
|
|
if pollingSignalErr, ok := AsPollingSignalError(err); ok {
|
|
if pollingSignalErr.IsStopTrying() {
|
|
if pollingSignalErr.IsSuccessful() {
|
|
if assertion.asyncType == AsyncAssertionTypeEventually {
|
|
fail("Told to stop trying (and ignoring call to Successfully(), as it is only relevant with Consistently)")
|
|
} else {
|
|
return true // early escape hatch for Consistently
|
|
}
|
|
} else {
|
|
fail("Told to stop trying")
|
|
}
|
|
return false
|
|
}
|
|
if pollingSignalErr.IsTryAgainAfter() {
|
|
nextPoll = time.After(pollingSignalErr.TryAgainDuration())
|
|
isTryAgainAfterError = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if actualErr == nil && matcherErr == nil && matches == desiredMatch {
|
|
if assertion.asyncType == AsyncAssertionTypeEventually {
|
|
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 {
|
|
if assertion.asyncType == AsyncAssertionTypeEventually {
|
|
fail("No future change is possible. Bailing out early")
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
if nextPoll == nil {
|
|
nextPoll = assertion.afterPolling()
|
|
}
|
|
|
|
select {
|
|
case <-nextPoll:
|
|
a, e := pollActual()
|
|
lock.Lock()
|
|
actual, actualErr = a, e
|
|
lock.Unlock()
|
|
if actualErr == nil {
|
|
lock.Lock()
|
|
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:
|
|
err := context.Cause(assertion.ctx)
|
|
if err != nil && err != context.Canceled {
|
|
fail(fmt.Sprintf("Context was cancelled (cause: %s)", err))
|
|
} else {
|
|
fail("Context was cancelled")
|
|
}
|
|
return false
|
|
case <-timeout:
|
|
if assertion.asyncType == AsyncAssertionTypeEventually {
|
|
fail("Timed out")
|
|
return false
|
|
} else {
|
|
if isTryAgainAfterError {
|
|
fail("Timed out while waiting on TryAgainAfter")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|