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 }