Merge pull request #137268 from pohly/ktesting-tcontext-type

ktesting: remove type alias
This commit is contained in:
Kubernetes Prow Robot 2026-02-27 08:23:54 +05:30 committed by GitHub
commit 4e894632b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 220 additions and 247 deletions

View file

@ -284,7 +284,7 @@ type driverResourcesMutatorFunc func(map[string]resourceslice.DriverResources)
//
// Call this outside of ginkgo.It, then use the instance inside ginkgo.It.
func NewDriver(f *framework.Framework, nodes *Nodes, driverResourcesGenerator driverResourcesGenFunc, driverResourcesMutators ...driverResourcesMutatorFunc) *Driver {
d := NewDriverInstance(nil)
d := NewDriverInstance(ktesting.TContext{} /* no namespace yet, will be set later */)
ginkgo.BeforeEach(func() {
tCtx := f.TContext(context.Background())
@ -300,6 +300,7 @@ func NewDriver(f *framework.Framework, nodes *Nodes, driverResourcesGenerator dr
// NewDriverInstance is a variant of NewDriver where the driver is inactive and must
// be started explicitly with Run. May be used inside ginkgo.It or a Go unit test.
// The context is used to determine the test's and thus the driver's namespace.
func NewDriverInstance(tCtx ktesting.TContext) *Driver {
d := &Driver{
fail: map[MethodInstance]bool{},
@ -314,9 +315,7 @@ func NewDriverInstance(tCtx ktesting.TContext) *Driver {
WithRealNodes: true,
ExpectResourceSliceRemoval: true,
}
if tCtx != nil {
d.initName(tCtx)
}
d.initName(tCtx)
return d
}

View file

@ -58,10 +58,10 @@ func (f FailureError) Is(target error) bool {
// }
var ErrFailure error = FailureError{}
func gomegaAssertion(tc *TC, fatal bool, actual interface{}, extra ...interface{}) gomega.Assertion {
testingT := gtypes.GomegaTestingT(tc)
func gomegaAssertion(tCtx TContext, fatal bool, actual interface{}, extra ...interface{}) gomega.Assertion {
testingT := gtypes.GomegaTestingT(tCtx)
if !fatal {
testingT = assertTestingT{tc}
testingT = assertTestingT{tCtx}
}
return gomega.NewWithT(testingT).Expect(actual, extra...)
}
@ -70,7 +70,7 @@ func gomegaAssertion(tc *TC, fatal bool, actual interface{}, extra ...interface{
// reporting failures) using TContext.Errorf, i.e. testing continues after a
// failed assertion. The Helper method gets passed through.
type assertTestingT struct {
*TC
TContext
}
var _ gtypes.GomegaTestingT = assertTestingT{}
@ -104,34 +104,34 @@ func (a assertTestingT) Fatalf(format string, args ...any) {
//
// tCtx.ExpectNoError(somehelper.CreateSomething(tCtx, ...), "creating the first foobar")
// tCtx.ExpectNoError(somehelper.CreateSomething(tCtx, ...), "creating the second foobar")
func (tc *TC) ExpectNoError(err error, explain ...interface{}) {
tc.Helper()
tc.noError(tc.Fatalf, err, explain...)
func (tCtx TContext) ExpectNoError(err error, explain ...interface{}) {
tCtx.Helper()
tCtx.noError(tCtx.Fatalf, err, explain...)
}
// AssertNoError is a variant of ExpectNoError which reports an unexpected
// error without aborting the test. It returns true if there was no error.
func (tc *TC) AssertNoError(err error, explain ...interface{}) bool {
tc.Helper()
return tc.noError(tc.Errorf, err, explain...)
func (tCtx TContext) AssertNoError(err error, explain ...interface{}) bool {
tCtx.Helper()
return tCtx.noError(tCtx.Errorf, err, explain...)
}
func (tc *TC) noError(failf func(format string, args ...any), err error, explain ...interface{}) bool {
func (tCtx TContext) noError(failf func(format string, args ...any), err error, explain ...interface{}) bool {
if err == nil {
return true
}
tc.Helper()
tCtx.Helper()
description := buildDescription(explain...)
if errors.Is(err, ErrFailure) {
var failure FailureError
if tc.capture == nil && errors.As(err, &failure) {
if tCtx.capture == nil && errors.As(err, &failure) {
if backtrace := failure.Backtrace(); backtrace != "" {
if description != "" {
tc.Log(description)
tCtx.Log(description)
}
tc.Logf("Failed at:\n%s", backtrace)
tCtx.Logf("Failed at:\n%s", backtrace)
}
}
if description != "" {
@ -145,8 +145,8 @@ func (tc *TC) noError(failf func(format string, args ...any), err error, explain
if description == "" {
description = "Unexpected error"
}
if tc.capture == nil {
tc.Logf("%s:\n%s", description, format.Object(err, 0))
if tCtx.capture == nil {
tCtx.Logf("%s:\n%s", description, format.Object(err, 0))
}
failf("%s: %v", description, err.Error())
return false
@ -233,39 +233,39 @@ func buildDescription(explain ...interface{}) string {
// return value
// }
// tCtx.Eventually(cb).Should(gomega.Equal(42), "should be the answer to everything")
func (tc *TC) Eventually(arg any) gomega.AsyncAssertion {
tc.Helper()
return tc.newAsyncAssertion(gomega.NewWithT(tc).Eventually, arg)
func (tCtx TContext) Eventually(arg any) gomega.AsyncAssertion {
tCtx.Helper()
return tCtx.newAsyncAssertion(gomega.NewWithT(tCtx).Eventually, arg)
}
// AssertEventually is a variant of Eventually which merely records a failure
// without stopping the test.
func (tc *TC) AssertEventually(arg any) gomega.AsyncAssertion {
tc.Helper()
return tc.newAsyncAssertion(gomega.NewWithT(assertTestingT{tc}).Eventually, arg)
func (tCtx TContext) AssertEventually(arg any) gomega.AsyncAssertion {
tCtx.Helper()
return tCtx.newAsyncAssertion(gomega.NewWithT(assertTestingT{tCtx}).Eventually, arg)
}
// Consistently wraps [gomega.Consistently] the same way as [Eventually] wraps
// [gomega.Eventually].
func (tc *TC) Consistently(arg any) gomega.AsyncAssertion {
tc.Helper()
return tc.newAsyncAssertion(gomega.NewWithT(tc).Consistently, arg)
func (tCtx TContext) Consistently(arg any) gomega.AsyncAssertion {
tCtx.Helper()
return tCtx.newAsyncAssertion(gomega.NewWithT(tCtx).Consistently, arg)
}
// AssertConsistently is a variant of Consistently which merely records a failure
// without stopping the test.
func (tc *TC) AssertConsistently(arg any) gomega.AsyncAssertion {
tc.Helper()
return tc.newAsyncAssertion(gomega.NewWithT(assertTestingT{tc}).Consistently, arg)
func (tCtx TContext) AssertConsistently(arg any) gomega.AsyncAssertion {
tCtx.Helper()
return tCtx.newAsyncAssertion(gomega.NewWithT(assertTestingT{tCtx}).Consistently, arg)
}
func (tc *TC) newAsyncAssertion(eventuallyOrConsistently func(actualOrCtx any, args ...any) gomega.AsyncAssertion, arg any) gomega.AsyncAssertion {
tc.Helper()
func (tCtx TContext) newAsyncAssertion(eventuallyOrConsistently func(actualOrCtx any, args ...any) gomega.AsyncAssertion, arg any) gomega.AsyncAssertion {
tCtx.Helper()
// switch arg := arg.(type) {
// case func(tCtx TContext):
// // Tricky to handle via reflect, so let's cover this directly...
// return eventuallyOrConsistently(tc, func(g gomega.Gomega, ctx context.Context) (err error) {
// tCtx := WithContext(tc, ctx)
// return eventuallyOrConsistently(tCtx, func(g gomega.Gomega, ctx context.Context) (err error) {
// tCtx := WithContext(tCtx, ctx)
// tCtx, finalize := WithError(tCtx, &err)
// defer finalize()
// arg(tCtx)
@ -274,12 +274,12 @@ func (tc *TC) newAsyncAssertion(eventuallyOrConsistently func(actualOrCtx any, a
v := reflect.ValueOf(arg)
if v.Kind() != reflect.Func {
// Gomega must deal with it.
return eventuallyOrConsistently(tc, arg)
return eventuallyOrConsistently(tCtx, arg)
}
t := v.Type()
if t.NumIn() == 0 || t.In(0) != tContextType {
// Not a function we can wrap.
return eventuallyOrConsistently(tc, arg)
return eventuallyOrConsistently(tCtx, arg)
}
// Build a wrapper function with context instead of TContext as first parameter.
// The wrapper then builds that TContext when called and invokes the actual function.
@ -302,7 +302,7 @@ func (tc *TC) newAsyncAssertion(eventuallyOrConsistently func(actualOrCtx any, a
wrapperType := reflect.FuncOf(in, out, t.IsVariadic())
wrapper := reflect.MakeFunc(wrapperType, func(args []reflect.Value) (results []reflect.Value) {
var err error
tCtx, finalize := tc.WithContext(args[0].Interface().(context.Context)).
tCtx, finalize := tCtx.WithContext(args[0].Interface().(context.Context)).
WithCancel().
WithError(&err)
args[0] = reflect.ValueOf(tCtx)
@ -339,7 +339,7 @@ func (tc *TC) newAsyncAssertion(eventuallyOrConsistently func(actualOrCtx any, a
defer finalize() // Must be called directly, otherwise it cannot recover a panic.
return v.Call(args)
})
return eventuallyOrConsistently(tc, wrapper.Interface())
return eventuallyOrConsistently(tCtx, wrapper.Interface())
}
var (

View file

@ -29,27 +29,25 @@ import (
// WithRESTConfig initializes all client-go clients with new clients
// created for the config. The current test name gets included in the UserAgent.
func (tc *TC) WithRESTConfig(cfg *rest.Config) TContext {
func (tCtx TContext) WithRESTConfig(cfg *rest.Config) TContext {
cfg = rest.CopyConfig(cfg)
cfg.UserAgent = fmt.Sprintf("%s -- %s", rest.DefaultKubernetesUserAgent(), tc.Name())
cfg.UserAgent = fmt.Sprintf("%s -- %s", rest.DefaultKubernetesUserAgent(), tCtx.Name())
tc = tc.clone()
tc.restConfig = cfg
tc.client = clientset.NewForConfigOrDie(cfg)
tc.dynamic = dynamic.NewForConfigOrDie(cfg)
tc.apiextensions = apiextensions.NewForConfigOrDie(cfg)
cachedDiscovery := memory.NewMemCacheClient(tc.client.Discovery())
tc.restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery)
return tc
tCtx.restConfig = cfg
tCtx.client = clientset.NewForConfigOrDie(cfg)
tCtx.dynamic = dynamic.NewForConfigOrDie(cfg)
tCtx.apiextensions = apiextensions.NewForConfigOrDie(cfg)
cachedDiscovery := memory.NewMemCacheClient(tCtx.client.Discovery())
tCtx.restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscovery)
return tCtx
}
// WithClients uses an existing config and clients.
func (tc *TC) WithClients(cfg *rest.Config, mapper *restmapper.DeferredDiscoveryRESTMapper, client clientset.Interface, dynamic dynamic.Interface, apiextensions apiextensions.Interface) TContext {
tc = tc.clone()
tc.restConfig = cfg
tc.restMapper = mapper
tc.client = client
tc.dynamic = dynamic
tc.apiextensions = apiextensions
return tc
func (tCtx TContext) WithClients(cfg *rest.Config, mapper *restmapper.DeferredDiscoveryRESTMapper, client clientset.Interface, dynamic dynamic.Interface, apiextensions apiextensions.Interface) TContext {
tCtx.restConfig = cfg
tCtx.restMapper = mapper
tCtx.client = client
tCtx.dynamic = dynamic
tCtx.apiextensions = apiextensions
return tCtx
}

View file

@ -39,11 +39,10 @@ import (
// Test failures are not propagated to the parent context.
// WithRESTConfig initializes all client-go clients with new clients
// created for the config. The current test name gets included in the UserAgent.
func (tc *TC) WithError(err *error) (TContext, func()) {
tc = tc.clone()
tc.capture = &capture{}
func (tCtx TContext) WithError(err *error) (TContext, func()) {
tCtx.capture = &capture{}
return tc, func() {
return tCtx, 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.
@ -54,16 +53,16 @@ func (tc *TC) WithError(err *error) (TContext, func()) {
}
}
tc.finalize(err)
tCtx.finalize(err)
}
}
func (tc *TC) finalize(err *error) {
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
func (tCtx TContext) finalize(err *error) {
tCtx.capture.mutex.Lock()
defer tCtx.capture.mutex.Unlock()
errs := tc.capture.errors
if tc.capture.failed && len(errs) == 0 {
errs := tCtx.capture.errors
if tCtx.capture.failed && len(errs) == 0 {
errs = []error{errFailedWithNoExplanation}
}
if len(errs) == 0 {
@ -85,9 +84,9 @@ func (e failures) GomegaString() string {
// buildHeader handles:
// - "ERROR:<non-empty prefix><optional header><suffix>" -> use both prefix and suffix when we have a header, otherwise just the suffix
// - "<empty prefix><optional header><suffix>" -> use suffix only if we have a header
func (tc *TC) buildHeader(prefix, suffix string) string {
if tc.perTestHeader != nil {
return prefix + tc.perTestHeader() + suffix
func (tCtx TContext) buildHeader(prefix, suffix string) string {
if tCtx.perTestHeader != nil {
return prefix + tCtx.perTestHeader() + suffix
}
if prefix != "" {
return suffix
@ -104,45 +103,45 @@ func indent(msg string, all bool) string {
return header + strings.ReplaceAll(msg, "\n", "\n\t")
}
func (tc *TC) Skip(args ...any) {
tc.Helper()
func (tCtx TContext) Skip(args ...any) {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintln.
msg := strings.TrimSpace(fmt.Sprintln(args...))
tc.TB().Skip("SKIP:", tc.buildHeader(" ", " ")+tc.steps+indent(msg, false))
tCtx.TB().Skip("SKIP:", tCtx.buildHeader(" ", " ")+tCtx.steps+indent(msg, false))
}
func (tc *TC) Skipf(format string, args ...any) {
tc.Helper()
func (tCtx TContext) Skipf(format string, args ...any) {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintf.
msg := strings.TrimSpace(fmt.Sprintf(format, args...))
tc.TB().Skip("SKIP:", tc.buildHeader(" ", " ")+tc.steps+indent(msg, false))
tCtx.TB().Skip("SKIP:", tCtx.buildHeader(" ", " ")+tCtx.steps+indent(msg, false))
}
func (tc *TC) Log(args ...any) {
tc.Helper()
func (tCtx TContext) Log(args ...any) {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintln.
msg := strings.TrimSpace(fmt.Sprintln(args...))
tc.TB().Log(tc.buildHeader("", " ") + tc.steps + indent(msg, false))
tCtx.TB().Log(tCtx.buildHeader("", " ") + tCtx.steps + indent(msg, false))
}
func (tc *TC) Logf(format string, args ...any) {
tc.Helper()
func (tCtx TContext) Logf(format string, args ...any) {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintf.
msg := strings.TrimSpace(fmt.Sprintf(format, args...))
tc.TB().Log(tc.buildHeader("", " ") + tc.steps + indent(msg, false))
tCtx.TB().Log(tCtx.buildHeader("", " ") + tCtx.steps + indent(msg, false))
}
func (tc *TC) Error(args ...any) {
if tc.capture == nil {
tc.Helper()
func (tCtx TContext) Error(args ...any) {
if tCtx.capture == nil {
tCtx.Helper()
msg := strings.TrimSpace(fmt.Sprintln(args...))
// ERROR *before* header to make it stand out as failure.
tc.TB().Error("ERROR:" + tc.buildHeader(" ", "\n") + indent(tc.steps+msg, true))
tCtx.TB().Error("ERROR:" + tCtx.buildHeader(" ", "\n") + indent(tCtx.steps+msg, true))
return
}
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
tCtx.capture.mutex.Lock()
defer tCtx.capture.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
@ -150,89 +149,89 @@ func (tc *TC) Error(args ...any) {
// line breaks. Besides, Sprintln (required for `go vet printf`) also
// adds a trailing newline that we don't want.
msg := strings.TrimSpace(fmt.Sprintln(args...))
tc.capture.errors = append(tc.capture.errors, errors.New(tc.steps+msg))
tc.capture.failed = true
tCtx.capture.errors = append(tCtx.capture.errors, errors.New(tCtx.steps+msg))
tCtx.capture.failed = true
}
func (tc *TC) Errorf(format string, args ...any) {
if tc.capture == nil {
tc.Helper()
func (tCtx TContext) Errorf(format string, args ...any) {
if tCtx.capture == nil {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintln.
msg := strings.TrimSpace(fmt.Sprintf(format, args...))
// ERROR *before* header to make it stand out as failure.
tc.TB().Error("ERROR:" + tc.buildHeader(" ", "\n") + indent(tc.steps+msg, true))
tCtx.TB().Error("ERROR:" + tCtx.buildHeader(" ", "\n") + indent(tCtx.steps+msg, true))
return
}
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
tCtx.capture.mutex.Lock()
defer tCtx.capture.mutex.Unlock()
msg := strings.TrimSpace(fmt.Sprintf(format, args...))
tc.capture.errors = append(tc.capture.errors, errors.New(tc.steps+msg))
tc.capture.failed = true
tCtx.capture.errors = append(tCtx.capture.errors, errors.New(tCtx.steps+msg))
tCtx.capture.failed = true
}
func (tc *TC) Fail() {
if tc.capture == nil {
tc.TB().Fail()
func (tCtx TContext) Fail() {
if tCtx.capture == nil {
tCtx.TB().Fail()
return
}
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
tCtx.capture.mutex.Lock()
defer tCtx.capture.mutex.Unlock()
tc.capture.failed = true
tCtx.capture.failed = true
}
func (tc *TC) FailNow() {
if tc.capture == nil {
tc.TB().FailNow()
func (tCtx TContext) FailNow() {
if tCtx.capture == nil {
tCtx.TB().FailNow()
return
}
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
tCtx.capture.mutex.Lock()
defer tCtx.capture.mutex.Unlock()
tc.capture.failed = true
tCtx.capture.failed = true
panic(failed)
}
func (tc *TC) Failed() bool {
if tc.capture == nil {
return tc.TB().Failed()
func (tCtx TContext) Failed() bool {
if tCtx.capture == nil {
return tCtx.TB().Failed()
}
tc.capture.mutex.Lock()
defer tc.capture.mutex.Unlock()
tCtx.capture.mutex.Lock()
defer tCtx.capture.mutex.Unlock()
return tc.capture.failed
return tCtx.capture.failed
}
func (tc *TC) Fatal(args ...any) {
if tc.capture == nil {
tc.Helper()
func (tCtx TContext) Fatal(args ...any) {
if tCtx.capture == nil {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintln.
msg := strings.TrimSpace(fmt.Sprintln(args...))
// FATAL ERROR *before* header to make it stand out as failure.
tc.TB().Fatal("FATAL ERROR:" + tc.buildHeader(" ", "\n") + indent(tc.steps+msg, true))
tCtx.TB().Fatal("FATAL ERROR:" + tCtx.buildHeader(" ", "\n") + indent(tCtx.steps+msg, true))
}
tc.Error(args...)
tc.FailNow()
tCtx.Error(args...)
tCtx.FailNow()
}
func (tc *TC) Fatalf(format string, args ...any) {
if tc.capture == nil {
tc.Helper()
func (tCtx TContext) Fatalf(format string, args ...any) {
if tCtx.capture == nil {
tCtx.Helper()
// Enable `go vet printf` by directly calling fmt.Sprintf.
msg := strings.TrimSpace(fmt.Sprintf(format, args...))
// FATAL ERROR *before* header to make it stand out as failure.
tc.TB().Fatal("FATAL ERROR:" + tc.buildHeader(" ", "\n") + indent(tc.steps+msg, true))
tCtx.TB().Fatal("FATAL ERROR:" + tCtx.buildHeader(" ", "\n") + indent(tCtx.steps+msg, true))
return
}
tc.Errorf(format, args...)
tc.FailNow()
tCtx.Errorf(format, args...)
tCtx.FailNow()
}
// fatalWithError is the internal type that should never get propagated up. The

View file

@ -26,10 +26,9 @@ package ktesting
// The string should describe the operation that is about to happen ("starting
// the controller", "list items") or what is being operated on ("HTTP server").
// Multiple different prefixes get concatenated with a colon.
func (tc *TC) WithStep(step string) *TC {
tc = tc.clone()
tc.steps += step + ": "
return tc
func (tCtx TContext) WithStep(step string) TContext {
tCtx.steps += step + ": "
return tCtx
}
// Step is useful when the context with the step information is
@ -45,22 +44,22 @@ func (tc *TC) WithStep(step string) *TC {
// Inside the callback, the tCtx variable is the one where the step
// has been added. This avoids the need to introduce multiple different
// context variables and risk of using the wrong one.
func (tc *TC) Step(step string, cb func(tCtx TContext)) {
tc.Helper()
cb(tc.WithStep(step))
func (tCtx TContext) Step(step string, cb func(tCtx TContext)) {
tCtx.Helper()
cb(tCtx.WithStep(step))
}
// Value intercepts a search for the special "GINKGO_SPEC_CONTEXT" and
// wraps the underlying reporter so that the steps are visible in the report.
func (tc *TC) Value(key any) any {
if tc.steps != "" {
func (tCtx TContext) Value(key any) any {
if tCtx.steps != "" {
if s, ok := key.(string); ok && s == ginkgoSpecContextKey {
if reporter, ok := tc.Context.Value(key).(ginkgoReporter); ok {
return ginkgoReporter(&stepReporter{reporter: reporter, steps: tc.steps})
if reporter, ok := tCtx.Context.Value(key).(ginkgoReporter); ok {
return ginkgoReporter(&stepReporter{reporter: reporter, steps: tCtx.steps})
}
}
}
return tc.Context.Value(key)
return tCtx.Context.Value(key)
}
type stepReporter struct {

View file

@ -216,11 +216,10 @@ type InitOption = initoption.InitOption
// Functional options are part of the API, but currently
// there are none which have an effect.
func InitCtx(ctx context.Context, tb TB, _ ...InitOption) TContext {
tc := TC{
return TContext{
Context: ctx,
testingTB: testingTB{TB: tb},
}
return &tc
}
// withTB constructs a new TContext with a different TB instance.
@ -239,23 +238,21 @@ func InitCtx(ctx context.Context, tb TB, _ ...InitOption) TContext {
// })
//
// withTB sets up cancellation for the sub-test and uses per-test output.
func (tc *TC) withTB(tb TB) TContext {
tc = tc.clone()
tc.testingTB.TB = tb
if tc.perTestHeader != nil {
func (tCtx TContext) withTB(tb TB) TContext {
tCtx.testingTB.TB = tb
if tCtx.perTestHeader != nil {
logger := newLogger(tb, false /* don't buffer logs in sub-test */)
tc.Context = klog.NewContext(tc.Context, logger)
tCtx.Context = klog.NewContext(tCtx.Context, logger)
}
tc = tc.WithCancel()
return tc
return tCtx.WithCancel()
}
// run implements the different Run and SyncTest methods. It's not an exported
// method because tCtx.Run is more discoverable (same usage as
// with normal Go).
func run(tc *TC, name string, syncTest bool, cb func(tc *TC)) bool {
tc.Helper()
switch tb := tc.TB().(type) {
func run(tCtx TContext, name string, syncTest bool, cb func(tCtx TContext)) bool {
tCtx.Helper()
switch tb := tCtx.TB().(type) {
case *testing.T:
if syncTest {
f := func(t *testing.T) {
@ -265,10 +262,9 @@ func run(tc *TC, name string, syncTest bool, cb func(tc *TC)) bool {
//
// Sync tests shouldn't need the overall suite timeout,
// so this seems okay.
tc = tc.clone()
tc.isSyncTest = true
tc = tc.WithoutCancel().withTB(t)
cb(tc)
tCtx.isSyncTest = true
tCtx = tCtx.WithoutCancel().withTB(t)
cb(tCtx)
}
if name != "" {
return tb.Run(name, func(t *testing.T) { synctest.Test(t, f) })
@ -277,12 +273,12 @@ func run(tc *TC, name string, syncTest bool, cb func(tc *TC)) bool {
return true
}
return tb.Run(name, func(t *testing.T) {
cb(tc.withTB(t))
cb(tCtx.withTB(t))
})
case *testing.B:
if !syncTest {
return tb.Run(name, func(b *testing.B) {
cb(tc.withTB(b))
cb(tCtx.withTB(b))
})
}
}
@ -291,7 +287,7 @@ func run(tc *TC, name string, syncTest bool, cb func(tc *TC)) bool {
if syncTest {
what = "SyncTest"
}
tc.Fatalf("%s not implemented, underlying %T does not support it", what, tc.TB())
tCtx.Fatalf("%s not implemented, underlying %T does not support it", what, tCtx.TB())
return false
}
@ -306,28 +302,23 @@ func run(tc *TC, name string, syncTest bool, cb func(tc *TC)) bool {
//
// This is important because the Context in the callback could have
// a different deadline than in the parent TContext.
func (tc *TC) WithContext(ctx context.Context) TContext {
tc = tc.clone()
logger := tc.Logger()
tc.Context = ctx
func (tCtx TContext) WithContext(ctx context.Context) TContext {
logger := tCtx.Logger()
tCtx.Context = ctx
if _, err := logr.FromContext(ctx); err != nil {
// Keep using the logger from the parent context.
tc = tc.WithLogger(logger)
tCtx = tCtx.WithLogger(logger)
}
return tc
return tCtx
}
// WithValue wraps [context.WithValue] such that the result is again a TContext.
func (tc *TC) WithValue(key, val any) TContext {
ctx := context.WithValue(tc, key, val)
return tc.WithContext(ctx)
func (tCtx TContext) WithValue(key, val any) TContext {
ctx := context.WithValue(tCtx, key, val)
return tCtx.WithContext(ctx)
}
// TContext is the recommended type for storing a [TC] instance.
// The type alias is necessary because TContext used to be an interface.
type TContext = *TC
// TC implements [context.Context], [testing.TB] and some additional
// TContext implements [context.Context], [testing.TB] and some additional
// methods. [TContext] is the public pointer type for referencing a TC.
// Variables are usually called tCtx. To ensure that test code does not
// use `t` directly unintentionally, it is recommended to use two functions:
@ -355,7 +346,7 @@ type TContext = *TC
// Progress reporting is more informative when doing polling with
// [gomega.Eventually] and [gomega.Consistently]. Without that, it
// can only report which tests are active.
type TC struct {
type TContext struct {
// Context makes the methods of the underlying context
// available. It must not be modified.
context.Context
@ -402,12 +393,6 @@ type capture struct {
failed bool
}
// tc makes a shallow copy.
func (tc *TC) clone() *TC {
clone := *tc
return &clone
}
// testingTB is needed to avoid a name conflict
// between field and method in tContext.
type testingTB struct {
@ -417,8 +402,6 @@ type testingTB struct {
TB
}
var _ TContext = &TC{}
// Parallel signals that this test is to be run in parallel with (and
// only with) other parallel tests. In other words, it needs to be
// called in each test which is meant to run in parallel.
@ -427,11 +410,11 @@ var _ TContext = &TC{}
//
// When a unit test is run multiple times due to use of -test.count or -test.cpu,
// multiple instances of a single test never run in parallel with each other.
func (tc *TC) Parallel() {
if tb, ok := tc.TB().(interface{ Parallel() }); ok {
func (tCtx TContext) Parallel() {
if tb, ok := tCtx.TB().(interface{ Parallel() }); ok {
tb.Parallel()
} else {
tc.Fatalf("Parallel not implemented, underlying %T does not support it", tc.TB())
tCtx.Fatalf("Parallel not implemented, underlying %T does not support it", tCtx.TB())
}
}
@ -442,9 +425,9 @@ func (tc *TC) Parallel() {
// The cause, if non-empty, is turned into an error which is equivalent
// to context.Canceled. context.Cause will return that error for the
// context.
func (tc *TC) Cancel(cause string) {
if tc.cancel != nil {
tc.cancel(cause)
func (tCtx TContext) Cancel(cause string) {
if tCtx.cancel != nil {
tCtx.cancel(cause)
}
}
@ -464,24 +447,24 @@ func (tc *TC) Cancel(cause string) {
//
// The logger and clients are the same as in the TContext that CleanupCtx
// is invoked on.
func (tc *TC) CleanupCtx(cb func(TContext)) {
tc.Helper()
func (tCtx TContext) CleanupCtx(cb func(TContext)) {
tCtx.Helper()
if tb, ok := tc.TB().(ContextTB); ok {
if tb, ok := tCtx.TB().(ContextTB); ok {
// Use context from base TB (most likely Ginkgo).
tb.CleanupCtx(func(ctx context.Context) {
tCtx := tc.WithContext(ctx)
tCtx := tCtx.WithContext(ctx)
cb(tCtx)
})
return
}
tc.Cleanup(func() {
tCtx.Cleanup(func() {
// Use new context. This is the code path for "go test". The
// context then has *no* deadline. In the code path above for
// Ginkgo, Ginkgo is more sophisticated and also applies
// timeouts to cleanup calls which accept a context.
childCtx := tc.WithContext(context.WithoutCancel(tc))
childCtx := tCtx.WithContext(context.WithoutCancel(tCtx))
cb(childCtx)
})
}
@ -491,8 +474,8 @@ func (tc *TC) CleanupCtx(cb func(TContext)) {
//
// Only supported in Go unit tests or benchmarks. It fails the current
// test when called elsewhere.
func (tc *TC) Run(name string, cb func(tCtx TContext)) bool {
return run(tc, name, false, cb)
func (tCtx TContext) Run(name string, cb func(tCtx TContext)) bool {
return run(tCtx, name, false, cb)
}
// SyncTest uses [synctest.Test] to execute the callback inside a bubble.
@ -500,8 +483,8 @@ func (tc *TC) Run(name string, cb func(tCtx TContext)) bool {
// the bubble directly in the current test context.
//
// Only works in Go unit tests.
func (tc *TC) SyncTest(name string, cb func(tCtx TContext)) bool {
return run(tc, name, true, cb)
func (tCtx TContext) SyncTest(name string, cb func(tCtx TContext)) bool {
return run(tCtx, name, true, cb)
}
// IsSyncTest returns true if the context was created by SyncTest.
@ -513,8 +496,8 @@ func (tc *TC) SyncTest(name string, cb func(tCtx TContext)) bool {
// Eventually and Consistently both call Wait and then check
// the condition.
// - Outside, polling or some synchronization mechanism has to be used.
func (tc *TC) IsSyncTest() bool {
return tc.isSyncTest
func (tCtx TContext) IsSyncTest() bool {
return tCtx.isSyncTest
}
// Wait calls [synctest.Wait] and thus ensures that all background
@ -522,7 +505,7 @@ func (tc *TC) IsSyncTest() bool {
//
// Only works inside a bubble started by SyncTest (can be checked with
// IsSyncTest), panics elsewhere.
func (tc *TC) Wait() {
func (tCtx TContext) Wait() {
synctest.Wait()
}
@ -542,7 +525,7 @@ func (tc *TC) Wait() {
// ...
// }
// }
func (tc *TC) TB() TB { return tc.testingTB.TB }
func (tCtx TContext) TB() TB { return tCtx.testingTB.TB }
// Logger returns a logger for the current test. This is a shortcut
// for calling klog.FromContext.
@ -554,22 +537,22 @@ func (tc *TC) TB() TB { return tc.testingTB.TB }
//
// To skip intermediate helper functions during stack unwinding,
// TB.Helper can be called in those functions.
func (tc *TC) Logger() klog.Logger {
return klog.FromContext(tc.Context)
func (tCtx TContext) Logger() klog.Logger {
return klog.FromContext(tCtx.Context)
}
// RESTConfig returns a copy of the config for a rest client with the UserAgent
// set to include the current test name or nil if not available. Several typed
// clients using this config are available through [Client], [Dynamic],
// [APIExtensions].
func (tc *TC) RESTConfig() *rest.Config {
return rest.CopyConfig(tc.restConfig)
func (tCtx TContext) RESTConfig() *rest.Config {
return rest.CopyConfig(tCtx.restConfig)
}
func (tc *TC) RESTMapper() *restmapper.DeferredDiscoveryRESTMapper { return tc.restMapper }
func (tc *TC) Client() clientset.Interface { return tc.client }
func (tc *TC) Dynamic() dynamic.Interface { return tc.dynamic }
func (tc *TC) APIExtensions() apiextensions.Interface { return tc.apiextensions }
func (tCtx TContext) RESTMapper() *restmapper.DeferredDiscoveryRESTMapper { return tCtx.restMapper }
func (tCtx TContext) Client() clientset.Interface { return tCtx.client }
func (tCtx TContext) Dynamic() dynamic.Interface { return tCtx.dynamic }
func (tCtx TContext) APIExtensions() apiextensions.Interface { return tCtx.apiextensions }
// Expect wraps [gomega.Expect] such that a failure will be reported via
// [TContext.Fatal]. As with [gomega.Expect], additional values
@ -579,27 +562,26 @@ func (tc *TC) APIExtensions() apiextensions.Interface { return tc.a
//
// myAmazingThing := func(int, error) { ...}
// tCtx.Expect(myAmazingThing()).Should(gomega.Equal(1))
func (tc *TC) Expect(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tc, true, actual, extra...)
func (tCtx TContext) Expect(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tCtx, true, actual, extra...)
}
// Require is an alias for Expect.
func (tc *TC) Require(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tc, true, actual, extra...)
func (tCtx TContext) Require(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tCtx, true, actual, extra...)
}
// Assert also wraps [gomega.Expect], but in contrast to Expect = Require,
// it reports a failure through [TContext.Error]. This makes it possible
// to test several different assertions.
func (tc *TC) Assert(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tc, false, actual, extra...)
func (tCtx TContext) Assert(actual interface{}, extra ...interface{}) gomega.Assertion {
return gomegaAssertion(tCtx, false, actual, extra...)
}
// WithNamespace creates a new context with a Kubernetes namespace name for retrieval through [Namespace].
func (tc *TC) WithNamespace(namespace string) TContext {
tc = tc.clone()
tc.namespace = namespace
return tc
func (tCtx TContext) WithNamespace(namespace string) TContext {
tCtx.namespace = namespace
return tCtx
}
// Namespace returns the Kubernetes namespace name that was set previously
@ -608,6 +590,6 @@ func (tc *TC) WithNamespace(namespace string) TContext {
// This namespace is the one to be used by tests which need to create namespace-scoped
// objects. The name is guaranteed to be unique for the test context, so tests running
// in parallel need to be set up so that each test has its own namespace.
func (tc *TC) Namespace() string {
return tc.namespace
func (tCtx TContext) Namespace() string {
return tCtx.namespace
}

View file

@ -26,31 +26,29 @@ import (
// WithCancel sets up cancellation in a [TContext.Cleanup] callback and
// constructs a new TContext where [TContext.Cancel] cancels only the new
// context.
func (tc *TC) WithCancel() TContext {
ctx, cancel := context.WithCancelCause(tc)
func (tCtx TContext) WithCancel() TContext {
ctx, cancel := context.WithCancelCause(tCtx)
tc = tc.clone()
tc.Context = ctx
tc.cancel = func(cause string) {
tCtx.Context = ctx
tCtx.cancel = func(cause string) {
var cancelCause error
if cause != "" {
cancelCause = canceledError(cause)
}
cancel(cancelCause)
}
return tc
return tCtx
}
// WithoutCancel causes the returned context to ignore cancellation of its parent.
// Calling Cancel will not cancel the parent either.
// This matches [context.WithoutCancel].
func (tc *TC) WithoutCancel() TContext {
ctx := context.WithoutCancel(tc)
func (tCtx TContext) WithoutCancel() TContext {
ctx := context.WithoutCancel(tCtx)
tc = tc.clone()
tc.Context = ctx
tc.cancel = nil
return tc
tCtx.Context = ctx
tCtx.cancel = nil
return tCtx
}
// WithTimeout sets up new context with a timeout. Canceling the timeout gets
@ -58,20 +56,18 @@ func (tc *TC) WithoutCancel() TContext {
// the new context. The cause is used as reason why the context is canceled
// once the timeout is reached. It may be empty, in which case the usual
// "context canceled" error is used.
func (tc *TC) WithTimeout(timeout time.Duration, timeoutCause string) TContext {
ctx, cancel := withTimeout(tc, tc.TB(), timeout, timeoutCause)
func (tCtx TContext) WithTimeout(timeout time.Duration, timeoutCause string) TContext {
ctx, cancel := withTimeout(tCtx, tCtx.TB(), timeout, timeoutCause)
tc = tc.clone()
tc.Context = ctx
tc.cancel = cancel
return tc
tCtx.Context = ctx
tCtx.cancel = cancel
return tCtx
}
// WithLogger constructs a new context with a different logger.
func (tc *TC) WithLogger(logger klog.Logger) TContext {
ctx := klog.NewContext(tc, logger)
func (tCtx TContext) WithLogger(logger klog.Logger) TContext {
ctx := klog.NewContext(tCtx, logger)
tc = tc.clone()
tc.Context = ctx
return tc
tCtx.Context = ctx
return tCtx
}