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 }