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  }