istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/test.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package framework
    16  
    17  import (
    18  	context2 "context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	traceapi "go.opentelemetry.io/otel/trace"
    24  
    25  	"istio.io/istio/pkg/test/framework/label"
    26  	"istio.io/istio/pkg/test/framework/resource"
    27  	"istio.io/istio/pkg/test/scopes"
    28  	"istio.io/istio/pkg/tracing"
    29  )
    30  
    31  type Test interface {
    32  	// Label applies the given labels to this test.
    33  	Label(labels ...label.Instance) Test
    34  	// RequireIstioVersion ensures that all installed versions of Istio are at least the
    35  	// required version for the annotated test to pass
    36  	RequireIstioVersion(version string) Test
    37  	// RequireKubernetesMinorVersion ensures that all Kubernetes clusters used in this test
    38  	// are at least the required version for the annotated test to pass
    39  	RequireKubernetesMinorVersion(minorVersion uint) Test
    40  	// RequiresMinClusters ensures that the current environment contains at least the expected number of clusters.
    41  	// Otherwise it stops test execution and skips the test.
    42  	//
    43  	// Deprecated: Tests should not make assumptions about number of clusters.
    44  	RequiresMinClusters(minClusters int) Test
    45  	// RequiresSingleCluster this a utility that requires the min/max clusters to both = 1.
    46  	//
    47  	// Deprecated: All new tests should support multiple clusters.
    48  	RequiresSingleCluster() Test
    49  	// RequiresLocalControlPlane ensures that clusters are using locally-deployed control planes.
    50  	//
    51  	// Deprecated: Tests should not make assumptions regarding control plane topology.
    52  	RequiresLocalControlPlane() Test
    53  	// RequiresSingleNetwork ensures that clusters are in the same network
    54  	//
    55  	// Deprecated: Tests should not make assumptions regarding number of networks.
    56  	RequiresSingleNetwork() Test
    57  	// TopLevel marks a test as a "top-level test" meaning a container test that has many subtests.
    58  	// Resources created at this level will be in-scope for dumping when any descendant test fails.
    59  	TopLevel() Test
    60  	// Run the test, supplied as a lambda.
    61  	Run(fn func(t TestContext))
    62  	// RunParallel runs this test in parallel with other children of the same parent test/suite. Under the hood,
    63  	// this relies on Go's t.Parallel() and will, therefore, have the same behavior.
    64  	//
    65  	// A parallel test will run in parallel with siblings that share the same parent test. The parent test function
    66  	// will exit before the parallel children are executed. It should be noted that if the parent test is prevented
    67  	// from exiting (e.g. parent test is waiting for something to occur within the child test), the test will
    68  	// deadlock.
    69  	//
    70  	// Example:
    71  	//
    72  	// func TestParallel(t *testing.T) {
    73  	//     framework.NewTest(t).
    74  	//         Run(func(ctx framework.TestContext) {
    75  	//             ctx.NewSubTest("T1").
    76  	//                 Run(func(ctx framework.TestContext) {
    77  	//                     ctx.NewSubTest("T1a").
    78  	//                         RunParallel(func(ctx framework.TestContext) {
    79  	//                             // Run in parallel with T1b
    80  	//                         })
    81  	//                     ctx.NewSubTest("T1b").
    82  	//                         RunParallel(func(ctx framework.TestContext) {
    83  	//                             // Run in parallel with T1a
    84  	//                         })
    85  	//                     // Exits before T1a and T1b are run.
    86  	//                 })
    87  	//
    88  	//             ctx.NewSubTest("T2").
    89  	//                 Run(func(ctx framework.TestContext) {
    90  	//                     ctx.NewSubTest("T2a").
    91  	//                         RunParallel(func(ctx framework.TestContext) {
    92  	//                             // Run in parallel with T2b
    93  	//                         })
    94  	//                     ctx.NewSubTest("T2b").
    95  	//                         RunParallel(func(ctx framework.TestContext) {
    96  	//                             // Run in parallel with T2a
    97  	//                         })
    98  	//                     // Exits before T2a and T2b are run.
    99  	//                 })
   100  	//         })
   101  	// }
   102  	//
   103  	// In the example above, non-parallel parents T1 and T2 contain parallel children T1a, T1b, T2a, T2b.
   104  	//
   105  	// Since both T1 and T2 are non-parallel, they are run synchronously: T1 followed by T2. After T1 exits,
   106  	// T1a and T1b are run asynchronously with each other. After T1a and T1b complete, T2 is then run in the
   107  	// same way: T2 exits, then T2a and T2b are run asynchronously to completion.
   108  	RunParallel(fn func(t TestContext))
   109  }
   110  
   111  // Test allows the test author to specify test-related metadata in a fluent-style, before commencing execution.
   112  type testImpl struct {
   113  	// name to be used when creating a Golang test. Only used for subtests.
   114  	name                      string
   115  	parent                    *testImpl
   116  	goTest                    *testing.T
   117  	labels                    []label.Instance
   118  	s                         *suiteContext
   119  	requiredMinClusters       int
   120  	requiredMaxClusters       int
   121  	requireLocalIstiod        bool
   122  	requireSingleNetwork      bool
   123  	minIstioVersion           string
   124  	minKubernetesMinorVersion uint
   125  	topLevel                  bool
   126  
   127  	ctx *testContext
   128  	tc  context2.Context
   129  	ts  traceapi.Span
   130  }
   131  
   132  // NewTest returns a new test wrapper for running a single test.
   133  func NewTest(t *testing.T) Test {
   134  	rtMu.Lock()
   135  	defer rtMu.Unlock()
   136  
   137  	if rt == nil {
   138  		panic("call to scope without running the test framework")
   139  	}
   140  
   141  	ctx, span := tracing.Start(rt.suiteContext().traceContext, t.Name())
   142  
   143  	runner := &testImpl{
   144  		tc:     ctx,
   145  		ts:     span,
   146  		s:      rt.suiteContext(),
   147  		goTest: t,
   148  	}
   149  
   150  	return runner
   151  }
   152  
   153  func (t *testImpl) Label(labels ...label.Instance) Test {
   154  	t.labels = append(t.labels, labels...)
   155  	return t
   156  }
   157  
   158  func (t *testImpl) TopLevel() Test {
   159  	t.topLevel = true
   160  	return t
   161  }
   162  
   163  func (t *testImpl) RequiresMinClusters(minClusters int) Test {
   164  	t.requiredMinClusters = minClusters
   165  	return t
   166  }
   167  
   168  func (t *testImpl) RequiresSingleCluster() Test {
   169  	t.requiredMaxClusters = 1
   170  	// nolint: staticcheck
   171  	return t.RequiresMinClusters(1)
   172  }
   173  
   174  func (t *testImpl) RequiresLocalControlPlane() Test {
   175  	t.requireLocalIstiod = true
   176  	return t
   177  }
   178  
   179  func (t *testImpl) RequiresSingleNetwork() Test {
   180  	t.requireSingleNetwork = true
   181  	return t
   182  }
   183  
   184  func (t *testImpl) RequireIstioVersion(version string) Test {
   185  	t.minIstioVersion = version
   186  	return t
   187  }
   188  
   189  func (t *testImpl) RequireKubernetesMinorVersion(minorVersion uint) Test {
   190  	t.minKubernetesMinorVersion = minorVersion
   191  	return t
   192  }
   193  
   194  func (t *testImpl) Run(fn func(ctx TestContext)) {
   195  	t.runInternal(fn, false)
   196  }
   197  
   198  func (t *testImpl) RunParallel(fn func(ctx TestContext)) {
   199  	t.runInternal(fn, true)
   200  }
   201  
   202  func (t *testImpl) runInternal(fn func(ctx TestContext), parallel bool) {
   203  	// Disallow running the same test more than once.
   204  	if t.ctx != nil {
   205  		testName := t.name
   206  		if testName == "" && t.goTest != nil {
   207  			testName = t.goTest.Name()
   208  		}
   209  		panic(fmt.Sprintf("Attempting to run test `%s` more than once", testName))
   210  	}
   211  
   212  	if t.s.skipped {
   213  		t.goTest.Skip("Skipped because parent Suite was skipped.")
   214  		return
   215  	}
   216  
   217  	if t.parent != nil {
   218  		// Create a new subtest under the parent's test.
   219  		parentGoTest := t.parent.goTest
   220  		parentCtx := t.parent.ctx
   221  		parentGoTest.Run(t.name, func(goTest *testing.T) {
   222  			t.goTest = goTest
   223  			t.doRun(parentCtx.newChildContext(t), fn, parallel)
   224  		})
   225  	} else {
   226  		// Not a child context. Running with the test provided during construction.
   227  		t.doRun(newRootContext(t, t.goTest, t.labels...), fn, parallel)
   228  	}
   229  }
   230  
   231  func (t *testImpl) doRun(ctx *testContext, fn func(ctx TestContext), parallel bool) {
   232  	if fn == nil {
   233  		panic("attempting to run test with nil function")
   234  	}
   235  
   236  	t.ctx = ctx
   237  
   238  	// we check kube for min clusters, these assume we're talking about real multicluster.
   239  	// it's possible to have 1 kube cluster then 1 non-kube cluster (vm for example)
   240  	if t.requiredMinClusters > 0 && len(t.s.Environment().Clusters().Kube()) < t.requiredMinClusters {
   241  		t.goTest.Skipf("Skipping %q: number of clusters %d is below required min %d",
   242  			t.goTest.Name(), len(t.s.Environment().Clusters()), t.requiredMinClusters)
   243  		return
   244  	}
   245  
   246  	// max clusters doesn't check kube only, the test may be written in a way that doesn't loop over all of Clusters()
   247  	if t.requiredMaxClusters > 0 && len(t.s.Environment().Clusters()) > t.requiredMaxClusters {
   248  		t.goTest.Skipf("Skipping %q: number of clusters %d is above required max %d",
   249  			t.goTest.Name(), len(t.s.Environment().Clusters()), t.requiredMaxClusters)
   250  		return
   251  	}
   252  
   253  	if t.minKubernetesMinorVersion > 0 {
   254  		for _, c := range ctx.Clusters() {
   255  			if !c.MinKubeVersion(t.minKubernetesMinorVersion) {
   256  				t.goTest.Skipf("Skipping %q: cluster %s is below required min k8s version 1.%d",
   257  					t.goTest.Name(), c.Name(), t.minKubernetesMinorVersion)
   258  				return
   259  			}
   260  		}
   261  	}
   262  
   263  	if t.requireLocalIstiod {
   264  		for _, c := range ctx.Clusters() {
   265  			if !c.IsPrimary() {
   266  				t.goTest.Skipf(fmt.Sprintf("Skipping %q: cluster %s is not using a local control plane",
   267  					t.goTest.Name(), c.Name()))
   268  				return
   269  			}
   270  		}
   271  	}
   272  
   273  	if t.requireSingleNetwork && t.s.Environment().IsMultiNetwork() {
   274  		t.goTest.Skipf(fmt.Sprintf("Skipping %q: only single network allowed",
   275  			t.goTest.Name()))
   276  		return
   277  	}
   278  
   279  	if t.minIstioVersion != "" {
   280  		if !t.ctx.Settings().Revisions.AtLeast(resource.IstioVersion(t.minIstioVersion)) {
   281  			t.goTest.Skipf("Skipping %q: running with min Istio version %q, test requires at least %s",
   282  				t.goTest.Name(), t.ctx.Settings().Revisions.Minimum(), t.minIstioVersion)
   283  		}
   284  	}
   285  
   286  	start := time.Now()
   287  	scopes.Framework.Infof("=== BEGIN: Test: '%s[%s]' ===", rt.suiteContext().Settings().TestID, t.goTest.Name())
   288  
   289  	// Initial setup if we're running in Parallel.
   290  	if parallel {
   291  		// Run the underlying Go test in parallel. This will not return until the parent
   292  		// test (if there is one) exits.
   293  		t.goTest.Parallel()
   294  	}
   295  
   296  	// Register the cleanup function for when the Go test completes.
   297  	t.goTest.Cleanup(func() {
   298  		message := "passed"
   299  		if t.goTest.Failed() {
   300  			message = "failed"
   301  		}
   302  		scopes.Framework.Infof("=== DONE (%s):  Test: '%s[%s] (%v)' ===",
   303  			message,
   304  			rt.suiteContext().Settings().TestID,
   305  			t.goTest.Name(),
   306  			time.Since(start))
   307  		t.ts.End()
   308  	})
   309  
   310  	// Run the user's test function.
   311  	fn(ctx)
   312  }