From f9e7b15c001fb5de702b9f7868f655a05543ff82 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Thu, 20 Feb 2025 14:41:06 +0100 Subject: [PATCH] ktesting: add Run This is useful in Go unit tests because it directly replaces the corresponding testing.T/B.Run. --- test/utils/ktesting/clientcontext.go | 4 +++ test/utils/ktesting/errorcontext.go | 4 +++ test/utils/ktesting/tcontext.go | 37 ++++++++++++++++++++++++++++ test/utils/ktesting/tcontext_test.go | 27 ++++++++++++++++++++ test/utils/ktesting/withcontext.go | 4 +++ 5 files changed, 76 insertions(+) diff --git a/test/utils/ktesting/clientcontext.go b/test/utils/ktesting/clientcontext.go index d07cbccf14a..36da9793bdd 100644 --- a/test/utils/ktesting/clientcontext.go +++ b/test/utils/ktesting/clientcontext.go @@ -86,6 +86,10 @@ func (cCtx clientContext) ExpectNoError(err error, explain ...interface{}) { expectNoError(cCtx, err, explain...) } +func (cCtx clientContext) Run(name string, cb func(tCtx TContext)) bool { + return run(cCtx, name, cb) +} + func (cCtx clientContext) Logger() klog.Logger { return klog.FromContext(cCtx) } diff --git a/test/utils/ktesting/errorcontext.go b/test/utils/ktesting/errorcontext.go index 318e8070a13..bb1214e4329 100644 --- a/test/utils/ktesting/errorcontext.go +++ b/test/utils/ktesting/errorcontext.go @@ -149,6 +149,10 @@ func (eCtx *errorContext) ExpectNoError(err error, explain ...interface{}) { expectNoError(eCtx, err, explain...) } +func (cCtx *errorContext) Run(name string, cb func(tCtx TContext)) bool { + return run(cCtx, name, cb) +} + func (eCtx *errorContext) Logger() klog.Logger { return klog.FromContext(eCtx) } diff --git a/test/utils/ktesting/tcontext.go b/test/utils/ktesting/tcontext.go index 1d3de80e21c..67b94500a82 100644 --- a/test/utils/ktesting/tcontext.go +++ b/test/utils/ktesting/tcontext.go @@ -21,6 +21,7 @@ import ( "flag" "fmt" "strings" + "testing" "time" "github.com/onsi/gomega" @@ -84,6 +85,13 @@ type TContext interface { // a single test never run in parallel with each other. Parallel() + // Run runs f as a subtest of t called name. It blocks until f returns or + // calls t.Parallel to become a parallel test. + // + // Only supported in Go unit tests or benchmarks. It fails the current + // test when called elsewhere. + Run(name string, f func(tCtx TContext)) bool + // Cancel can be invoked to cancel the context before the test is completed. // Tests which use the context to control goroutines and then wait for // termination of those goroutines must call Cancel to avoid a deadlock. @@ -174,6 +182,7 @@ type TContext interface { // - CleanupCtx // - Expect // - ExpectNoError + // - Run // - Logger // // Usually these methods would be stand-alone functions with a TContext @@ -337,6 +346,9 @@ func InitCtx(ctx context.Context, tb TB, _ ...InitOption) TContext { // }) // // WithTB sets up cancellation for the sub-test. +// +// A simpler API is to use TContext.Run as replacement +// for [testing.T.Run]. func WithTB(parentCtx TContext, tb TB) TContext { tCtx := InitCtx(parentCtx, tb) tCtx = WithCancel(tCtx) @@ -350,6 +362,27 @@ func WithTB(parentCtx TContext, tb TB) TContext { return tCtx } +// run implements the different Run methods. It's not an exported +// method because tCtx.Run is more discoverable (same usage as +// with normal Go). +func run(tCtx TContext, name string, cb func(tCtx TContext)) bool { + tCtx.Helper() + switch tb := tCtx.TB().(type) { + case interface { + Run(string, func(t *testing.T)) bool + }: + return tb.Run(name, func(t *testing.T) { cb(WithTB(tCtx, t)) }) + case interface { + Run(string, func(t *testing.B)) bool + }: + return tb.Run(name, func(b *testing.B) { cb(WithTB(tCtx, b)) }) + default: + tCtx.Fatalf("Run not implemented, underlying %T does not support it", tCtx.TB()) + } + + return false +} + // WithContext constructs a new TContext with a different Context instance. // This can be used in callbacks which receive a Context, for example // from Gomega: @@ -439,6 +472,10 @@ func cleanupCtx(tCtx TContext, cb func(TContext)) { }) } +func (cCtx tContext) Run(name string, cb func(tCtx TContext)) bool { + return run(cCtx, name, cb) +} + func (tCtx tContext) Logger() klog.Logger { return klog.FromContext(tCtx) } diff --git a/test/utils/ktesting/tcontext_test.go b/test/utils/ktesting/tcontext_test.go index 79605d8388b..d158b9f5336 100644 --- a/test/utils/ktesting/tcontext_test.go +++ b/test/utils/ktesting/tcontext_test.go @@ -123,6 +123,33 @@ func TestWithTB(t *testing.T) { assert.Equal(t, apiextensions, tCtx.APIExtensions(), "APIExtensions") tCtx.Cancel("test is complete") + <-tCtx.Done() + }) + + if err := tCtx.Err(); err != nil { + t.Errorf("parent TContext should not have been cancelled: %v", err) + } +} + +func TestRun(t *testing.T) { + tCtx := ktesting.Init(t) + + cfg := new(rest.Config) + mapper := new(restmapper.DeferredDiscoveryRESTMapper) + client := clientset.New(nil) + dynamic := dynamic.New(nil) + apiextensions := apiextensions.New(nil) + tCtx = ktesting.WithClients(tCtx, cfg, mapper, client, dynamic, apiextensions) + + tCtx.Run("sub", func(tCtx ktesting.TContext) { + assert.Equal(t, cfg, tCtx.RESTConfig(), "RESTConfig") + assert.Equal(t, mapper, tCtx.RESTMapper(), "RESTMapper") + assert.Equal(t, client, tCtx.Client(), "Client") + assert.Equal(t, dynamic, tCtx.Dynamic(), "Dynamic") + assert.Equal(t, apiextensions, tCtx.APIExtensions(), "APIExtensions") + + tCtx.Cancel("test is complete") + <-tCtx.Done() }) if err := tCtx.Err(); err != nil { diff --git a/test/utils/ktesting/withcontext.go b/test/utils/ktesting/withcontext.go index db4bddb160b..6aa9b2d456b 100644 --- a/test/utils/ktesting/withcontext.go +++ b/test/utils/ktesting/withcontext.go @@ -102,6 +102,10 @@ func (wCtx withContext) ExpectNoError(err error, explain ...interface{}) { expectNoError(wCtx, err, explain...) } +func (cCtx withContext) Run(name string, cb func(tCtx TContext)) bool { + return run(cCtx, name, cb) +} + func (wCtx withContext) Logger() klog.Logger { return klog.FromContext(wCtx) }