...

Source file src/k8s.io/kubernetes/test/utils/ktesting/errorcontext.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  	"errors"
    21  	"fmt"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/onsi/gomega"
    26  	"k8s.io/klog/v2"
    27  )
    28  
    29  // WithError creates a context where test failures are collected and stored in
    30  // the provided error instance when the caller is done. Use it like this:
    31  //
    32  //	func doSomething(tCtx ktesting.TContext) (finalErr error) {
    33  //	     tCtx, finalize := WithError(tCtx, &finalErr)
    34  //	     defer finalize()
    35  //	     ...
    36  //	     tCtx.Fatal("some failure")
    37  //
    38  // Any error already stored in the variable will get overwritten by finalize if
    39  // there were test failures, otherwise the variable is left unchanged.
    40  // If there were multiple test errors, then the error will wrap all of
    41  // them with errors.Join.
    42  //
    43  // Test failures are not propagated to the parent context.
    44  func WithError(tCtx TContext, err *error) (TContext, func()) {
    45  	eCtx := &errorContext{
    46  		TContext: tCtx,
    47  	}
    48  
    49  	return eCtx, func() {
    50  		// Recover has to be called in the deferred function. When called inside
    51  		// a function called by a deferred function (like finalize below), it
    52  		// returns nil.
    53  		if e := recover(); e != nil {
    54  			if _, ok := e.(fatalWithError); !ok {
    55  				// Not our own panic, pass it on instead of setting the error.
    56  				panic(e)
    57  			}
    58  		}
    59  
    60  		eCtx.finalize(err)
    61  	}
    62  }
    63  
    64  type errorContext struct {
    65  	TContext
    66  
    67  	mutex  sync.Mutex
    68  	errors []error
    69  	failed bool
    70  }
    71  
    72  func (eCtx *errorContext) finalize(err *error) {
    73  	eCtx.mutex.Lock()
    74  	defer eCtx.mutex.Unlock()
    75  
    76  	if !eCtx.failed {
    77  		return
    78  	}
    79  
    80  	errs := eCtx.errors
    81  	if len(errs) == 0 {
    82  		errs = []error{errFailedWithNoExplanation}
    83  	}
    84  	*err = errors.Join(errs...)
    85  }
    86  
    87  func (eCtx *errorContext) Error(args ...any) {
    88  	eCtx.mutex.Lock()
    89  	defer eCtx.mutex.Unlock()
    90  
    91  	// Gomega adds a leading newline in https://github.com/onsi/gomega/blob/f804ac6ada8d36164ecae0513295de8affce1245/internal/gomega.go#L37
    92  	// Let's strip that at start and end because ktesting will make errors
    93  	// stand out more with the "ERROR" prefix, so there's no need for additional
    94  	// line breaks.
    95  	eCtx.errors = append(eCtx.errors, errors.New(strings.TrimSpace(fmt.Sprintln(args...))))
    96  	eCtx.failed = true
    97  }
    98  
    99  func (eCtx *errorContext) Errorf(format string, args ...any) {
   100  	eCtx.mutex.Lock()
   101  	defer eCtx.mutex.Unlock()
   102  
   103  	eCtx.errors = append(eCtx.errors, errors.New(strings.TrimSpace(fmt.Sprintf(format, args...))))
   104  	eCtx.failed = true
   105  }
   106  
   107  func (eCtx *errorContext) Fail() {
   108  	eCtx.mutex.Lock()
   109  	defer eCtx.mutex.Unlock()
   110  
   111  	eCtx.failed = true
   112  }
   113  
   114  func (eCtx *errorContext) FailNow() {
   115  	eCtx.Helper()
   116  	eCtx.Fail()
   117  	panic(failed)
   118  }
   119  
   120  func (eCtx *errorContext) Failed() bool {
   121  	eCtx.mutex.Lock()
   122  	defer eCtx.mutex.Unlock()
   123  
   124  	return eCtx.failed
   125  }
   126  
   127  func (eCtx *errorContext) Fatal(args ...any) {
   128  	eCtx.Error(args...)
   129  	eCtx.FailNow()
   130  }
   131  
   132  func (eCtx *errorContext) Fatalf(format string, args ...any) {
   133  	eCtx.Errorf(format, args...)
   134  	eCtx.FailNow()
   135  }
   136  
   137  func (eCtx *errorContext) CleanupCtx(cb func(TContext)) {
   138  	eCtx.Helper()
   139  	cleanupCtx(eCtx, cb)
   140  }
   141  
   142  func (eCtx *errorContext) Expect(actual interface{}, extra ...interface{}) gomega.Assertion {
   143  	eCtx.Helper()
   144  	return expect(eCtx, actual, extra...)
   145  }
   146  
   147  func (eCtx *errorContext) ExpectNoError(err error, explain ...interface{}) {
   148  	eCtx.Helper()
   149  	expectNoError(eCtx, err, explain...)
   150  }
   151  
   152  func (eCtx *errorContext) Logger() klog.Logger {
   153  	return klog.FromContext(eCtx)
   154  }
   155  
   156  // fatalWithError is the internal type that should never get propagated up. The
   157  // only case where that can happen is when the developer forgot to call
   158  // finalize via defer. The string explains that, in case that developers get to
   159  // see it.
   160  type fatalWithError string
   161  
   162  const failed = fatalWithError("WithError TContext encountered a fatal error, but the finalize function was not called via defer as it should have been.")
   163  
   164  var errFailedWithNoExplanation = errors.New("WithError context was marked as failed without recording an error")
   165  

View as plain text