istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/suite.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 "errors" 19 "fmt" 20 "os" 21 "path/filepath" 22 "regexp" 23 goruntime "runtime" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 "github.com/hashicorp/go-multierror" 30 31 kubelib "istio.io/istio/pkg/kube" 32 "istio.io/istio/pkg/test/echo" 33 "istio.io/istio/pkg/test/framework/components/cluster" 34 "istio.io/istio/pkg/test/framework/components/environment/kube" 35 "istio.io/istio/pkg/test/framework/config" 36 ferrors "istio.io/istio/pkg/test/framework/errors" 37 "istio.io/istio/pkg/test/framework/label" 38 "istio.io/istio/pkg/test/framework/resource" 39 "istio.io/istio/pkg/test/prow" 40 "istio.io/istio/pkg/test/scopes" 41 "istio.io/istio/pkg/tracing" 42 ) 43 44 // test.Run uses 0, 1, 2 exit codes. Use different exit codes for our framework. 45 const ( 46 // Indicates a framework-level init error 47 exitCodeInitError = -1 48 49 // Indicates an error due to the setup function supplied by the user 50 exitCodeSetupError = -2 51 ) 52 53 var ( 54 rt *runtime 55 rtMu sync.Mutex 56 57 // Well-known paths which are stripped when generating test IDs. 58 // Note: Order matters! Always specify the most specific directory first. 59 wellKnownPaths = mustCompileAll( 60 // This allows us to trim test IDs on the istio.io/istio.io repo. 61 ".*/istio.io/istio.io/", 62 63 // These allow us to trim test IDs on istio.io/istio repo. 64 ".*/istio.io/istio/tests/integration/", 65 ".*/istio.io/istio/", 66 67 // These are also used for istio.io/istio, but make help to satisfy 68 // the feature label enforcement when running with BUILD_WITH_CONTAINER=1. 69 "^/work/tests/integration/", 70 "^/work/", 71 72 // Outside of standard Istio GOPATH 73 ".*/istio/tests/integration/", 74 ) 75 ) 76 77 // getSettingsFunc is a function used to extract the default settings for the Suite. 78 type getSettingsFunc func(string) (*resource.Settings, error) 79 80 // mRunFn abstracts testing.M.run, so that the framework itself can be tested. 81 type mRunFn func(ctx *suiteContext) int 82 83 // Suite allows the test author to specify suite-related metadata and do setup in a fluent-style, before commencing execution. 84 type Suite interface { 85 // EnvironmentFactory sets a custom function used for creating the resource.Environment for this Suite. 86 EnvironmentFactory(fn resource.EnvironmentFactory) Suite 87 // Label all the tests in suite with the given labels 88 Label(labels ...label.Instance) Suite 89 // SkipIf skips the suite if the function returns true 90 SkipIf(reason string, fn resource.ShouldSkipFn) Suite 91 // Skip marks a suite as skipped with the given reason. This will prevent any setup functions from occurring. 92 Skip(reason string) Suite 93 // RequireMinClusters ensures that the current environment contains at least the given number of clusters. 94 // Otherwise it stops test execution. 95 // 96 // Deprecated: Tests should not make assumptions about number of clusters. 97 RequireMinClusters(minClusters int) Suite 98 // RequireMaxClusters ensures that the current environment contains at least the given number of clusters. 99 // Otherwise it stops test execution. 100 // 101 // Deprecated: Tests should not make assumptions about number of clusters. 102 RequireMaxClusters(maxClusters int) Suite 103 // RequireSingleCluster is a utility method that requires that there be exactly 1 cluster in the environment. 104 // 105 // Deprecated: All new tests should support multiple clusters. 106 RequireSingleCluster() Suite 107 // RequireMultiPrimary ensures that each cluster is running a control plane. 108 // 109 // Deprecated: All new tests should work for any control plane topology. 110 RequireMultiPrimary() Suite 111 // SkipExternalControlPlaneTopology skips the tests in external plane and config cluster topology 112 SkipExternalControlPlaneTopology() Suite 113 // RequireExternalControlPlaneTopology requires the environment to be external control plane topology 114 RequireExternalControlPlaneTopology() Suite 115 // RequireMinVersion validates the environment meets a minimum version 116 RequireMinVersion(minorVersion uint) Suite 117 // RequireMaxVersion validates the environment meets a maximum version 118 RequireMaxVersion(minorVersion uint) Suite 119 // Setup runs enqueues the given setup function to run before test execution. 120 Setup(fn resource.SetupFn) Suite 121 Teardown(fn resource.TeardownFn) Suite 122 // SetupParallel runs the given setup functions in parallel before test execution. 123 SetupParallel(fns ...resource.SetupFn) Suite 124 // Run the suite. This method calls os.Exit and does not return. 125 Run() 126 } 127 128 // suiteImpl will actually run the test suite 129 type suiteImpl struct { 130 testID string 131 skipMessage string 132 skipFn resource.ShouldSkipFn 133 mRun mRunFn 134 osExit func(int) 135 labels label.Set 136 137 requireFns []resource.SetupFn 138 setupFns []resource.SetupFn 139 teardownFns []resource.TeardownFn 140 141 getSettings getSettingsFunc 142 envFactory resource.EnvironmentFactory 143 } 144 145 // Given the filename of a test, derive its suite name 146 func deriveSuiteName(caller string) string { 147 d := filepath.Dir(caller) 148 // We will trim out paths preceding some well known paths. This should handle anything in istio or docs repo, 149 // as well as special case tests/integration. The end result is a test under ./tests/integration/pilot/ingress 150 // will become pilot_ingress 151 // Note: if this fails to trim, we end up with "ugly" suite names but otherwise no real impact. 152 for _, wellKnownPath := range wellKnownPaths { 153 // Try removing this path from the directory name. 154 result := wellKnownPath.ReplaceAllString(d, "") 155 if len(result) < len(d) { 156 // Successfully found and removed this path from the directory. 157 d = result 158 break 159 } 160 } 161 return strings.ReplaceAll(d, "/", "_") 162 } 163 164 // NewSuite returns a new suite instance. 165 func NewSuite(m *testing.M) Suite { 166 _, f, _, _ := goruntime.Caller(1) 167 suiteName := deriveSuiteName(f) 168 169 return newSuite(suiteName, 170 func(_ *suiteContext) int { 171 return m.Run() 172 }, 173 os.Exit, 174 getSettings) 175 } 176 177 func newSuite(testID string, fn mRunFn, osExit func(int), getSettingsFn getSettingsFunc) *suiteImpl { 178 s := &suiteImpl{ 179 testID: testID, 180 mRun: fn, 181 osExit: osExit, 182 getSettings: getSettingsFn, 183 labels: label.NewSet(), 184 } 185 186 return s 187 } 188 189 func (s *suiteImpl) EnvironmentFactory(fn resource.EnvironmentFactory) Suite { 190 if fn != nil && s.envFactory != nil { 191 scopes.Framework.Warn("EnvironmentFactory overridden multiple times for Suite") 192 } 193 s.envFactory = fn 194 return s 195 } 196 197 func (s *suiteImpl) Label(labels ...label.Instance) Suite { 198 s.labels = s.labels.Add(labels...) 199 return s 200 } 201 202 func (s *suiteImpl) Skip(reason string) Suite { 203 s.skipMessage = reason 204 s.skipFn = func(ctx resource.Context) bool { 205 return true 206 } 207 return s 208 } 209 210 func (s *suiteImpl) SkipIf(reason string, fn resource.ShouldSkipFn) Suite { 211 s.skipMessage = reason 212 s.skipFn = fn 213 return s 214 } 215 216 func (s *suiteImpl) RequireMinClusters(minClusters int) Suite { 217 if minClusters <= 0 { 218 minClusters = 1 219 } 220 221 fn := func(ctx resource.Context) error { 222 if len(clusters(ctx)) < minClusters { 223 s.Skip(fmt.Sprintf("Number of clusters %d does not exceed minimum %d", 224 len(clusters(ctx)), minClusters)) 225 } 226 return nil 227 } 228 229 s.requireFns = append(s.requireFns, fn) 230 return s 231 } 232 233 func (s *suiteImpl) RequireMaxClusters(maxClusters int) Suite { 234 if maxClusters <= 0 { 235 maxClusters = 1 236 } 237 238 fn := func(ctx resource.Context) error { 239 if len(clusters(ctx)) > maxClusters { 240 s.Skip(fmt.Sprintf("Number of clusters %d exceeds maximum %d", 241 len(clusters(ctx)), maxClusters)) 242 } 243 return nil 244 } 245 246 s.requireFns = append(s.requireFns, fn) 247 return s 248 } 249 250 func (s *suiteImpl) RequireSingleCluster() Suite { 251 // nolint: staticcheck 252 return s.RequireMinClusters(1).RequireMaxClusters(1) 253 } 254 255 func (s *suiteImpl) RequireMultiPrimary() Suite { 256 fn := func(ctx resource.Context) error { 257 for _, c := range ctx.Clusters() { 258 if !c.IsPrimary() { 259 s.Skip(fmt.Sprintf("Cluster %s is not using a local control plane", 260 c.Name())) 261 } 262 } 263 return nil 264 } 265 266 s.requireFns = append(s.requireFns, fn) 267 return s 268 } 269 270 func (s *suiteImpl) SkipExternalControlPlaneTopology() Suite { 271 fn := func(ctx resource.Context) error { 272 for _, c := range ctx.Clusters() { 273 if c.IsConfig() && !c.IsPrimary() { 274 s.Skip(fmt.Sprintf("Cluster %s is a config cluster, we can't run external control plane topology", 275 c.Name())) 276 } 277 } 278 return nil 279 } 280 s.requireFns = append(s.requireFns, fn) 281 return s 282 } 283 284 func (s *suiteImpl) RequireExternalControlPlaneTopology() Suite { 285 fn := func(ctx resource.Context) error { 286 for _, c := range ctx.Clusters() { 287 if c.IsConfig() && !c.IsPrimary() { 288 // the test environment is an external control plane topology, the test can go on 289 return nil 290 } 291 } 292 // the test environment is not an external control plane topology, skip the test 293 s.Skip("Not an external control plane topology, skip this test") 294 return nil 295 } 296 s.requireFns = append(s.requireFns, fn) 297 return s 298 } 299 300 func (s *suiteImpl) RequireMinVersion(minorVersion uint) Suite { 301 fn := func(ctx resource.Context) error { 302 for _, c := range ctx.Clusters().Kube() { 303 ver, err := c.GetKubernetesVersion() 304 if err != nil { 305 return fmt.Errorf("failed to get Kubernetes version: %v", err) 306 } 307 if !kubelib.IsAtLeastVersion(c, minorVersion) { 308 s.Skip(fmt.Sprintf("Required Kubernetes version (1.%v) is greater than current: %v", 309 minorVersion, ver.String())) 310 } 311 } 312 return nil 313 } 314 315 s.requireFns = append(s.requireFns, fn) 316 return s 317 } 318 319 func (s *suiteImpl) RequireMaxVersion(minorVersion uint) Suite { 320 fn := func(ctx resource.Context) error { 321 for _, c := range ctx.Clusters().Kube() { 322 ver, err := c.GetKubernetesVersion() 323 if err != nil { 324 return fmt.Errorf("failed to get Kubernetes version: %v", err) 325 } 326 if !kubelib.IsLessThanVersion(c, minorVersion+1) { 327 s.Skip(fmt.Sprintf("Maximum Kubernetes version (1.%v) is less than current: %v", 328 minorVersion, ver.String())) 329 } 330 } 331 return nil 332 } 333 334 s.requireFns = append(s.requireFns, fn) 335 return s 336 } 337 338 func (s *suiteImpl) Setup(fn resource.SetupFn) Suite { 339 s.setupFns = append(s.setupFns, fn) 340 return s 341 } 342 343 func (s *suiteImpl) Teardown(fn resource.TeardownFn) Suite { 344 s.teardownFns = append(s.teardownFns, fn) 345 return s 346 } 347 348 func (s *suiteImpl) SetupParallel(fns ...resource.SetupFn) Suite { 349 s.setupFns = append(s.setupFns, func(ctx resource.Context) error { 350 g := multierror.Group{} 351 for _, fn := range fns { 352 fn := fn 353 g.Go(func() error { 354 return fn(ctx) 355 }) 356 } 357 return g.Wait().ErrorOrNil() 358 }) 359 return s 360 } 361 362 func (s *suiteImpl) runSetupFn(fn resource.SetupFn, ctx SuiteContext) (err error) { 363 defer func() { 364 // Dump if the setup function fails 365 if err != nil && ctx.Settings().CIMode { 366 rt.Dump(ctx) 367 } 368 }() 369 err = fn(ctx) 370 return 371 } 372 373 func (s *suiteImpl) Run() { 374 s.osExit(s.run()) 375 } 376 377 func (s *suiteImpl) isSkipped(ctx SuiteContext) bool { 378 if s.skipFn != nil && s.skipFn(ctx) { 379 return true 380 } 381 return false 382 } 383 384 func (s *suiteImpl) doSkip(ctx *suiteContext) int { 385 scopes.Framework.Infof("Skipping suite %q: %s", ctx.Settings().TestID, s.skipMessage) 386 387 // Mark this suite as skipped in the context. 388 ctx.skipped = true 389 390 // Run the tests so that the golang test framework exits normally. The tests will not run because 391 // they see that this suite has been skipped. 392 _ = s.mRun(ctx) 393 394 // Return success. 395 return 0 396 } 397 398 func (s *suiteImpl) run() (errLevel int) { 399 tc, shutdown, err := tracing.InitializeFullBinary(s.testID) 400 if err != nil { 401 return 99 402 } 403 defer shutdown() 404 if err := initRuntime(s); err != nil { 405 scopes.Framework.Errorf("Error during test framework init: %v", err) 406 return exitCodeInitError 407 } 408 rt.context.traceContext = tc 409 410 ctx := rt.suiteContext() 411 // Skip the test if its explicitly skipped 412 if s.isSkipped(ctx) { 413 return s.doSkip(ctx) 414 } 415 416 // Before starting, check whether the current set of labels & label selectors will ever allow us to run tests. 417 // if not, simply exit now. 418 if ctx.Settings().Selector.Excludes(s.labels) { 419 s.Skip(fmt.Sprintf("Label mismatch: labels=%v, selector=%v", 420 s.labels, 421 ctx.Settings().Selector)) 422 return s.doSkip(ctx) 423 } 424 425 start := time.Now() 426 427 defer func() { 428 if errLevel != 0 && ctx.Settings().CIMode { 429 rt.Dump(ctx) 430 } 431 432 if err := rt.Close(); err != nil { 433 scopes.Framework.Errorf("Error during close: %v", err) 434 if rt.context.settings.FailOnDeprecation { 435 if ferrors.IsOrContainsDeprecatedError(err) { 436 errLevel = 1 437 } 438 } 439 } 440 rt = nil 441 }() 442 443 _, span := tracing.Start(tc, "setup") 444 if err := s.runSetupFns(ctx); err != nil { 445 scopes.Framework.Errorf("Exiting due to setup failure: %v", err) 446 return exitCodeSetupError 447 } 448 span.End() 449 450 // Check if one of the setup functions ended up skipping the suite. 451 if s.isSkipped(ctx) { 452 return s.doSkip(ctx) 453 } 454 455 defer func() { 456 end := time.Now() 457 scopes.Framework.Infof("=== Suite %q run time: %v ===", ctx.Settings().TestID, end.Sub(start)) 458 459 ctx.RecordTraceEvent("suite-runtime", end.Sub(start).Seconds()) 460 ctx.RecordTraceEvent("echo-calls", echo.GlobalEchoRequests.Load()) 461 ctx.RecordTraceEvent("yaml-apply", GlobalYAMLWrites.Load()) 462 traceFile := filepath.Join(ctx.Settings().BaseDir, "trace.yaml") 463 scopes.Framework.Infof("Wrote trace to %v", prow.ArtifactsURL(traceFile)) 464 _ = appendToFile(ctx.marshalTraceEvent(), traceFile) 465 }() 466 467 attempt := 0 468 for attempt <= ctx.settings.Retries { 469 attempt++ 470 scopes.Framework.Infof("=== BEGIN: Test Run: '%s' ===", ctx.Settings().TestID) 471 errLevel = s.mRun(ctx) 472 if errLevel == 0 { 473 scopes.Framework.Infof("=== DONE: Test Run: '%s' ===", ctx.Settings().TestID) 474 break 475 } 476 scopes.Framework.Infof("=== FAILED: Test Run: '%s' (exitCode: %v) ===", 477 ctx.Settings().TestID, errLevel) 478 if attempt <= ctx.settings.Retries { 479 scopes.Framework.Warnf("=== RETRY: Test Run: '%s' ===", ctx.Settings().TestID) 480 } 481 } 482 s.runTeardownFns(ctx) 483 484 return 485 } 486 487 func clusters(ctx resource.Context) []cluster.Cluster { 488 if ctx.Environment() != nil { 489 return ctx.Environment().Clusters() 490 } 491 return nil 492 } 493 494 func (s *suiteImpl) runSetupFns(ctx SuiteContext) (err error) { 495 scopes.Framework.Infof("=== BEGIN: Setup: '%s' ===", ctx.Settings().TestID) 496 497 // Run all the require functions first, then the setup functions. 498 setupFns := append(append([]resource.SetupFn{}, s.requireFns...), s.setupFns...) 499 500 // don't waste time setting up if already skipped 501 if s.isSkipped(ctx) { 502 return nil 503 } 504 505 start := time.Now() 506 for _, fn := range setupFns { 507 err := s.runSetupFn(fn, ctx) 508 if err != nil { 509 scopes.Framework.Errorf("Test setup error: %v", err) 510 scopes.Framework.Infof("=== FAILED: Setup: '%s' (%v) ===", ctx.Settings().TestID, err) 511 return err 512 } 513 514 // setup added a skip 515 if s.isSkipped(ctx) { 516 return nil 517 } 518 } 519 elapsed := time.Since(start) 520 scopes.Framework.Infof("=== DONE: Setup: '%s' (%v) ===", ctx.Settings().TestID, elapsed) 521 return nil 522 } 523 524 func (s *suiteImpl) runTeardownFns(ctx SuiteContext) { 525 if len(s.teardownFns) == 0 { 526 return 527 } 528 scopes.Framework.Infof("=== BEGIN: Teardown: '%s' ===", ctx.Settings().TestID) 529 530 // don't waste time tearing up if already skipped 531 if s.isSkipped(ctx) { 532 return 533 } 534 535 start := time.Now() 536 for _, fn := range s.teardownFns { 537 fn(ctx) 538 } 539 elapsed := time.Since(start) 540 scopes.Framework.Infof("=== DONE: Teardown: '%s' (%v) ===", ctx.Settings().TestID, elapsed) 541 } 542 543 func initRuntime(s *suiteImpl) error { 544 rtMu.Lock() 545 defer rtMu.Unlock() 546 547 if rt != nil { 548 return errors.New("framework is already initialized") 549 } 550 551 settings, err := s.getSettings(s.testID) 552 if err != nil { 553 return err 554 } 555 556 // Get the EnvironmentFactory. 557 environmentFactory := s.envFactory 558 if environmentFactory == nil { 559 environmentFactory = settings.EnvironmentFactory 560 } 561 if environmentFactory == nil { 562 environmentFactory = newEnvironment 563 } 564 565 if err := configureLogging(); err != nil { 566 return err 567 } 568 569 scopes.Framework.Infof("=== Test Framework Settings ===") 570 scopes.Framework.Info(settings.String()) 571 scopes.Framework.Infof("===============================") 572 573 // Ensure that the work dir is set. 574 if err := os.MkdirAll(settings.RunDir(), os.ModePerm); err != nil { 575 return fmt.Errorf("error creating rundir %q: %v", settings.RunDir(), err) 576 } 577 scopes.Framework.Infof("Test run dir: %v", settings.RunDir()) 578 579 rt, err = newRuntime(settings, environmentFactory, s.labels) 580 return err 581 } 582 583 func newEnvironment(ctx resource.Context) (resource.Environment, error) { 584 s, err := kube.NewSettingsFromCommandLine() 585 if err != nil { 586 return nil, err 587 } 588 return kube.New(ctx, s) 589 } 590 591 func getSettings(testID string) (*resource.Settings, error) { 592 // Parse flags and init logging. 593 if !config.Parsed() { 594 config.Parse() 595 } 596 597 return resource.SettingsFromCommandLine(testID) 598 } 599 600 func mustCompileAll(patterns ...string) []*regexp.Regexp { 601 out := make([]*regexp.Regexp, 0, len(patterns)) 602 for _, pattern := range patterns { 603 out = append(out, regexp.MustCompile(pattern)) 604 } 605 606 return out 607 } 608 609 func appendToFile(contents []byte, file string) error { 610 f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) 611 if err != nil { 612 return err 613 } 614 615 defer func() { 616 _ = f.Close() 617 }() 618 619 if _, err = f.Write(contents); err != nil { 620 return err 621 } 622 return nil 623 }