Skip to content

Commit b69e158

Browse files
GIT-112: enable framework specific fail-fast mode
1 parent 200c30b commit b69e158

File tree

6 files changed

+272
-0
lines changed

6 files changed

+272
-0
lines changed

docs/design/fail-fast-mode.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Fail Fast Mode
2+
3+
When writing test feature using `e2e-framework` it is possible that you can write test assessments grouped under the same feature that are dependent on each other.
4+
This is possible as the framework ensures that the assessments are processed in sequence of how they are registered. This key behavior bring up a need to be able to
5+
optionally terminate the Feature(s) under test when a specific assessment fails.
6+
7+
## Why not use `test.failfast`?
8+
9+
`go test` provides a handy way to terminate test execution of first sign of failure via the `-failfast` argument.
10+
However, this terminates the entire test suite in question.
11+
12+
Such termination of the suite is not desirable for the framework as the rest of the Tests can still be processed
13+
in case if an assessment in one test fails. This bring in the need to introduce a framework specific `fail-fast`
14+
mode that can perform the following.
15+
16+
1. It should Terminate the feature(s) under test and mark the test as failure
17+
2. Skip the Teardown workflow of the feature(s) under test to enable easy debug
18+
19+
## Framework specific `--fail-fast` Mode
20+
21+
`e2e-framework` introduces a new CLI argument flag that can be invoked while triggering the test to achieve the
22+
fail-fast behavior built into the framework.
23+
24+
There are certain caveats to how this feature works.
25+
26+
1. The `fail-fast` mode doesn't work in conjunction with the `parallel` test mode
27+
2. Test developers have to explicitly invoke the `t.Fail()` or `t.FailNow()` handlers in the assessment to inform
28+
the framework that the fail-fast mode needs to be triggered
29+
30+
## Example Assessment
31+
Below section shows a simple example of how the feature can be leveraged in the assessment. This should be combined with `--fail-fast` argument while invoking the test to leverage the full feature.
32+
33+
```go
34+
func TestFeatureOne(t *testing.T) {
35+
featureOne := features.New("feature-one").
36+
Assess("this fails", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
37+
if 1 != 2 {
38+
t.Log("1 != 2")
39+
t.FailNow() // mark test case as failed here, don't continue execution
40+
} else {
41+
t.Log("1 == 2")
42+
}
43+
return ctx
44+
}).
45+
Teardown(func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
46+
t.Log("This teardown should not be invoked")
47+
return ctx
48+
}).
49+
Feature()
50+
testenv.Test(t, failFeature, nextFeature)
51+
}
52+
```

examples/fail_fast/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Fail Fast Mode
2+
3+
There are times in your test infra that you want the rest of your feature assessments to fail in
4+
case if one of the current executing assessments fail.
5+
This can aid in getting a better turn around time especially in cases where your assessments are
6+
inter-related and a failed assessment in step 1 can mean that running the rest of the assessment
7+
is guaranteed to fail. This can be done with the help of a `--fail-fast` flag provided at the
8+
framework level.
9+
10+
This works similar to how the `-failfast` mode of the `go test` works but provides the same
11+
feature at the context of the `e2e-framework`.
12+
13+
# How to Use this feature ?
14+
15+
1. Invoke the tests using `--fail-fast` argument
16+
2. Test developers should make sure they invoke either `t.Fail()` or `t.FailNow()` from the assessment to make sure the
17+
additional handlers kick in to stop the test execution of the feature in question where the assessment has failed
18+
19+
20+
When the framework specific `--fail-fast` mode is used, this works as follows:
21+
22+
1. It stops the rest of the assessments from getting executed for the feature under test
23+
2. This stops the next feature from getting executed in case if the feature under test fails as per step 1.
24+
3. Marks the feature and test associated with it as Failure.
25+
4. Skips the teardown sequence to make sure it is easier to debug the test failure
26+
27+
> Current limitation is that this can't be combined with the `--parallel` switch
28+
29+
Since this can lead to a test failure, we have just documented an example of this. Thanks to @divmadan for the example.
30+
31+
```go
32+
// main_test.go
33+
package example
34+
35+
import (
36+
"log"
37+
"os"
38+
"path/filepath"
39+
"testing"
40+
41+
"sigs.k8s.io/e2e-framework/pkg/env"
42+
)
43+
44+
var testenv env.Environment
45+
46+
func TestMain(m *testing.M) {
47+
cfg, _ := envconf.NewFromFlags()
48+
testenv = env.NewWithConfig(cfg)
49+
50+
os.Exit(testenv.Run(m))
51+
}
52+
```
53+
54+
```go
55+
// example_test.go
56+
package example
57+
58+
import (
59+
"context"
60+
"testing"
61+
62+
"sigs.k8s.io/e2e-framework/pkg/envconf"
63+
"sigs.k8s.io/e2e-framework/pkg/features"
64+
)
65+
66+
func TestExample(t *testing.T) {
67+
failFeature := features.New("fail-feature").
68+
Assess("1==2", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
69+
if 1 != 2 {
70+
t.Log("1 != 2")
71+
t.FailNow() // mark test case as failed here, don't continue execution
72+
} else {
73+
t.Log("1 == 2")
74+
}
75+
return ctx
76+
}).
77+
Assess("print", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
78+
t.Log("THIS LINE SHOULDN'T BE PRINTED")
79+
return ctx
80+
}).
81+
Teardown(func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
82+
t.Log("This teardown should not be invoked")
83+
return ctx
84+
}).
85+
Feature()
86+
87+
nextFeature := features.New("next-feature").
88+
Assess("print", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
89+
t.Log("THIS LINE ALSO SHOULDN'T BE PRINTED")
90+
return ctx
91+
}).
92+
Feature()
93+
94+
testenv.Test(t, failFeature, nextFeature)
95+
}
96+
97+
// even if the previous testcase fails, execute this testcase
98+
func TestNext(t *testing.T) {
99+
nextFeature := features.New("next-test-feature").
100+
Assess("print", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
101+
t.Log("THIS LINE SHOULD BE PRINTED")
102+
return ctx
103+
}).
104+
Teardown(func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
105+
t.Log("This teardown should be invoked")
106+
return ctx
107+
}).
108+
Feature()
109+
110+
testenv.Test(t, nextFeature)
111+
}
112+
```
113+
114+
When run this using `--fail-fast` you get the following behavior.
115+
116+
```bash
117+
❯ go test . -test.v -args --fail-fast
118+
119+
=== RUN TestExample
120+
=== RUN TestExample/fail-feature
121+
=== RUN TestExample/fail-feature/1==2
122+
example_test.go:15: 1 != 2
123+
--- FAIL: TestExample (0.00s)
124+
--- FAIL: TestExample/fail-feature (0.00s)
125+
--- FAIL: TestExample/fail-feature/1==2 (0.00s)
126+
=== RUN TestNext
127+
=== RUN TestNext/next-test-feature
128+
=== RUN TestNext/next-test-feature/print
129+
example_test.go:42: THIS LINE SHOULD BE PRINTED
130+
--- PASS: TestNext (0.00s)
131+
--- PASS: TestNext/next-test-feature (0.00s)
132+
--- PASS: TestNext/next-test-feature/print (0.00s)
133+
FAIL
134+
```

pkg/env/env.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ func (e *testEnv) processTests(t *testing.T, enableParallelRun bool, testFeature
260260
}(&wg, featName, featureCopy)
261261
} else {
262262
e.processTestFeature(t, featName, featureCopy)
263+
// In case if the feature under test has failed, skip reset of the features
264+
// that are part of the same test
265+
if e.cfg.FailFast() && t.Failed() {
266+
break
267+
}
263268
}
264269
}
265270
if runInParallel {
@@ -417,6 +422,7 @@ func (e *testEnv) execFeature(ctx context.Context, t *testing.T, featName string
417422
// assessments run as feature/assessment sub level
418423
assessments := features.GetStepsByLevel(f.Steps(), types.LevelAssess)
419424

425+
failed := false
420426
for i, assess := range assessments {
421427
assessName := assess.Name()
422428
if assessName == "" {
@@ -429,6 +435,20 @@ func (e *testEnv) execFeature(ctx context.Context, t *testing.T, featName string
429435
}
430436
ctx = e.executeSteps(ctx, t, []types.Step{assess})
431437
})
438+
// Check if the Test assessment under question performed a `t.Fail()` or `t.Failed()` invocation.
439+
// We need to track that and stop the next set of assessment in the feature under test from getting
440+
// executed
441+
if e.cfg.FailFast() && t.Failed() {
442+
failed = true
443+
break
444+
}
445+
}
446+
447+
// Let us fail the test fast and not run the teardown in case if the framework specific fail-fast mode is
448+
// invoked to make sure we leave the traces of the failed test behind to enable better debugging for the
449+
// test developers
450+
if e.cfg.FailFast() && failed {
451+
t.FailNow()
432452
}
433453

434454
// teardowns run at feature-level

pkg/envconf/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Config struct {
4242
skipAssessmentRegex *regexp.Regexp
4343
parallelTests bool
4444
dryRun bool
45+
failFast bool
4546
}
4647

4748
// New creates and initializes an empty environment configuration
@@ -81,6 +82,7 @@ func NewFromFlags() (*Config, error) {
8182
e.skipLabels = envFlags.SkipLabels()
8283
e.parallelTests = envFlags.Parallel()
8384
e.dryRun = envFlags.DryRun()
85+
e.failFast = envFlags.FailFast()
8486

8587
return e, nil
8688
}
@@ -220,11 +222,15 @@ func (c *Config) SkipLabels() map[string]string {
220222
return c.skipLabels
221223
}
222224

225+
// WithParallelTestEnabled can be used to enable parallel run of the test
226+
// features
223227
func (c *Config) WithParallelTestEnabled() *Config {
224228
c.parallelTests = true
225229
return c
226230
}
227231

232+
// ParallelTestEnabled indicates if the test features are being run in
233+
// parallel or not
228234
func (c *Config) ParallelTestEnabled() bool {
229235
return c.parallelTests
230236
}
@@ -238,6 +244,21 @@ func (c *Config) DryRunMode() bool {
238244
return c.dryRun
239245
}
240246

247+
// WithFailFast can be used to enable framework specific fail fast mode
248+
// that controls the test execution of the features and assessments under
249+
// test
250+
func (c *Config) WithFailFast() *Config {
251+
c.failFast = true
252+
return c
253+
}
254+
255+
// FailFast indicate if the framework is running in fail fast mode. This
256+
// controls the behavior of how the assessments and features are handled
257+
// if a test encounters a failure result
258+
func (c *Config) FailFast() bool {
259+
return c.failFast
260+
}
261+
241262
func randNS() string {
242263
return RandomName("testns-", 32)
243264
}

pkg/envconf/config_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,15 @@ func TestConfig_New_WithDryRun(t *testing.T) {
6161
t.Errorf("expected dryRun mode to be enabled with invoked with --dry-run arguments")
6262
}
6363
}
64+
65+
func TestConfig_New_WithFailFastAndIgnoreFinalize(t *testing.T) {
66+
flag.CommandLine = &flag.FlagSet{}
67+
os.Args = []string{"test-binary", "-fail-fast"}
68+
cfg, err := NewFromFlags()
69+
if err != nil {
70+
t.Error("failed to parse args", err)
71+
}
72+
if !cfg.FailFast() {
73+
t.Error("expected fail-fast mode to be enabled when -fail-fast argument is passed")
74+
}
75+
}

0 commit comments

Comments
 (0)