// Package framework provides a bare-bones base framework that is intended to be // embedded by other framework implementations (see test/{integration,e2e}/framework) package framework import ( "fmt" "math/rand" "os" "github.com/stretchr/testify/suite" "edge-infra.dev/pkg/lib/build/bazel" "edge-infra.dev/test/framework/skipper" ) // Ensures that we are executing tests from the current working directory. func init() { // determine current working directory cwd := bazel.ResolveWdOrDie() if err := os.Chdir(cwd); err != nil { panic(err) } } // Step is a test lifecycle step function that is passed the Freamwork instance. // The framework can be extended by implementing the `SubFramework` interface, // allowing life cycle steps to be registered with the framework via `Register`. // A typical use case for a Step function is setting up randomized values based on // framework.BaseName, framework.UniqueName, and taking advantage of // framework.Assertions (e.g., `framework.True()`, `framework.NoError()`) type Step func(*Framework) // Framework represents the internal state of the test runner and provides // utilities for tests that leverage the global test context (defined in context.go // and populated via flags.go as well as test/framework/config). // // The test context is not embedded in the Framework to avoid requiring a global // framework: each suite or describe block can initialize its own framework // without worrying about binding additional flags or causing the configuration // to be re-parsed. type Framework struct { suite.Suite // A distinct name for the test(s) the framework will be running. It should be // descriptive enough to set it apart from other framework instances. BaseName string // A randomly generated unique name for this specific instance, based on the // BaseName. This allows suites and specs to be ran in parallel (even the same // one). UniqueName string // Labels that are used to filter test execution. They should describe the tests // being defined both in behavior (e.g., disruptive, slow) and what (e.g., // component name). // TODO(aw185176): add a link to some doc here when we have one and a set of // common labels labels map[string]string beforeEachFns []Step afterEachFns []Step setupFns []Step teardownFns []Step } // New creates a base framework using a base name provided (typically this // is unique per describe block or suite). It instantiates a logger using the // io.Writer that Ginkgo provides and registers universal BeforeEach hooks for // each spec. func New(baseName string) *Framework { f := &Framework{ BaseName: baseName, labels: map[string]string{}, } return f } // BeforeEach adds a function to the framework instance that is executed before // each test in a suite, similar to ginkgo.BeforeEach. The functions are passed // a copy of the framework so that they can set up randomized values based on // framework.BaseName, framework.UniqueName, and use framework methods func (f *Framework) BeforeEachTest(fns ...Step) *Framework { if f.beforeEachFns == nil { f.beforeEachFns = []Step{} } f.beforeEachFns = append(f.beforeEachFns, fns...) return f } // AfterEach adds a function to the framework instance that is executed after // each test in a suite, similar to ginkgo.AfterEach. func (f *Framework) AfterEachTest(fns ...Step) *Framework { if f.afterEachFns == nil { f.afterEachFns = []Step{} } f.afterEachFns = append(f.afterEachFns, fns...) return f } // Setup adds a lifecycle function that is ran once, before the suite is executed. // Similar to `ginkgo.BeforeSuite`. func (f *Framework) Setup(fns ...Step) *Framework { if f.setupFns == nil { f.setupFns = []Step{} } f.setupFns = append(f.setupFns, fns...) return f } // Teardown adds a lifecycle function that is ran once, after the suite is executed. // Similar to `ginkgo.AfterSuite` func (f *Framework) Teardown(fns ...Step) *Framework { if f.teardownFns == nil { f.teardownFns = []Step{} } f.teardownFns = append(f.teardownFns, fns...) return f } // SubFramework type SubFramework interface { SetupWithFramework(*Framework) } // Register calls `SetupWithFramework()`, allowing a `SubFramework` to add lifecycle // steps (e.g., BeforeEachTest), labels, or other modifications to the registering // `Framework` instance. This is the main way for additional framework components // to integrate lifecycle hooks for a specific suite automatically. func (f *Framework) Register(subs ...SubFramework) *Framework { for _, s := range subs { s.SetupWithFramework(f) } return f } // Log is syntactic sugar for framework.T().Log, so that it can be used naturally // in test suites. func (f *Framework) Log(args ...interface{}) { f.T().Log(args...) } // Logf is syntactic sugar for framework.T().Logf, so that it can be used naturally // in test suites. func (f *Framework) Logf(format string, args ...interface{}) { f.T().Logf(format, args...) } // Skip is syntactic sugar for framework.T().Skip, with an extra parameter to // make it easier to create contextual skip messages with a uniform format, e.g., // [Context] skipping: Message // [KonfigKonnector] skipping: misisng key.json for non-GKE runtime // f.Skip("KonfigKonnector", "missing key.json for non-GKE runtime") func (f *Framework) Skip(ctx string, msg string) { f.T().Skipf("[%s] skipping: %s", ctx, msg) } // SkipBasedOnLabels will conditionally call t.Skip() if the test suite should be // skipped based on the -labels and -skip-labels flags, by calling // skipper.SkipBasedOnLabel. func (f *Framework) SkipBasedOnLabels(labels map[string]string) { skip, msg := skipper.SkipBasedOnLabels(labels, Context.Labels, Context.SkipLabels) if skip { f.T().Skip(msg) } } // SetupTest reasserts its own internal state or creates new randomized values // for each test spec to ensure that we can run tests (even the same tests) // in parallel repeatedly and handles skipping by labels. // // SetupTest is the function that testify/suite executes before each test in a // suite: https://pkg.go.dev/github.com/stretchr/testify/suite#SetupTestSuite func (f *Framework) SetupTest() { // check if labels for this test are eligible for execution based on the // -labels or -skip-labels flags f.SkipBasedOnLabels(f.labels) for _, fn := range f.beforeEachFns { fn(f) } } // TearDownTest runs the added AfterEachTest functions after each test in the suite. // // TearDownTest is the function that testify/suite executes after each test in a // suite: https://pkg.go.dev/github.com/stretchr/testify/suite#TearDownTestSuite func (f *Framework) TearDownTest() { for _, fn := range f.afterEachFns { fn(f) } } // SetupSuite runs the added Setup functions once per suite. This is when // framework.UniqueName is populated. // // It is the function that testify/suite executes to drive the suite: // https://pkg.go.dev/github.com/stretchr/testify/suite#SetupAllSuite func (f *Framework) SetupSuite() { // not guaranteed to be unique, but very likely // #nosec G404 - random string is cosmetic f.UniqueName = fmt.Sprintf("%s-%08x", f.BaseName, rand.Int31()) for _, fn := range f.setupFns { fn(f) } } // TearDownSuite runs the added Teardown functions once per suite. // // It is the function that testify/suite executes to drive the suite: // https://pkg.go.dev/github.com/stretchr/testify/suite#TearDownAllSuite func (f *Framework) TearDownSuite() { for _, fn := range f.teardownFns { fn(f) } } // RandGKEName generates a max length (40) random string that conform to GKE cluster naming requirements. // It appends random characters to a base name, e.g. "my-gke-cluster-{24 character random string}". Base name // must start with a lowercase letter and has a max length of 38 characters. UTF details are blissfully ignored. func RandGKEName(basename string) (string, error) { if len(basename) > 38 { return "", fmt.Errorf("base name '%s' exceeds max length of 38", basename) } var runes = []rune("abcdefghijklmnopqrstuvwxyz0123456789") n := 39 - len(basename) // leave room for a hyphen genName := make([]rune, n) for i := range genName { genName[i] = runes[rand.Intn(len(runes))] // #nosec G404 - random string is cosmetic } return fmt.Sprintf("%s-%s", basename, string(genName)), nil }