package f2 import ( "strings" ) type Feature interface { Name() string Labels() map[string]string Steps() []Step } type feature struct { name string labels map[string]string steps []Step } type Step struct { Name string Fn StepFn Phase Phase } func (f *feature) Name() string { return f.name } func (f *feature) Labels() map[string]string { return f.labels } func (f *feature) Steps() []Step { return f.steps } // FeatureBuilder allows concise test definition with the builder pattern without // increasing the surface area of the [Feature] interface type FeatureBuilder struct { f *feature } func NewFeature(name string) *FeatureBuilder { return &FeatureBuilder{f: &feature{ name: name, steps: make([]Step, 0), labels: make(map[string]string), }} } func (b *FeatureBuilder) Test(name string, t StepFn) *FeatureBuilder { b.f.steps = append(b.f.steps, Step{name, t, phaseTest}) return b } func (b *FeatureBuilder) Setup(name string, fn StepFn) *FeatureBuilder { b.f.steps = append(b.f.steps, Step{name, fn, phaseBeforeFeature}) return b } func (b *FeatureBuilder) Teardown(name string, fn StepFn) *FeatureBuilder { b.f.steps = append(b.f.steps, Step{name, fn, phaseAfterFeature}) return b } func (b *FeatureBuilder) WithStep(name string, p Phase, fn StepFn) *FeatureBuilder { b.f.steps = append(b.f.steps, Step{name, fn, p}) return b } func (b *FeatureBuilder) Feature() Feature { return b.f } func getStepsInPhase(f Feature, Phase Phase) []Step { if f.Steps() == nil { return nil } result := make([]Step, 0) for _, s := range f.Steps() { if s.Phase == Phase { result = append(result, s) } } return result } // deepCopyFeature just copies the values from the Feature but creates a deep // copy to avoid mutation when we just want an informational copy. func deepCopyFeature(f Feature) Feature { fcopy := NewFeature(f.Name()) for k, v := range f.Labels() { fcopy = fcopy.WithLabel(k, v) } for _, step := range f.Steps() { fcopy = fcopy.WithStep(step.Name, step.Phase, nil) } return fcopy.Feature() } // you can find a list of labels here: // https://docs.edge-infra.dev/dev/contributing/testing/conventions/ // WithLabel adds a label with one or more values to the framework to describe the block of tests // the framework instance will be used for. Returns a pointer to itself so it // can be chained together to add multiple labels: // // fin := f2.NewFeature("hello feature").WithLabel("bar", "boo", "baz", "boz").Flaky().Feature() func (b *FeatureBuilder) WithLabel(key string, values ...string) *FeatureBuilder { if b.f.labels == nil { b.f.labels = map[string]string{} } b.f.labels[key] = commaSepList(values...) return b } // WithID is a label used to associate Tests with specific user-facing features. // Should be identified by unique identifier (GitHub issue number or JIRA ticket) // Can have multiple values, separated by comma. func (b *FeatureBuilder) WithID(f ...string) *FeatureBuilder { return b.WithLabel("id", f...) } // Priviledged is a label that defines tests that require escalted privileges in various contexts. // Examples: FolderAdmin, BillingAdmin, ProjectAdmin describe GCP privileges that tests may need. func (b *FeatureBuilder) Priviledged(p ...string) *FeatureBuilder { return b.WithLabel("priviledged", p...) } // Component is syntactic sugar for adding a component label to the test block. // Returns a pointer to itself for function chaining. func (b *FeatureBuilder) Component(c ...string) *FeatureBuilder { return b.WithLabel("component", c...) } // Slow is a label that should be applied to all test suites which take longer // than 5 minutes to execute. func (b *FeatureBuilder) Slow() *FeatureBuilder { return b.WithLabel("slow", "true") } // Disruptive is a label that should be applied to all test suites that are // likely to disrupt other test cases being ran at the same time. func (b *FeatureBuilder) Disruptive() *FeatureBuilder { return b.WithLabel("disruptive", "true") } // Serial is a label that should be applied to all test suites that can't be // run in parallel. func (b *FeatureBuilder) Serial() *FeatureBuilder { return b.WithLabel("serial", "true") } // Flaky is a label that should be applied to tests with inconsistent results. func (b *FeatureBuilder) Flaky() *FeatureBuilder { return b.WithLabel("flaky", "true") } // commaSepList takes in a list of strings and returns a comma separated string // // ex: [a b c d e] --> a,b,c,d,e func commaSepList(list ...string) string { var values strings.Builder for i, x := range list { if i == 0 { values.WriteString(x) } else { values.WriteString("," + x) } } return values.String() }