k8s.io/kubernetes@v1.29.3/test/e2e/framework/expect.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package framework
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	ginkgotypes "github.com/onsi/ginkgo/v2/types"
    27  	"github.com/onsi/gomega"
    28  	"github.com/onsi/gomega/format"
    29  	"github.com/onsi/gomega/types"
    30  )
    31  
    32  // MakeMatcher builds a gomega.Matcher based on a single callback function.
    33  // That function is passed the actual value that is to be checked.
    34  // There are three possible outcomes of the check:
    35  //   - An error is returned, which then is converted into a failure
    36  //     by Gomega.
    37  //   - A non-nil failure function is returned, which then is called
    38  //     by Gomega once a failure string is needed. This is useful
    39  //     to avoid unnecessarily preparing a failure string for intermediate
    40  //     failures in Eventually or Consistently.
    41  //   - Both function and error are nil, which means that the check
    42  //     succeeded.
    43  func MakeMatcher[T interface{}](match func(actual T) (failure func() string, err error)) types.GomegaMatcher {
    44  	return &matcher[T]{
    45  		match: match,
    46  	}
    47  }
    48  
    49  type matcher[T interface{}] struct {
    50  	match   func(actual T) (func() string, error)
    51  	failure func() string
    52  }
    53  
    54  func (m *matcher[T]) Match(actual interface{}) (success bool, err error) {
    55  	if actual, ok := actual.(T); ok {
    56  		failure, err := m.match(actual)
    57  		if err != nil {
    58  			return false, err
    59  		}
    60  		m.failure = failure
    61  		if failure != nil {
    62  			return false, nil
    63  		}
    64  		return true, nil
    65  	}
    66  	var empty T
    67  	return false, gomega.StopTrying(fmt.Sprintf("internal error: expected %T, got:\n%s", empty, format.Object(actual, 1)))
    68  }
    69  
    70  func (m *matcher[T]) FailureMessage(actual interface{}) string {
    71  	return m.failure()
    72  }
    73  
    74  func (m matcher[T]) NegatedFailureMessage(actual interface{}) string {
    75  	return m.failure()
    76  }
    77  
    78  var _ types.GomegaMatcher = &matcher[string]{}
    79  
    80  // Gomega returns an interface that can be used like gomega to express
    81  // assertions. The difference is that failed assertions are returned as an
    82  // error:
    83  //
    84  //	if err := Gomega().Expect(pod.Status.Phase).To(gomega.Equal(v1.Running)); err != nil {
    85  //	    return fmt.Errorf("test pod not running: %w", err)
    86  //	}
    87  //
    88  // This error can get wrapped to provide additional context for the
    89  // failure. The test then should use ExpectNoError to turn a non-nil error into
    90  // a failure.
    91  //
    92  // When using this approach, there is no need for call offsets and extra
    93  // descriptions for the Expect call because the call stack will be dumped when
    94  // ExpectNoError is called and the additional description(s) can be added by
    95  // wrapping the error.
    96  //
    97  // Asynchronous assertions use the framework's Poll interval and PodStart timeout
    98  // by default.
    99  func Gomega() GomegaInstance {
   100  	return gomegaInstance{}
   101  }
   102  
   103  type GomegaInstance interface {
   104  	Expect(actual interface{}) Assertion
   105  	Eventually(ctx context.Context, args ...interface{}) AsyncAssertion
   106  	Consistently(ctx context.Context, args ...interface{}) AsyncAssertion
   107  }
   108  
   109  type Assertion interface {
   110  	Should(matcher types.GomegaMatcher) error
   111  	ShouldNot(matcher types.GomegaMatcher) error
   112  	To(matcher types.GomegaMatcher) error
   113  	ToNot(matcher types.GomegaMatcher) error
   114  	NotTo(matcher types.GomegaMatcher) error
   115  }
   116  
   117  type AsyncAssertion interface {
   118  	Should(matcher types.GomegaMatcher) error
   119  	ShouldNot(matcher types.GomegaMatcher) error
   120  
   121  	WithTimeout(interval time.Duration) AsyncAssertion
   122  	WithPolling(interval time.Duration) AsyncAssertion
   123  }
   124  
   125  type gomegaInstance struct{}
   126  
   127  var _ GomegaInstance = gomegaInstance{}
   128  
   129  func (g gomegaInstance) Expect(actual interface{}) Assertion {
   130  	return assertion{actual: actual}
   131  }
   132  
   133  func (g gomegaInstance) Eventually(ctx context.Context, args ...interface{}) AsyncAssertion {
   134  	return newAsyncAssertion(ctx, args, false)
   135  }
   136  
   137  func (g gomegaInstance) Consistently(ctx context.Context, args ...interface{}) AsyncAssertion {
   138  	return newAsyncAssertion(ctx, args, true)
   139  }
   140  
   141  func newG() (*FailureError, gomega.Gomega) {
   142  	var failure FailureError
   143  	g := gomega.NewGomega(func(msg string, callerSkip ...int) {
   144  		failure = FailureError{
   145  			msg: msg,
   146  		}
   147  	})
   148  
   149  	return &failure, g
   150  }
   151  
   152  type assertion struct {
   153  	actual interface{}
   154  }
   155  
   156  func (a assertion) Should(matcher types.GomegaMatcher) error {
   157  	err, g := newG()
   158  	if !g.Expect(a.actual).Should(matcher) {
   159  		err.backtrace()
   160  		return *err
   161  	}
   162  	return nil
   163  }
   164  
   165  func (a assertion) ShouldNot(matcher types.GomegaMatcher) error {
   166  	err, g := newG()
   167  	if !g.Expect(a.actual).ShouldNot(matcher) {
   168  		err.backtrace()
   169  		return *err
   170  	}
   171  	return nil
   172  }
   173  
   174  func (a assertion) To(matcher types.GomegaMatcher) error {
   175  	err, g := newG()
   176  	if !g.Expect(a.actual).To(matcher) {
   177  		err.backtrace()
   178  		return *err
   179  	}
   180  	return nil
   181  }
   182  
   183  func (a assertion) ToNot(matcher types.GomegaMatcher) error {
   184  	err, g := newG()
   185  	if !g.Expect(a.actual).ToNot(matcher) {
   186  		err.backtrace()
   187  		return *err
   188  	}
   189  	return nil
   190  }
   191  
   192  func (a assertion) NotTo(matcher types.GomegaMatcher) error {
   193  	err, g := newG()
   194  	if !g.Expect(a.actual).NotTo(matcher) {
   195  		err.backtrace()
   196  		return *err
   197  	}
   198  	return nil
   199  }
   200  
   201  type asyncAssertion struct {
   202  	ctx          context.Context
   203  	args         []interface{}
   204  	timeout      time.Duration
   205  	interval     time.Duration
   206  	consistently bool
   207  }
   208  
   209  func newAsyncAssertion(ctx context.Context, args []interface{}, consistently bool) asyncAssertion {
   210  	return asyncAssertion{
   211  		ctx:  ctx,
   212  		args: args,
   213  		// PodStart is used as default because waiting for a pod is the
   214  		// most common operation.
   215  		timeout:      TestContext.timeouts.PodStart,
   216  		interval:     TestContext.timeouts.Poll,
   217  		consistently: consistently,
   218  	}
   219  }
   220  
   221  func (a asyncAssertion) newAsync() (*FailureError, gomega.AsyncAssertion) {
   222  	err, g := newG()
   223  	var assertion gomega.AsyncAssertion
   224  	if a.consistently {
   225  		assertion = g.Consistently(a.ctx, a.args...)
   226  	} else {
   227  		assertion = g.Eventually(a.ctx, a.args...)
   228  	}
   229  	assertion = assertion.WithTimeout(a.timeout).WithPolling(a.interval)
   230  	return err, assertion
   231  }
   232  
   233  func (a asyncAssertion) Should(matcher types.GomegaMatcher) error {
   234  	err, assertion := a.newAsync()
   235  	if !assertion.Should(matcher) {
   236  		err.backtrace()
   237  		return *err
   238  	}
   239  	return nil
   240  }
   241  
   242  func (a asyncAssertion) ShouldNot(matcher types.GomegaMatcher) error {
   243  	err, assertion := a.newAsync()
   244  	if !assertion.ShouldNot(matcher) {
   245  		err.backtrace()
   246  		return *err
   247  	}
   248  	return nil
   249  }
   250  
   251  func (a asyncAssertion) WithTimeout(timeout time.Duration) AsyncAssertion {
   252  	a.timeout = timeout
   253  	return a
   254  }
   255  
   256  func (a asyncAssertion) WithPolling(interval time.Duration) AsyncAssertion {
   257  	a.interval = interval
   258  	return a
   259  }
   260  
   261  // FailureError is an error where the error string is meant to be passed to
   262  // ginkgo.Fail directly, i.e. adding some prefix like "unexpected error" is not
   263  // necessary. It is also not necessary to dump the error struct.
   264  type FailureError struct {
   265  	msg            string
   266  	fullStackTrace string
   267  }
   268  
   269  func (f FailureError) Error() string {
   270  	return f.msg
   271  }
   272  
   273  func (f FailureError) Backtrace() string {
   274  	return f.fullStackTrace
   275  }
   276  
   277  func (f FailureError) Is(target error) bool {
   278  	return target == ErrFailure
   279  }
   280  
   281  func (f *FailureError) backtrace() {
   282  	f.fullStackTrace = ginkgotypes.NewCodeLocationWithStackTrace(2).FullStackTrace
   283  }
   284  
   285  // ErrFailure is an empty error that can be wrapped to indicate that an error
   286  // is a FailureError. It can also be used to test for a FailureError:.
   287  //
   288  //	return fmt.Errorf("some problem%w", ErrFailure)
   289  //	...
   290  //	err := someOperation()
   291  //	if errors.Is(err, ErrFailure) {
   292  //	    ...
   293  //	}
   294  var ErrFailure error = FailureError{}
   295  
   296  // ExpectNotEqual expects the specified two are not the same, otherwise an exception raises
   297  //
   298  // Deprecated: use gomega.Expect().ToNot(gomega.Equal())
   299  func ExpectNotEqual(actual interface{}, extra interface{}, explain ...interface{}) {
   300  	gomega.ExpectWithOffset(1, actual).NotTo(gomega.Equal(extra), explain...)
   301  }
   302  
   303  // ExpectError expects an error happens, otherwise an exception raises
   304  //
   305  // Deprecated: use gomega.Expect().To(gomega.HaveOccurred()) or (better!) check
   306  // specifically for the error that is expected with
   307  // gomega.Expect().To(gomega.MatchError(gomega.ContainSubstring()))
   308  func ExpectError(err error, explain ...interface{}) {
   309  	gomega.ExpectWithOffset(1, err).To(gomega.HaveOccurred(), explain...)
   310  }
   311  
   312  // ExpectNoError checks if "err" is set, and if so, fails assertion while logging the error.
   313  func ExpectNoError(err error, explain ...interface{}) {
   314  	ExpectNoErrorWithOffset(1, err, explain...)
   315  }
   316  
   317  // ExpectNoErrorWithOffset checks if "err" is set, and if so, fails assertion while logging the error at "offset" levels above its caller
   318  // (for example, for call chain f -> g -> ExpectNoErrorWithOffset(1, ...) error would be logged for "f").
   319  func ExpectNoErrorWithOffset(offset int, err error, explain ...interface{}) {
   320  	if err == nil {
   321  		return
   322  	}
   323  
   324  	// Errors usually contain unexported fields. We have to use
   325  	// a formatter here which can print those.
   326  	prefix := ""
   327  	if len(explain) > 0 {
   328  		if str, ok := explain[0].(string); ok {
   329  			prefix = fmt.Sprintf(str, explain[1:]...) + ": "
   330  		} else {
   331  			prefix = fmt.Sprintf("unexpected explain arguments, need format string: %v", explain)
   332  		}
   333  	}
   334  
   335  	// This intentionally doesn't use gomega.Expect. Instead we take
   336  	// full control over what information is presented where:
   337  	// - The complete error object is logged because it may contain
   338  	//   additional information that isn't included in its error
   339  	//   string.
   340  	// - It is not included in the failure message because
   341  	//   it might make the failure message very large and/or
   342  	//   cause error aggregation to work less well: two
   343  	//   failures at the same code line might not be matched in
   344  	//   https://go.k8s.io/triage because the error details are too
   345  	//   different.
   346  	//
   347  	// Some errors include all relevant information in the Error
   348  	// string. For those we can skip the redundant log message.
   349  	// For our own failures we only log the additional stack backtrace
   350  	// because it is not included in the failure message.
   351  	var failure FailureError
   352  	if errors.As(err, &failure) && failure.Backtrace() != "" {
   353  		Logf("Failed inside E2E framework:\n    %s", strings.ReplaceAll(failure.Backtrace(), "\n", "\n    "))
   354  	} else if !errors.Is(err, ErrFailure) {
   355  		Logf("Unexpected error: %s\n%s", prefix, format.Object(err, 1))
   356  	}
   357  	Fail(prefix+err.Error(), 1+offset)
   358  }