mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	This is useful in Go unit tests because it directly replaces the corresponding testing.T/B.Run.
		
			
				
	
	
		
			169 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			169 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2024 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package ktesting
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
	"sync"
 | 
						|
 | 
						|
	"github.com/onsi/gomega"
 | 
						|
	"k8s.io/klog/v2"
 | 
						|
)
 | 
						|
 | 
						|
// WithError creates a context where test failures are collected and stored in
 | 
						|
// the provided error instance when the caller is done. Use it like this:
 | 
						|
//
 | 
						|
//	func doSomething(tCtx ktesting.TContext) (finalErr error) {
 | 
						|
//	     tCtx, finalize := WithError(tCtx, &finalErr)
 | 
						|
//	     defer finalize()
 | 
						|
//	     ...
 | 
						|
//	     tCtx.Fatal("some failure")
 | 
						|
//
 | 
						|
// Any error already stored in the variable will get overwritten by finalize if
 | 
						|
// there were test failures, otherwise the variable is left unchanged.
 | 
						|
// If there were multiple test errors, then the error will wrap all of
 | 
						|
// them with errors.Join.
 | 
						|
//
 | 
						|
// Test failures are not propagated to the parent context.
 | 
						|
func WithError(tCtx TContext, err *error) (TContext, func()) {
 | 
						|
	eCtx := &errorContext{
 | 
						|
		TContext: tCtx,
 | 
						|
	}
 | 
						|
 | 
						|
	return eCtx, func() {
 | 
						|
		// Recover has to be called in the deferred function. When called inside
 | 
						|
		// a function called by a deferred function (like finalize below), it
 | 
						|
		// returns nil.
 | 
						|
		if e := recover(); e != nil {
 | 
						|
			if _, ok := e.(fatalWithError); !ok {
 | 
						|
				// Not our own panic, pass it on instead of setting the error.
 | 
						|
				panic(e)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		eCtx.finalize(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type errorContext struct {
 | 
						|
	TContext
 | 
						|
 | 
						|
	mutex  sync.Mutex
 | 
						|
	errors []error
 | 
						|
	failed bool
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) finalize(err *error) {
 | 
						|
	eCtx.mutex.Lock()
 | 
						|
	defer eCtx.mutex.Unlock()
 | 
						|
 | 
						|
	if !eCtx.failed {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	errs := eCtx.errors
 | 
						|
	if len(errs) == 0 {
 | 
						|
		errs = []error{errFailedWithNoExplanation}
 | 
						|
	}
 | 
						|
	*err = errors.Join(errs...)
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Error(args ...any) {
 | 
						|
	eCtx.mutex.Lock()
 | 
						|
	defer eCtx.mutex.Unlock()
 | 
						|
 | 
						|
	// Gomega adds a leading newline in https://github.com/onsi/gomega/blob/f804ac6ada8d36164ecae0513295de8affce1245/internal/gomega.go#L37
 | 
						|
	// Let's strip that at start and end because ktesting will make errors
 | 
						|
	// stand out more with the "ERROR" prefix, so there's no need for additional
 | 
						|
	// line breaks.
 | 
						|
	eCtx.errors = append(eCtx.errors, errors.New(strings.TrimSpace(fmt.Sprintln(args...))))
 | 
						|
	eCtx.failed = true
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Errorf(format string, args ...any) {
 | 
						|
	eCtx.mutex.Lock()
 | 
						|
	defer eCtx.mutex.Unlock()
 | 
						|
 | 
						|
	eCtx.errors = append(eCtx.errors, errors.New(strings.TrimSpace(fmt.Sprintf(format, args...))))
 | 
						|
	eCtx.failed = true
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Fail() {
 | 
						|
	eCtx.mutex.Lock()
 | 
						|
	defer eCtx.mutex.Unlock()
 | 
						|
 | 
						|
	eCtx.failed = true
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) FailNow() {
 | 
						|
	eCtx.Helper()
 | 
						|
	eCtx.Fail()
 | 
						|
	panic(failed)
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Failed() bool {
 | 
						|
	eCtx.mutex.Lock()
 | 
						|
	defer eCtx.mutex.Unlock()
 | 
						|
 | 
						|
	return eCtx.failed
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Fatal(args ...any) {
 | 
						|
	eCtx.Error(args...)
 | 
						|
	eCtx.FailNow()
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Fatalf(format string, args ...any) {
 | 
						|
	eCtx.Errorf(format, args...)
 | 
						|
	eCtx.FailNow()
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) CleanupCtx(cb func(TContext)) {
 | 
						|
	eCtx.Helper()
 | 
						|
	cleanupCtx(eCtx, cb)
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Expect(actual interface{}, extra ...interface{}) gomega.Assertion {
 | 
						|
	eCtx.Helper()
 | 
						|
	return expect(eCtx, actual, extra...)
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) ExpectNoError(err error, explain ...interface{}) {
 | 
						|
	eCtx.Helper()
 | 
						|
	expectNoError(eCtx, err, explain...)
 | 
						|
}
 | 
						|
 | 
						|
func (cCtx *errorContext) Run(name string, cb func(tCtx TContext)) bool {
 | 
						|
	return run(cCtx, name, cb)
 | 
						|
}
 | 
						|
 | 
						|
func (eCtx *errorContext) Logger() klog.Logger {
 | 
						|
	return klog.FromContext(eCtx)
 | 
						|
}
 | 
						|
 | 
						|
// fatalWithError is the internal type that should never get propagated up. The
 | 
						|
// only case where that can happen is when the developer forgot to call
 | 
						|
// finalize via defer. The string explains that, in case that developers get to
 | 
						|
// see it.
 | 
						|
type fatalWithError string
 | 
						|
 | 
						|
const failed = fatalWithError("WithError TContext encountered a fatal error, but the finalize function was not called via defer as it should have been.")
 | 
						|
 | 
						|
var errFailedWithNoExplanation = errors.New("WithError context was marked as failed without recording an error")
 |