...

Source file src/edge-infra.dev/test/framework/framework.go

Documentation: edge-infra.dev/test/framework

     1  // Package framework provides a bare-bones base framework that is intended to be
     2  // embedded by other framework implementations (see test/{integration,e2e}/framework)
     3  package framework
     4  
     5  import (
     6  	"fmt"
     7  	"math/rand"
     8  	"os"
     9  
    10  	"github.com/stretchr/testify/suite"
    11  
    12  	"edge-infra.dev/pkg/lib/build/bazel"
    13  	"edge-infra.dev/test/framework/skipper"
    14  )
    15  
    16  // Ensures that we are executing tests from the current working directory.
    17  func init() {
    18  	// determine current working directory
    19  	cwd := bazel.ResolveWdOrDie()
    20  	if err := os.Chdir(cwd); err != nil {
    21  		panic(err)
    22  	}
    23  }
    24  
    25  // Step is a test lifecycle step function that is passed the Freamwork instance.
    26  // The framework can be extended by implementing the `SubFramework` interface,
    27  // allowing life cycle steps to be registered with the framework via `Register`.
    28  // A typical use case for a Step function is setting up randomized values based on
    29  // framework.BaseName, framework.UniqueName, and taking advantage of
    30  // framework.Assertions (e.g., `framework.True()`, `framework.NoError()`)
    31  type Step func(*Framework)
    32  
    33  // Framework represents the internal state of the test runner and provides
    34  // utilities for tests that leverage the global test context (defined in context.go
    35  // and populated via flags.go as well as test/framework/config).
    36  //
    37  // The test context is not embedded in the Framework to avoid requiring a global
    38  // framework: each suite or describe block can initialize its own framework
    39  // without worrying about binding additional flags or causing the configuration
    40  // to be re-parsed.
    41  type Framework struct {
    42  	suite.Suite
    43  	// A distinct name for the test(s) the framework will be running.  It should be
    44  	// descriptive enough to set it apart from other framework instances.
    45  	BaseName string
    46  	// A randomly generated unique name for this specific instance, based on the
    47  	// BaseName.  This allows suites and specs to be ran in parallel (even the same
    48  	// one).
    49  	UniqueName string
    50  	// Labels that are used to filter test execution.  They should describe the tests
    51  	// being defined both in behavior (e.g., disruptive, slow) and what (e.g.,
    52  	// component name).
    53  	// TODO(aw185176): add a link to some doc here when we have one and a set of
    54  	//   	             common labels
    55  	labels map[string]string
    56  
    57  	beforeEachFns []Step
    58  	afterEachFns  []Step
    59  	setupFns      []Step
    60  	teardownFns   []Step
    61  }
    62  
    63  // New creates a base framework using a base name provided (typically this
    64  // is unique per describe block or suite).  It instantiates a logger using the
    65  // io.Writer that Ginkgo provides and registers universal BeforeEach hooks for
    66  // each spec.
    67  func New(baseName string) *Framework {
    68  	f := &Framework{
    69  		BaseName: baseName,
    70  		labels:   map[string]string{},
    71  	}
    72  
    73  	return f
    74  }
    75  
    76  // BeforeEach adds a function to the framework instance that is executed before
    77  // each test in a suite, similar to ginkgo.BeforeEach.  The functions are passed
    78  // a copy of the framework so that they can set up randomized values based on
    79  // framework.BaseName, framework.UniqueName, and use framework methods
    80  func (f *Framework) BeforeEachTest(fns ...Step) *Framework {
    81  	if f.beforeEachFns == nil {
    82  		f.beforeEachFns = []Step{}
    83  	}
    84  	f.beforeEachFns = append(f.beforeEachFns, fns...)
    85  
    86  	return f
    87  }
    88  
    89  // AfterEach adds a function to the framework instance that is executed after
    90  // each test in a suite, similar to ginkgo.AfterEach.
    91  func (f *Framework) AfterEachTest(fns ...Step) *Framework {
    92  	if f.afterEachFns == nil {
    93  		f.afterEachFns = []Step{}
    94  	}
    95  	f.afterEachFns = append(f.afterEachFns, fns...)
    96  
    97  	return f
    98  }
    99  
   100  // Setup adds a lifecycle function that is ran once, before the suite is executed.
   101  // Similar to `ginkgo.BeforeSuite`.
   102  func (f *Framework) Setup(fns ...Step) *Framework {
   103  	if f.setupFns == nil {
   104  		f.setupFns = []Step{}
   105  	}
   106  	f.setupFns = append(f.setupFns, fns...)
   107  
   108  	return f
   109  }
   110  
   111  // Teardown adds a lifecycle function that is ran once, after the suite is executed.
   112  // Similar to `ginkgo.AfterSuite`
   113  func (f *Framework) Teardown(fns ...Step) *Framework {
   114  	if f.teardownFns == nil {
   115  		f.teardownFns = []Step{}
   116  	}
   117  	f.teardownFns = append(f.teardownFns, fns...)
   118  
   119  	return f
   120  }
   121  
   122  // SubFramework
   123  type SubFramework interface {
   124  	SetupWithFramework(*Framework)
   125  }
   126  
   127  // Register calls `SetupWithFramework()`, allowing a `SubFramework` to add lifecycle
   128  // steps (e.g., BeforeEachTest), labels, or other modifications to the registering
   129  // `Framework` instance. This is the main way for additional framework components
   130  // to integrate lifecycle hooks for a specific suite automatically.
   131  func (f *Framework) Register(subs ...SubFramework) *Framework {
   132  	for _, s := range subs {
   133  		s.SetupWithFramework(f)
   134  	}
   135  
   136  	return f
   137  }
   138  
   139  // Log is syntactic sugar for framework.T().Log, so that it can be used naturally
   140  // in test suites.
   141  func (f *Framework) Log(args ...interface{}) {
   142  	f.T().Log(args...)
   143  }
   144  
   145  // Logf is syntactic sugar for framework.T().Logf, so that it can be used naturally
   146  // in test suites.
   147  func (f *Framework) Logf(format string, args ...interface{}) {
   148  	f.T().Logf(format, args...)
   149  }
   150  
   151  // Skip is syntactic sugar for framework.T().Skip, with an extra parameter to
   152  // make it easier to create contextual skip messages with a uniform format, e.g.,
   153  // [Context] skipping: Message
   154  // [KonfigKonnector] skipping: misisng key.json for non-GKE runtime
   155  // f.Skip("KonfigKonnector", "missing key.json for non-GKE runtime")
   156  func (f *Framework) Skip(ctx string, msg string) {
   157  	f.T().Skipf("[%s] skipping: %s", ctx, msg)
   158  }
   159  
   160  // SkipBasedOnLabels will conditionally call t.Skip() if the test suite should be
   161  // skipped based on the -labels and -skip-labels flags, by calling
   162  // skipper.SkipBasedOnLabel.
   163  func (f *Framework) SkipBasedOnLabels(labels map[string]string) {
   164  	skip, msg := skipper.SkipBasedOnLabels(labels, Context.Labels, Context.SkipLabels)
   165  	if skip {
   166  		f.T().Skip(msg)
   167  	}
   168  }
   169  
   170  // SetupTest reasserts its own internal state or creates new randomized values
   171  // for each test spec to ensure that we can run tests (even the same tests)
   172  // in parallel repeatedly and handles skipping by labels.
   173  //
   174  // SetupTest is the function that testify/suite executes before each test in a
   175  // suite: https://pkg.go.dev/github.com/stretchr/testify/suite#SetupTestSuite
   176  func (f *Framework) SetupTest() {
   177  	// check if labels for this test are eligible for execution based on the
   178  	// -labels or -skip-labels flags
   179  	f.SkipBasedOnLabels(f.labels)
   180  
   181  	for _, fn := range f.beforeEachFns {
   182  		fn(f)
   183  	}
   184  }
   185  
   186  // TearDownTest runs the added AfterEachTest functions after each test in the suite.
   187  //
   188  // TearDownTest is the function that testify/suite executes after each test in a
   189  // suite: https://pkg.go.dev/github.com/stretchr/testify/suite#TearDownTestSuite
   190  func (f *Framework) TearDownTest() {
   191  	for _, fn := range f.afterEachFns {
   192  		fn(f)
   193  	}
   194  }
   195  
   196  // SetupSuite runs the added Setup functions once per suite.  This is when
   197  // framework.UniqueName is populated.
   198  //
   199  // It is the function that testify/suite executes to drive the suite:
   200  // https://pkg.go.dev/github.com/stretchr/testify/suite#SetupAllSuite
   201  func (f *Framework) SetupSuite() {
   202  	// not guaranteed to be unique, but very likely
   203  	// #nosec G404 - random string is cosmetic
   204  	f.UniqueName = fmt.Sprintf("%s-%08x", f.BaseName, rand.Int31())
   205  
   206  	for _, fn := range f.setupFns {
   207  		fn(f)
   208  	}
   209  }
   210  
   211  // TearDownSuite runs the added Teardown functions once per suite.
   212  //
   213  // It is the function that testify/suite executes to drive the suite:
   214  // https://pkg.go.dev/github.com/stretchr/testify/suite#TearDownAllSuite
   215  func (f *Framework) TearDownSuite() {
   216  	for _, fn := range f.teardownFns {
   217  		fn(f)
   218  	}
   219  }
   220  
   221  // RandGKEName generates a max length (40) random string that conform to GKE cluster naming requirements.
   222  // It appends random characters to a base name, e.g. "my-gke-cluster-{24 character random string}". Base name
   223  // must start with a lowercase letter and has a max length of 38 characters. UTF details are blissfully ignored.
   224  func RandGKEName(basename string) (string, error) {
   225  	if len(basename) > 38 {
   226  		return "", fmt.Errorf("base name '%s' exceeds max length of 38", basename)
   227  	}
   228  	var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
   229  	n := 39 - len(basename) // leave room for a hyphen
   230  	genName := make([]rune, n)
   231  	for i := range genName {
   232  		genName[i] = runes[rand.Intn(len(runes))] // #nosec G404 - random string is cosmetic
   233  	}
   234  	return fmt.Sprintf("%s-%s", basename, string(genName)), nil
   235  }
   236  

View as plain text