Skip to content

Commit c1a5560

Browse files
authored
ci: E2E Framework [Core types] [1/6] (#2526)
add types
1 parent 9ff1440 commit c1a5560

File tree

7 files changed

+297
-0
lines changed

7 files changed

+297
-0
lines changed

test/e2e/Makefile

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.PHONY: generate
2+
generate:
3+
@go generate ./...
4+
5+
.PHONY: acndev
6+
acndev:
7+
mkdir -p ./bin
8+
go build -o ./bin/acndev ./cmd/

test/e2e/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ACN E2E
2+
3+
## Objectives
4+
- Steps are reusable
5+
- Steps parameters are saved to the context of the job
6+
- Once written to the job context, the values are immutable
7+
- Cluster resources used in code should be able to be generated to yaml for easy manual repro
8+
- Avoid shell/ps calls wherever possible and use go libraries for typed parameters (avoid capturing error codes/stderr/stdout)
9+
10+
---
11+
## Starter Example:
12+
13+
When authoring tests, make sure to prefix the test name with `TestE2E` so that it is skipped by existing pipeline unit test framework.
14+
For reference, see the `test-all` recipe in the root [Makefile](../../Makefile).
15+
16+
17+
For sample test, please check out:
18+
[the Hubble E2E.](./scenarios/hubble/index_test.go)
19+
20+
21+
## acndev CLI
22+
23+
The `acndev` CLI is a tool for manually interacting with E2E steps for quick access.
24+
25+
It is used to create and manage clusters, but **not** to author tests with, and should **not** be referenced in pipeline yaml. Please stick to using tests with `TestE2E` prefix for authoring tests.

test/e2e/framework/types/job.go

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package types
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"reflect"
7+
)
8+
9+
var (
10+
ErrEmptyDescription = fmt.Errorf("job description is empty")
11+
ErrNonNilError = fmt.Errorf("expected error to be non-nil")
12+
ErrNilError = fmt.Errorf("expected error to be nil")
13+
ErrMissingParameter = fmt.Errorf("missing parameter")
14+
ErrParameterAlreadySet = fmt.Errorf("parameter already set")
15+
)
16+
17+
type Job struct {
18+
Values *JobValues
19+
Description string
20+
Steps []*StepWrapper
21+
}
22+
23+
type StepWrapper struct {
24+
Step Step
25+
Opts *StepOptions
26+
}
27+
28+
func responseDivider(jobname string) {
29+
totalWidth := 100
30+
start := 20
31+
i := 0
32+
for ; i < start; i++ {
33+
fmt.Print("#")
34+
}
35+
mid := fmt.Sprintf(" %s ", jobname)
36+
fmt.Print(mid)
37+
for ; i < totalWidth-(start+len(mid)); i++ {
38+
fmt.Print("#")
39+
}
40+
fmt.Println()
41+
}
42+
43+
func NewJob(description string) *Job {
44+
return &Job{
45+
Values: &JobValues{
46+
kv: make(map[string]string),
47+
},
48+
Description: description,
49+
}
50+
}
51+
52+
func (j *Job) AddScenario(steps ...StepWrapper) {
53+
for _, step := range steps {
54+
j.AddStep(step.Step, step.Opts)
55+
}
56+
}
57+
58+
func (j *Job) AddStep(step Step, opts *StepOptions) {
59+
j.Steps = append(j.Steps, &StepWrapper{
60+
Step: step,
61+
Opts: opts,
62+
})
63+
}
64+
65+
func (j *Job) Run() error {
66+
if j.Description == "" {
67+
return ErrEmptyDescription
68+
}
69+
70+
err := j.Validate()
71+
if err != nil {
72+
return err // nolint:wrapcheck // don't wrap error, wouldn't provide any more context than the error itself
73+
}
74+
75+
for _, wrapper := range j.Steps {
76+
err := wrapper.Step.Prevalidate()
77+
if err != nil {
78+
return err //nolint:wrapcheck // don't wrap error, wouldn't provide any more context than the error itself
79+
}
80+
}
81+
82+
for _, wrapper := range j.Steps {
83+
responseDivider(reflect.TypeOf(wrapper.Step).Elem().Name())
84+
log.Printf("INFO: step options provided: %+v\n", wrapper.Opts)
85+
err := wrapper.Step.Run()
86+
if wrapper.Opts.ExpectError && err == nil {
87+
return fmt.Errorf("expected error from step %s but got nil: %w", reflect.TypeOf(wrapper.Step).Elem().Name(), ErrNilError)
88+
} else if !wrapper.Opts.ExpectError && err != nil {
89+
return fmt.Errorf("did not expect error from step %s but got error: %w", reflect.TypeOf(wrapper.Step).Elem().Name(), err)
90+
}
91+
}
92+
93+
for _, wrapper := range j.Steps {
94+
err := wrapper.Step.Postvalidate()
95+
if err != nil {
96+
return err //nolint:wrapcheck // don't wrap error, wouldn't provide any more context than the error itself
97+
}
98+
}
99+
return nil
100+
}
101+
102+
func (j *Job) Validate() error {
103+
for _, wrapper := range j.Steps {
104+
err := j.validateStep(wrapper)
105+
if err != nil {
106+
return err
107+
}
108+
}
109+
110+
return nil
111+
}
112+
113+
func (j *Job) validateStep(stepw *StepWrapper) error {
114+
stepName := reflect.TypeOf(stepw.Step).Elem().Name()
115+
val := reflect.ValueOf(stepw.Step).Elem()
116+
117+
// set default options if none are provided
118+
if stepw.Opts == nil {
119+
stepw.Opts = &DefaultOpts
120+
}
121+
122+
for i, f := range reflect.VisibleFields(val.Type()) {
123+
124+
// skip saving unexported fields
125+
if !f.IsExported() {
126+
continue
127+
}
128+
129+
k := reflect.Indirect(val.Field(i)).Kind()
130+
131+
if k == reflect.String {
132+
parameter := val.Type().Field(i).Name
133+
value := val.Field(i).Interface().(string)
134+
storedValue := j.Values.Get(parameter)
135+
136+
if storedValue == "" {
137+
if value != "" {
138+
if stepw.Opts.SaveParametersToJob {
139+
fmt.Printf("%s setting parameter %s in job context to %s\n", stepName, parameter, value)
140+
j.Values.Set(parameter, value)
141+
}
142+
continue
143+
}
144+
return fmt.Errorf("missing parameter %s for step %s: %w", parameter, stepName, ErrMissingParameter)
145+
146+
}
147+
148+
if value != "" {
149+
return fmt.Errorf("parameter %s for step %s is already set from previous step: %w", parameter, stepName, ErrParameterAlreadySet)
150+
}
151+
152+
// don't use log format since this is technically preexecution and easier to read
153+
fmt.Println(stepName, "using previously stored value for parameter", parameter, "set as", j.Values.Get(parameter))
154+
val.Field(i).SetString(storedValue)
155+
}
156+
}
157+
158+
return nil
159+
}

test/e2e/framework/types/jobvalues.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package types
2+
3+
import "sync"
4+
5+
type JobValues struct {
6+
RWLock sync.RWMutex
7+
kv map[string]string
8+
}
9+
10+
func (j *JobValues) New() *JobValues {
11+
return &JobValues{
12+
kv: make(map[string]string),
13+
}
14+
}
15+
16+
func (j *JobValues) Contains(key string) bool {
17+
j.RWLock.RLock()
18+
defer j.RWLock.RUnlock()
19+
_, ok := j.kv[key]
20+
return ok
21+
}
22+
23+
func (j *JobValues) Get(key string) string {
24+
j.RWLock.RLock()
25+
defer j.RWLock.RUnlock()
26+
return j.kv[key]
27+
}
28+
29+
func (j *JobValues) Set(key, value string) {
30+
j.RWLock.Lock()
31+
defer j.RWLock.Unlock()
32+
j.kv[key] = value
33+
}

test/e2e/framework/types/runner.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package types
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
type Runner struct {
10+
t *testing.T
11+
Job *Job
12+
}
13+
14+
func NewRunner(t *testing.T, job *Job) *Runner {
15+
return &Runner{
16+
t: t,
17+
Job: job,
18+
}
19+
}
20+
21+
func (r *Runner) Run() {
22+
if r.t.Failed() {
23+
return
24+
}
25+
require.NoError(r.t, r.Job.Run())
26+
}

test/e2e/framework/types/step.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package types
2+
3+
var DefaultOpts = StepOptions{
4+
ExpectError: false,
5+
SaveParametersToJob: true,
6+
}
7+
8+
type Step interface {
9+
Prevalidate() error
10+
Run() error
11+
Postvalidate() error
12+
}
13+
14+
type StepOptions struct {
15+
ExpectError bool
16+
17+
// Generally set this to false when you want to reuse
18+
// a step, but you don't want to save the parameters
19+
// ex: Sleep for 15 seconds, then Sleep for 10 seconds,
20+
// you don't want to save the parameters
21+
SaveParametersToJob bool
22+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package types
2+
3+
import (
4+
"log"
5+
"time"
6+
)
7+
8+
type Sleep struct {
9+
Duration time.Duration
10+
}
11+
12+
func (c *Sleep) Run() error {
13+
log.Printf("sleeping for %s...\n", c.Duration)
14+
time.Sleep(c.Duration)
15+
return nil
16+
}
17+
18+
func (c *Sleep) Prevalidate() error {
19+
return nil
20+
}
21+
22+
func (c *Sleep) Postvalidate() error {
23+
return nil
24+
}

0 commit comments

Comments
 (0)