k8s.io/kubernetes@v1.29.3/test/e2e/framework/ginkgowrapper.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package framework
    18  
    19  import (
    20  	"fmt"
    21  	"path"
    22  	"reflect"
    23  	"regexp"
    24  	"slices"
    25  	"strings"
    26  
    27  	"github.com/onsi/ginkgo/v2"
    28  	"github.com/onsi/ginkgo/v2/types"
    29  
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    33  	"k8s.io/component-base/featuregate"
    34  )
    35  
    36  // Feature is the name of a certain feature that the cluster under test must have.
    37  // Such features are different from feature gates.
    38  type Feature string
    39  
    40  // Environment is the name for the environment in which a test can run, like
    41  // "Linux" or "Windows".
    42  type Environment string
    43  
    44  // NodeFeature is the name of a feature that a node must support. To be
    45  // removed, see
    46  // https://github.com/kubernetes/enhancements/tree/master/keps/sig-testing/3041-node-conformance-and-features#nodefeature.
    47  type NodeFeature string
    48  
    49  type Valid[T comparable] struct {
    50  	items  sets.Set[T]
    51  	frozen bool
    52  }
    53  
    54  // Add registers a new valid item name. The expected usage is
    55  //
    56  //	var SomeFeature = framework.ValidFeatures.Add("Some")
    57  //
    58  // during the init phase of an E2E suite. Individual tests should not register
    59  // their own, to avoid uncontrolled proliferation of new items. E2E suites can,
    60  // but don't have to, enforce that by freezing the set of valid names.
    61  func (v *Valid[T]) Add(item T) T {
    62  	if v.frozen {
    63  		RecordBug(NewBug(fmt.Sprintf(`registry %T is already frozen, "%v" must not be added anymore`, *v, item), 1))
    64  	}
    65  	if v.items == nil {
    66  		v.items = sets.New[T]()
    67  	}
    68  	if v.items.Has(item) {
    69  		RecordBug(NewBug(fmt.Sprintf(`registry %T already contains "%v", it must not be added again`, *v, item), 1))
    70  	}
    71  	v.items.Insert(item)
    72  	return item
    73  }
    74  
    75  func (v *Valid[T]) Freeze() {
    76  	v.frozen = true
    77  }
    78  
    79  // These variables contain the parameters that [WithFeature], [WithEnvironment]
    80  // and [WithNodeFeatures] accept. The framework itself has no pre-defined
    81  // constants. Test suites and tests may define their own and then add them here
    82  // before calling these With functions.
    83  var (
    84  	ValidFeatures     Valid[Feature]
    85  	ValidEnvironments Valid[Environment]
    86  	ValidNodeFeatures Valid[NodeFeature]
    87  )
    88  
    89  var errInterface = reflect.TypeOf((*error)(nil)).Elem()
    90  
    91  // IgnoreNotFound can be used to wrap an arbitrary function in a call to
    92  // [ginkgo.DeferCleanup]. When the wrapped function returns an error that
    93  // `apierrors.IsNotFound` considers as "not found", the error is ignored
    94  // instead of failing the test during cleanup. This is useful for cleanup code
    95  // that just needs to ensure that some object does not exist anymore.
    96  func IgnoreNotFound(in any) any {
    97  	inType := reflect.TypeOf(in)
    98  	inValue := reflect.ValueOf(in)
    99  	return reflect.MakeFunc(inType, func(args []reflect.Value) []reflect.Value {
   100  		out := inValue.Call(args)
   101  		if len(out) > 0 {
   102  			lastValue := out[len(out)-1]
   103  			last := lastValue.Interface()
   104  			if last != nil && lastValue.Type().Implements(errInterface) && apierrors.IsNotFound(last.(error)) {
   105  				out[len(out)-1] = reflect.Zero(errInterface)
   106  			}
   107  		}
   108  		return out
   109  	}).Interface()
   110  }
   111  
   112  // AnnotatedLocation can be used to provide more informative source code
   113  // locations by passing the result as additional parameter to a
   114  // BeforeEach/AfterEach/DeferCleanup/It/etc.
   115  func AnnotatedLocation(annotation string) types.CodeLocation {
   116  	return AnnotatedLocationWithOffset(annotation, 1)
   117  }
   118  
   119  // AnnotatedLocationWithOffset skips additional call stack levels. With 0 as offset
   120  // it is identical to [AnnotatedLocation].
   121  func AnnotatedLocationWithOffset(annotation string, offset int) types.CodeLocation {
   122  	codeLocation := types.NewCodeLocation(offset + 1)
   123  	codeLocation.FileName = path.Base(codeLocation.FileName)
   124  	codeLocation = types.NewCustomCodeLocation(annotation + " | " + codeLocation.String())
   125  	return codeLocation
   126  }
   127  
   128  // SIGDescribe returns a wrapper function for ginkgo.Describe which injects
   129  // the SIG name as annotation. The parameter should be lowercase with
   130  // no spaces and no sig- or SIG- prefix.
   131  func SIGDescribe(sig string) func(...interface{}) bool {
   132  	if !sigRE.MatchString(sig) || strings.HasPrefix(sig, "sig-") {
   133  		RecordBug(NewBug(fmt.Sprintf("SIG label must be lowercase, no spaces and no sig- prefix, got instead: %q", sig), 1))
   134  	}
   135  	return func(args ...interface{}) bool {
   136  		args = append([]interface{}{WithLabel("sig-" + sig)}, args...)
   137  		return registerInSuite(ginkgo.Describe, args)
   138  	}
   139  }
   140  
   141  var sigRE = regexp.MustCompile(`^[a-z]+(-[a-z]+)*$`)
   142  
   143  // ConformanceIt is wrapper function for ginkgo It.  Adds "[Conformance]" tag and makes static analysis easier.
   144  func ConformanceIt(args ...interface{}) bool {
   145  	args = append(args, ginkgo.Offset(1), WithConformance())
   146  	return It(args...)
   147  }
   148  
   149  // It is a wrapper around [ginkgo.It] which supports framework With* labels as
   150  // optional arguments in addition to those already supported by ginkgo itself,
   151  // like [ginkgo.Label] and [gingko.Offset].
   152  //
   153  // Text and arguments may be mixed. The final text is a concatenation
   154  // of the text arguments and special tags from the With functions.
   155  func It(args ...interface{}) bool {
   156  	return registerInSuite(ginkgo.It, args)
   157  }
   158  
   159  // It is a shorthand for the corresponding package function.
   160  func (f *Framework) It(args ...interface{}) bool {
   161  	return registerInSuite(ginkgo.It, args)
   162  }
   163  
   164  // Describe is a wrapper around [ginkgo.Describe] which supports framework
   165  // With* labels as optional arguments in addition to those already supported by
   166  // ginkgo itself, like [ginkgo.Label] and [gingko.Offset].
   167  //
   168  // Text and arguments may be mixed. The final text is a concatenation
   169  // of the text arguments and special tags from the With functions.
   170  func Describe(args ...interface{}) bool {
   171  	return registerInSuite(ginkgo.Describe, args)
   172  }
   173  
   174  // Describe is a shorthand for the corresponding package function.
   175  func (f *Framework) Describe(args ...interface{}) bool {
   176  	return registerInSuite(ginkgo.Describe, args)
   177  }
   178  
   179  // Context is a wrapper around [ginkgo.Context] which supports framework With*
   180  // labels as optional arguments in addition to those already supported by
   181  // ginkgo itself, like [ginkgo.Label] and [gingko.Offset].
   182  //
   183  // Text and arguments may be mixed. The final text is a concatenation
   184  // of the text arguments and special tags from the With functions.
   185  func Context(args ...interface{}) bool {
   186  	return registerInSuite(ginkgo.Context, args)
   187  }
   188  
   189  // Context is a shorthand for the corresponding package function.
   190  func (f *Framework) Context(args ...interface{}) bool {
   191  	return registerInSuite(ginkgo.Context, args)
   192  }
   193  
   194  // registerInSuite is the common implementation of all wrapper functions. It
   195  // expects to be called through one intermediate wrapper.
   196  func registerInSuite(ginkgoCall func(string, ...interface{}) bool, args []interface{}) bool {
   197  	var ginkgoArgs []interface{}
   198  	var offset ginkgo.Offset
   199  	var texts []string
   200  
   201  	addLabel := func(label string) {
   202  		texts = append(texts, fmt.Sprintf("[%s]", label))
   203  		ginkgoArgs = append(ginkgoArgs, ginkgo.Label(label))
   204  	}
   205  
   206  	haveEmptyStrings := false
   207  	for _, arg := range args {
   208  		switch arg := arg.(type) {
   209  		case label:
   210  			fullLabel := strings.Join(arg.parts, ":")
   211  			addLabel(fullLabel)
   212  			if arg.extra != "" {
   213  				addLabel(arg.extra)
   214  			}
   215  			if fullLabel == "Serial" {
   216  				ginkgoArgs = append(ginkgoArgs, ginkgo.Serial)
   217  			}
   218  		case ginkgo.Offset:
   219  			offset = arg
   220  		case string:
   221  			if arg == "" {
   222  				haveEmptyStrings = true
   223  			}
   224  			texts = append(texts, arg)
   225  		default:
   226  			ginkgoArgs = append(ginkgoArgs, arg)
   227  		}
   228  	}
   229  	offset += 2 // This function and its direct caller.
   230  
   231  	// Now that we have the final offset, we can record bugs.
   232  	if haveEmptyStrings {
   233  		RecordBug(NewBug("empty strings as separators are unnecessary and need to be removed", int(offset)))
   234  	}
   235  
   236  	// Enforce that text snippets to not start or end with spaces because
   237  	// those lead to double spaces when concatenating below.
   238  	for _, text := range texts {
   239  		if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
   240  			RecordBug(NewBug(fmt.Sprintf("trailing or leading spaces are unnecessary and need to be removed: %q", text), int(offset)))
   241  		}
   242  	}
   243  
   244  	ginkgoArgs = append(ginkgoArgs, offset)
   245  	text := strings.Join(texts, " ")
   246  	return ginkgoCall(text, ginkgoArgs...)
   247  }
   248  
   249  var (
   250  	tagRe                 = regexp.MustCompile(`\[.*?\]`)
   251  	deprecatedTags        = sets.New("Conformance", "NodeConformance", "Disruptive", "Serial", "Slow")
   252  	deprecatedTagPrefixes = sets.New("Environment", "Feature", "NodeFeature", "FeatureGate")
   253  	deprecatedStability   = sets.New("Alpha", "Beta")
   254  )
   255  
   256  // validateSpecs checks that the test specs were registered as intended.
   257  func validateSpecs(specs types.SpecReports) {
   258  	checked := sets.New[call]()
   259  
   260  	for _, spec := range specs {
   261  		for i, text := range spec.ContainerHierarchyTexts {
   262  			c := call{
   263  				text:     text,
   264  				location: spec.ContainerHierarchyLocations[i],
   265  			}
   266  			if checked.Has(c) {
   267  				// No need to check the same container more than once.
   268  				continue
   269  			}
   270  			checked.Insert(c)
   271  			validateText(c.location, text, spec.ContainerHierarchyLabels[i])
   272  		}
   273  		c := call{
   274  			text:     spec.LeafNodeText,
   275  			location: spec.LeafNodeLocation,
   276  		}
   277  		if !checked.Has(c) {
   278  			validateText(spec.LeafNodeLocation, spec.LeafNodeText, spec.LeafNodeLabels)
   279  			checked.Insert(c)
   280  		}
   281  	}
   282  }
   283  
   284  // call acts as (mostly) unique identifier for a container node call like
   285  // Describe or Context. It's not perfect because theoretically a line might
   286  // have multiple calls with the same text, but that isn't a problem in
   287  // practice.
   288  type call struct {
   289  	text     string
   290  	location types.CodeLocation
   291  }
   292  
   293  // validateText checks for some known tags that should not be added through the
   294  // plain text strings anymore. Eventually, all such tags should get replaced
   295  // with the new APIs.
   296  func validateText(location types.CodeLocation, text string, labels []string) {
   297  	for _, tag := range tagRe.FindAllString(text, -1) {
   298  		if tag == "[]" {
   299  			recordTextBug(location, "[] in plain text is invalid")
   300  			continue
   301  		}
   302  		// Strip square brackets.
   303  		tag = tag[1 : len(tag)-1]
   304  		if slices.Contains(labels, tag) {
   305  			// Okay, was also set as label.
   306  			continue
   307  		}
   308  		if deprecatedTags.Has(tag) {
   309  			recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added through With%s instead", tag, tag))
   310  		}
   311  		if deprecatedStability.Has(tag) {
   312  			recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added by defining the feature gate through WithFeatureGate instead", tag))
   313  		}
   314  		if index := strings.Index(tag, ":"); index > 0 {
   315  			prefix := tag[:index]
   316  			if deprecatedTagPrefixes.Has(prefix) {
   317  				recordTextBug(location, fmt.Sprintf("[%s] in plain text is deprecated and must be added through With%s(%s) instead", tag, prefix, tag[index+1:]))
   318  			}
   319  		}
   320  	}
   321  }
   322  
   323  func recordTextBug(location types.CodeLocation, message string) {
   324  	RecordBug(Bug{FileName: location.FileName, LineNumber: location.LineNumber, Message: message})
   325  }
   326  
   327  // WithEnvironment specifies that a certain test or group of tests only works
   328  // with a feature available. The return value must be passed as additional
   329  // argument to [framework.It], [framework.Describe], [framework.Context].
   330  //
   331  // The feature must be listed in ValidFeatures.
   332  func WithFeature(name Feature) interface{} {
   333  	return withFeature(name)
   334  }
   335  
   336  // WithFeature is a shorthand for the corresponding package function.
   337  func (f *Framework) WithFeature(name Feature) interface{} {
   338  	return withFeature(name)
   339  }
   340  
   341  func withFeature(name Feature) interface{} {
   342  	if !ValidFeatures.items.Has(name) {
   343  		RecordBug(NewBug(fmt.Sprintf("WithFeature: unknown feature %q", name), 2))
   344  	}
   345  	return newLabel("Feature", string(name))
   346  }
   347  
   348  // WithFeatureGate specifies that a certain test or group of tests depends on a
   349  // feature gate being enabled. The return value must be passed as additional
   350  // argument to [framework.It], [framework.Describe], [framework.Context].
   351  //
   352  // The feature gate must be listed in
   353  // [k8s.io/apiserver/pkg/util/feature.DefaultMutableFeatureGate]. Once a
   354  // feature gate gets removed from there, the WithFeatureGate calls using it
   355  // also need to be removed.
   356  func WithFeatureGate(featureGate featuregate.Feature) interface{} {
   357  	return withFeatureGate(featureGate)
   358  }
   359  
   360  // WithFeatureGate is a shorthand for the corresponding package function.
   361  func (f *Framework) WithFeatureGate(featureGate featuregate.Feature) interface{} {
   362  	return withFeatureGate(featureGate)
   363  }
   364  
   365  func withFeatureGate(featureGate featuregate.Feature) interface{} {
   366  	spec, ok := utilfeature.DefaultMutableFeatureGate.GetAll()[featureGate]
   367  	if !ok {
   368  		RecordBug(NewBug(fmt.Sprintf("WithFeatureGate: the feature gate %q is unknown", featureGate), 2))
   369  	}
   370  
   371  	// We use mixed case (i.e. Beta instead of BETA). GA feature gates have no level string.
   372  	var level string
   373  	if spec.PreRelease != "" {
   374  		level = string(spec.PreRelease)
   375  		level = strings.ToUpper(level[0:1]) + strings.ToLower(level[1:])
   376  	}
   377  
   378  	l := newLabel("FeatureGate", string(featureGate))
   379  	l.extra = level
   380  	return l
   381  }
   382  
   383  // WithEnvironment specifies that a certain test or group of tests only works
   384  // in a certain environment. The return value must be passed as additional
   385  // argument to [framework.It], [framework.Describe], [framework.Context].
   386  //
   387  // The environment must be listed in ValidEnvironments.
   388  func WithEnvironment(name Environment) interface{} {
   389  	return withEnvironment(name)
   390  }
   391  
   392  // WithEnvironment is a shorthand for the corresponding package function.
   393  func (f *Framework) WithEnvironment(name Environment) interface{} {
   394  	return withEnvironment(name)
   395  }
   396  
   397  func withEnvironment(name Environment) interface{} {
   398  	if !ValidEnvironments.items.Has(name) {
   399  		RecordBug(NewBug(fmt.Sprintf("WithEnvironment: unknown environment %q", name), 2))
   400  	}
   401  	return newLabel("Environment", string(name))
   402  }
   403  
   404  // WithNodeFeature specifies that a certain test or group of tests only works
   405  // if the node supports a certain feature. The return value must be passed as
   406  // additional argument to [framework.It], [framework.Describe],
   407  // [framework.Context].
   408  //
   409  // The environment must be listed in ValidNodeFeatures.
   410  func WithNodeFeature(name NodeFeature) interface{} {
   411  	return withNodeFeature(name)
   412  }
   413  
   414  // WithNodeFeature is a shorthand for the corresponding package function.
   415  func (f *Framework) WithNodeFeature(name NodeFeature) interface{} {
   416  	return withNodeFeature(name)
   417  }
   418  
   419  func withNodeFeature(name NodeFeature) interface{} {
   420  	if !ValidNodeFeatures.items.Has(name) {
   421  		RecordBug(NewBug(fmt.Sprintf("WithNodeFeature: unknown environment %q", name), 2))
   422  	}
   423  	return newLabel("NodeFeature", string(name))
   424  }
   425  
   426  // WithConformace specifies that a certain test or group of tests must pass in
   427  // all conformant Kubernetes clusters. The return value must be passed as
   428  // additional argument to [framework.It], [framework.Describe],
   429  // [framework.Context].
   430  func WithConformance() interface{} {
   431  	return withConformance()
   432  }
   433  
   434  // WithConformance is a shorthand for the corresponding package function.
   435  func (f *Framework) WithConformance() interface{} {
   436  	return withConformance()
   437  }
   438  
   439  func withConformance() interface{} {
   440  	return newLabel("Conformance")
   441  }
   442  
   443  // WithNodeConformance specifies that a certain test or group of tests for node
   444  // functionality that does not depend on runtime or Kubernetes distro specific
   445  // behavior. The return value must be passed as additional argument to
   446  // [framework.It], [framework.Describe], [framework.Context].
   447  func WithNodeConformance() interface{} {
   448  	return withNodeConformance()
   449  }
   450  
   451  // WithNodeConformance is a shorthand for the corresponding package function.
   452  func (f *Framework) WithNodeConformance() interface{} {
   453  	return withNodeConformance()
   454  }
   455  
   456  func withNodeConformance() interface{} {
   457  	return newLabel("NodeConformance")
   458  }
   459  
   460  // WithDisruptive specifies that a certain test or group of tests temporarily
   461  // affects the functionality of the Kubernetes cluster. The return value must
   462  // be passed as additional argument to [framework.It], [framework.Describe],
   463  // [framework.Context].
   464  func WithDisruptive() interface{} {
   465  	return withDisruptive()
   466  }
   467  
   468  // WithDisruptive is a shorthand for the corresponding package function.
   469  func (f *Framework) WithDisruptive() interface{} {
   470  	return withDisruptive()
   471  }
   472  
   473  func withDisruptive() interface{} {
   474  	return newLabel("Disruptive")
   475  }
   476  
   477  // WithSerial specifies that a certain test or group of tests must not run in
   478  // parallel with other tests. The return value must be passed as additional
   479  // argument to [framework.It], [framework.Describe], [framework.Context].
   480  //
   481  // Starting with ginkgo v2, serial and parallel tests can be executed in the
   482  // same invocation. Ginkgo itself will ensure that the serial tests run
   483  // sequentially.
   484  func WithSerial() interface{} {
   485  	return withSerial()
   486  }
   487  
   488  // WithSerial is a shorthand for the corresponding package function.
   489  func (f *Framework) WithSerial() interface{} {
   490  	return withSerial()
   491  }
   492  
   493  func withSerial() interface{} {
   494  	return newLabel("Serial")
   495  }
   496  
   497  // WithSlow specifies that a certain test or group of tests must not run in
   498  // parallel with other tests. The return value must be passed as additional
   499  // argument to [framework.It], [framework.Describe], [framework.Context].
   500  func WithSlow() interface{} {
   501  	return withSlow()
   502  }
   503  
   504  // WithSlow is a shorthand for the corresponding package function.
   505  func (f *Framework) WithSlow() interface{} {
   506  	return WithSlow()
   507  }
   508  
   509  func withSlow() interface{} {
   510  	return newLabel("Slow")
   511  }
   512  
   513  // WithLabel is a wrapper around [ginkgo.Label]. Besides adding an arbitrary
   514  // label to a test, it also injects the label in square brackets into the test
   515  // name.
   516  func WithLabel(label string) interface{} {
   517  	return withLabel(label)
   518  }
   519  
   520  // WithLabel is a shorthand for the corresponding package function.
   521  func (f *Framework) WithLabel(label string) interface{} {
   522  	return withLabel(label)
   523  }
   524  
   525  func withLabel(label string) interface{} {
   526  	return newLabel(label)
   527  }
   528  
   529  type label struct {
   530  	// parts get concatenated with ":" to build the full label.
   531  	parts []string
   532  	// extra is an optional fully-formed extra label.
   533  	extra string
   534  }
   535  
   536  func newLabel(parts ...string) label {
   537  	return label{parts: parts}
   538  }
   539  
   540  // TagsEqual can be used to check whether two tags are the same.
   541  // It's safe to compare e.g. the result of WithSlow() against the result
   542  // of WithSerial(), the result will be false. False is also returned
   543  // when a parameter is some completely different value.
   544  func TagsEqual(a, b interface{}) bool {
   545  	al, ok := a.(label)
   546  	if !ok {
   547  		return false
   548  	}
   549  	bl, ok := b.(label)
   550  	if !ok {
   551  		return false
   552  	}
   553  	if al.extra != bl.extra {
   554  		return false
   555  	}
   556  	return slices.Equal(al.parts, bl.parts)
   557  }