From c4c9a9d4de4cca32126c269912d149ef0a6b53c6 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 26 Feb 2026 15:32:43 +0100 Subject: [PATCH] ktesting: remove type alias The type alias made `go doc ./test/utils/ktesting.TContext` useless and was a weird workaround for preserving the original interface type name. Passing a TContext instance by value (almost) preserves the original API and is acceptable because the struct is still small. The only consumers which need to be updated are those which relied on passing nil as tCtx. If we ever find that TContext is or becomes too large, then we can make it a wrapper around some pointer. --- test/e2e/dra/utils/deploy.go | 7 +- test/utils/ktesting/assert.go | 74 +++++++------- test/utils/ktesting/clientcontext.go | 34 +++---- test/utils/ktesting/errorcontext.go | 141 +++++++++++++------------- test/utils/ktesting/stepcontext.go | 23 ++--- test/utils/ktesting/tcontext.go | 146 ++++++++++++--------------- test/utils/ktesting/withcontext.go | 42 ++++---- 7 files changed, 220 insertions(+), 247 deletions(-) diff --git a/test/e2e/dra/utils/deploy.go b/test/e2e/dra/utils/deploy.go index 6cc95949992..c44f2101b38 100644 --- a/test/e2e/dra/utils/deploy.go +++ b/test/e2e/dra/utils/deploy.go @@ -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 } diff --git a/test/utils/ktesting/assert.go b/test/utils/ktesting/assert.go index 2f6bbca8a4e..02b35aa4556 100644 --- a/test/utils/ktesting/assert.go +++ b/test/utils/ktesting/assert.go @@ -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 ( diff --git a/test/utils/ktesting/clientcontext.go b/test/utils/ktesting/clientcontext.go index e8555125caf..713821cb322 100644 --- a/test/utils/ktesting/clientcontext.go +++ b/test/utils/ktesting/clientcontext.go @@ -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 } diff --git a/test/utils/ktesting/errorcontext.go b/test/utils/ktesting/errorcontext.go index d8fd8a340b0..0316b548092 100644 --- a/test/utils/ktesting/errorcontext.go +++ b/test/utils/ktesting/errorcontext.go @@ -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:" -> use both prefix and suffix when we have a header, otherwise just the 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 diff --git a/test/utils/ktesting/stepcontext.go b/test/utils/ktesting/stepcontext.go index 34ed5e57b6a..13db3c79a73 100644 --- a/test/utils/ktesting/stepcontext.go +++ b/test/utils/ktesting/stepcontext.go @@ -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 { diff --git a/test/utils/ktesting/tcontext.go b/test/utils/ktesting/tcontext.go index db9ffbe9718..c9002527190 100644 --- a/test/utils/ktesting/tcontext.go +++ b/test/utils/ktesting/tcontext.go @@ -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 } diff --git a/test/utils/ktesting/withcontext.go b/test/utils/ktesting/withcontext.go index 5a87aad48ca..7a192748fd7 100644 --- a/test/utils/ktesting/withcontext.go +++ b/test/utils/ktesting/withcontext.go @@ -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 }