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