...

Source file src/k8s.io/kubernetes/test/utils/ktesting/assert.go

Documentation: k8s.io/kubernetes/test/utils/ktesting

     1  /*
     2  Copyright 2024 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 ktesting
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/onsi/gomega"
    26  	"github.com/onsi/gomega/format"
    27  )
    28  
    29  // FailureError is an error where the error string is meant to be passed to
    30  // [TContext.Fatal] directly, i.e. adding some prefix like "unexpected error" is not
    31  // necessary. It is also not necessary to dump the error struct.
    32  type FailureError struct {
    33  	Msg            string
    34  	FullStackTrace string
    35  }
    36  
    37  func (f FailureError) Error() string {
    38  	return f.Msg
    39  }
    40  
    41  func (f FailureError) Backtrace() string {
    42  	return f.FullStackTrace
    43  }
    44  
    45  func (f FailureError) Is(target error) bool {
    46  	return target == ErrFailure
    47  }
    48  
    49  // ErrFailure is an empty error that can be wrapped to indicate that an error
    50  // is a FailureError. It can also be used to test for a FailureError:.
    51  //
    52  //	return fmt.Errorf("some problem%w", ErrFailure)
    53  //	...
    54  //	err := someOperation()
    55  //	if errors.Is(err, ErrFailure) {
    56  //	    ...
    57  //	}
    58  var ErrFailure error = FailureError{}
    59  
    60  func expect(tCtx TContext, actual interface{}, extra ...interface{}) gomega.Assertion {
    61  	tCtx.Helper()
    62  	return gomega.NewWithT(tCtx).Expect(actual, extra...)
    63  }
    64  
    65  func expectNoError(tCtx TContext, err error, explain ...interface{}) {
    66  	if err == nil {
    67  		return
    68  	}
    69  
    70  	tCtx.Helper()
    71  
    72  	description := buildDescription(explain...)
    73  
    74  	if errors.Is(err, ErrFailure) {
    75  		var failure FailureError
    76  		if errors.As(err, &failure) {
    77  			if backtrace := failure.Backtrace(); backtrace != "" {
    78  				if description != "" {
    79  					tCtx.Log(description)
    80  				}
    81  				tCtx.Logf("Failed at:\n    %s", strings.ReplaceAll(backtrace, "\n", "\n    "))
    82  			}
    83  		}
    84  		if description != "" {
    85  			tCtx.Fatalf("%s: %s", description, err.Error())
    86  		}
    87  		tCtx.Fatal(err.Error())
    88  	}
    89  
    90  	if description == "" {
    91  		description = "Unexpected error"
    92  	}
    93  	tCtx.Logf("%s:\n%s", description, format.Object(err, 1))
    94  	tCtx.Fatalf("%s: %v", description, err.Error())
    95  }
    96  
    97  func buildDescription(explain ...interface{}) string {
    98  	switch len(explain) {
    99  	case 0:
   100  		return ""
   101  	case 1:
   102  		if describe, ok := explain[0].(func() string); ok {
   103  			return describe()
   104  		}
   105  	}
   106  	return fmt.Sprintf(explain[0].(string), explain[1:]...)
   107  }
   108  
   109  // Eventually wraps [gomega.Eventually] such that a failure will be reported via
   110  // TContext.Fatal.
   111  //
   112  // In contrast to [gomega.Eventually], the parameter is strongly typed. It must
   113  // accept a TContext as first argument and return one value, the one which is
   114  // then checked with the matcher.
   115  //
   116  // In contrast to direct usage of [gomega.Eventually], make additional
   117  // assertions inside the callback is okay as long as they use the TContext that
   118  // is passed in. For example, errors can be checked with ExpectNoError:
   119  //
   120  //	cb := func(func(tCtx ktesting.TContext) int {
   121  //	    value, err := doSomething(...)
   122  //	    tCtx.ExpectNoError(err, "something failed")
   123  //	    assert(tCtx, 42, value, "the answer")
   124  //	    return value
   125  //	}
   126  //	tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything")
   127  //
   128  // If there is no value, then an error can be returned:
   129  //
   130  //	cb := func(func(tCtx ktesting.TContext) error {
   131  //	    err := doSomething(...)
   132  //	    return err
   133  //	}
   134  //	tCtx.Eventually(cb).Should(gomega.Succeed(), "foobar should succeed")
   135  //
   136  // The default Gomega poll interval and timeout are used. Setting a specific
   137  // timeout may be useful:
   138  //
   139  //	tCtx.Eventually(cb).Timeout(5 * time.Second).Should(gomega.Succeed(), "foobar should succeed")
   140  //
   141  // Canceling the context in the callback only affects code in the callback. The
   142  // context passed to Eventually is not getting canceled. To abort polling
   143  // immediately because the expected condition is known to not be reached
   144  // anymore, use [gomega.StopTrying]:
   145  //
   146  //	cb := func(func(tCtx ktesting.TContext) int {
   147  //	    value, err := doSomething(...)
   148  //	    if errors.Is(err, SomeFinalErr) {
   149  //	        // This message completely replaces the normal
   150  //	        // failure message and thus should include all
   151  //	        // relevant information.
   152  //	        //
   153  //	        // github.com/onsi/gomega/format is a good way
   154  //	        // to format arbitrary data. It uses indention
   155  //	        // and falls back to YAML for Kubernetes API
   156  //	        // structs for readability.
   157  //	        gomega.StopTrying("permanent failure, last value:\n%s", format.Object(value, 1 /* indent one level */)).
   158  //	            Wrap(err).Now()
   159  //	    }
   160  //	    ktesting.ExpectNoError(tCtx, err, "something failed")
   161  //	    return value
   162  //	}
   163  //	tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything")
   164  //
   165  // To poll again after some specific timeout, use [gomega.TryAgainAfter]. This is
   166  // particularly useful in [Consistently] to ignore some intermittent error.
   167  //
   168  //	cb := func(func(tCtx ktesting.TContext) int {
   169  //	    value, err := doSomething(...)
   170  //	    var intermittentErr SomeIntermittentError
   171  //	    if errors.As(err, &intermittentErr) {
   172  //	        gomega.TryAgainAfter(intermittentErr.RetryPeriod).Wrap(err).Now()
   173  //	    }
   174  //	    ktesting.ExpectNoError(tCtx, err, "something failed")
   175  //	    return value
   176  //	 }
   177  //	 tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything")
   178  func Eventually[T any](tCtx TContext, cb func(TContext) T) gomega.AsyncAssertion {
   179  	tCtx.Helper()
   180  	return gomega.NewWithT(tCtx).Eventually(tCtx, func(ctx context.Context) (val T, err error) {
   181  		tCtx := WithContext(tCtx, ctx)
   182  		tCtx, finalize := WithError(tCtx, &err)
   183  		defer finalize()
   184  		tCtx = WithCancel(tCtx)
   185  		return cb(tCtx), nil
   186  	})
   187  }
   188  
   189  // Consistently wraps [gomega.Consistently] the same way as [Eventually] wraps
   190  // [gomega.Eventually].
   191  func Consistently[T any](tCtx TContext, cb func(TContext) T) gomega.AsyncAssertion {
   192  	tCtx.Helper()
   193  	return gomega.NewWithT(tCtx).Consistently(tCtx, func(ctx context.Context) (val T, err error) {
   194  		tCtx := WithContext(tCtx, ctx)
   195  		tCtx, finalize := WithError(tCtx, &err)
   196  		defer finalize()
   197  		return cb(tCtx), nil
   198  	})
   199  }
   200  

View as plain text