Skip to content

Commit 7695b81

Browse files
authored
feat: add retry_if support on steps (#490)
* feat: add retry_if support on steps Signed-off-by: GitHub <[email protected]>
1 parent 17054c0 commit 7695b81

9 files changed

+108
-18
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ testcases:
302302
method: GET
303303
url: https://eu.api.ovh.com/1.0/
304304
retry: 3
305+
retry_if: # (optional, lets you early break unrecoverable errors)
306+
- result.statuscode ShouldNotEqual 403
305307
delay: 2
306308
assertions:
307309
- result.statuscode ShouldEqual 200

assertion.go

+19
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,22 @@ func findLineNumber(filename, testcase string, stepNumber int, assertion string,
353353

354354
return countLine
355355
}
356+
357+
//This evaluates a string of assertions with a given vars scope, and returns a slice of failures (i.e. empty slice = all pass)
358+
func testConditionalStatement(ctx context.Context, tc *TestCase, assertions []string, vars H, text string) ([]string, error) {
359+
var failures []string
360+
for _, assertion := range assertions {
361+
Debug(ctx, "evaluating %s", assertion)
362+
assert, err := parseAssertions(ctx, assertion, vars)
363+
if err != nil {
364+
Error(ctx, "unable to parse assertion: %v", err)
365+
tc.AppendError(err)
366+
return failures, err
367+
}
368+
if err := assert.Func(assert.Actual, assert.Args...); err != nil {
369+
s := fmt.Sprintf(text, tc.originalName, err)
370+
failures = append(failures, s)
371+
}
372+
}
373+
return failures, nil
374+
}

process_testcase.go

+9-13
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,18 @@ func (v *Venom) runTestCase(ctx context.Context, ts *TestSuite, tc *TestCase) {
139139
}
140140

141141
func (v *Venom) runTestSteps(ctx context.Context, tc *TestCase) {
142-
for _, skipAssertion := range tc.Skip {
143-
Debug(ctx, "evaluating %s", skipAssertion)
144-
assert, err := parseAssertions(ctx, skipAssertion, tc.Vars)
145-
if err != nil {
146-
Error(ctx, "unable to parse skip assertion: %v", err)
147-
tc.AppendError(err)
148-
return
149-
}
150-
if err := assert.Func(assert.Actual, assert.Args...); err != nil {
151-
s := fmt.Sprintf("skipping testcase %q: %v", tc.originalName, err)
142+
143+
results, err := testConditionalStatement(ctx, tc, tc.Skip, tc.Vars, "skipping testcase %q: %v")
144+
if err != nil {
145+
Error(ctx, "unable to evaluate \"skip\" assertions: %v", err)
146+
tc.AppendError(err)
147+
return
148+
}
149+
if len(results) > 0 {
150+
for _, s := range results {
152151
tc.Skipped = append(tc.Skipped, Skipped{Value: s})
153152
Warn(ctx, s)
154153
}
155-
}
156-
157-
if len(tc.Skipped) > 0 {
158154
return
159155
}
160156

process_teststep.go

+9
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ func (v *Venom) RunTestStep(ctx context.Context, e ExecutorRunner, tc *TestCase,
112112
if assertRes.ok {
113113
break
114114
}
115+
failures, err := testConditionalStatement(ctx, tc, e.RetryIf(), tc.computedVars, "")
116+
if err != nil {
117+
return fmt.Errorf("Error while evaluating retry condition: %v", err)
118+
}
119+
if len(failures) > 0 {
120+
failure := newFailure(*tc, stepNumber, "", fmt.Errorf("retry conditions not fulfilled, skipping %d remaining retries", e.Retry()-retry))
121+
tc.Failures = append(tc.Failures, *failure)
122+
break
123+
}
115124
}
116125

117126
tc.Errors = append(tc.Errors, assertRes.errors...)

tests/failing/retry_if.yml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
testcases:
2+
3+
- name: test retry
4+
steps:
5+
- type: exec
6+
script: echo pending
7+
retry: 2
8+
assertions:
9+
- result.systemout ShouldEqual ok
10+
11+
- name: test retry and retry_if
12+
steps:
13+
- type: exec
14+
script: sleep 2 && echo error
15+
retry: 5
16+
retry_if:
17+
- result.systemout ShouldEqual pending
18+
assertions:
19+
- result.systemout ShouldEqual ok

tests/retry_if.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: testsuite with retry if
2+
testcases:
3+
- name: testsuite with retry if (success)
4+
steps:
5+
- type: exec
6+
# we use a tmp file as "memory" to know whether we're on first attempt or second one
7+
script: |
8+
test -f /tmp/retry-if-first-attempt
9+
RC=$?
10+
touch /tmp/retry-if-first-attempt
11+
exit $RC
12+
retry: 1
13+
retry_if:
14+
- result.code ShouldNotEqual 0
15+
assertions:
16+
- result.code ShouldEqual 0
17+
18+
- name: testsuite with retry if (failing)
19+
steps:
20+
# spawn a venom sub-process and expect it to fail
21+
- type: exec
22+
script: '{{.venom.executable}} run failing/retry_if.yml'
23+
assertions:
24+
- result.code ShouldEqual 2
25+
- result.systemerr ShouldBeEmpty
26+
# classic retry
27+
- result.systemout ShouldContainSubstring "It's a failure after 3 attempts"
28+
# retry with condition (sleep 2 * 5 retries = max 10 seconds)
29+
- result.timeseconds ShouldBeLessThan 10
30+
- result.systemout ShouldContainSubstring "retry conditions not fulfilled, skipping 5 remaining retries"

types.go

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ func (t TestStep) StringSliceValue(name string) ([]string, error) {
151151
}
152152
return out, nil
153153
}
154+
//If string is empty, return an empty slice instead
155+
if len(out) == 0 {
156+
return []string{}, nil
157+
}
154158
return []string{out}, nil
155159
}
156160

types_executor.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type ExecutorRunner interface {
2525
ExecutorWithSetup
2626
Name() string
2727
Retry() int
28+
RetryIf() []string
2829
Delay() int
2930
Timeout() int
3031
Info() []string
@@ -39,6 +40,7 @@ type executor struct {
3940
Executor
4041
name string
4142
retry int // nb retry a test case if it is in failure.
43+
retryIf []string // retry conditions to check before performing any retries
4244
delay int // delay between two retries
4345
timeout int // timeout on executor
4446
info []string // info to display after the run and before the assertion
@@ -57,6 +59,10 @@ func (e executor) Retry() int {
5759
return e.retry
5860
}
5961

62+
func (e executor) RetryIf() []string {
63+
return e.retryIf
64+
}
65+
6066
func (e executor) Delay() int {
6167
return e.delay
6268
}
@@ -124,11 +130,12 @@ func (e executor) Run(ctx context.Context, step TestStep) (interface{}, error) {
124130
return e.Executor.Run(ctx, step)
125131
}
126132

127-
func newExecutorRunner(e Executor, name, stype string, retry, delay, timeout int, info []string) ExecutorRunner {
133+
func newExecutorRunner(e Executor, name, stype string, retry int, retryIf []string, delay, timeout int, info []string) ExecutorRunner {
128134
return &executor{
129135
Executor: e,
130136
name: name,
131137
retry: retry,
138+
retryIf: retryIf,
132139
delay: delay,
133140
timeout: timeout,
134141
info: info,

venom.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ func (v *Venom) GetExecutorRunner(ctx context.Context, ts TestStep, h H) (contex
108108
if err != nil {
109109
return nil, nil, err
110110
}
111+
retryIf, err := ts.StringSliceValue("retry_if")
112+
if err != nil {
113+
return nil, nil, err
114+
}
111115
delay, err := ts.IntValue("delay")
112116
if err != nil {
113117
return nil, nil, err
@@ -131,19 +135,19 @@ func (v *Venom) GetExecutorRunner(ctx context.Context, ts TestStep, h H) (contex
131135
ctx = context.WithValue(ctx, ContextKey("vars"), allKeys)
132136

133137
if name == "" {
134-
return ctx, newExecutorRunner(nil, name, "builtin", retry, delay, timeout, info), nil
138+
return ctx, newExecutorRunner(nil, name, "builtin", retry, retryIf, delay, timeout, info), nil
135139
}
136140

137141
if ex, ok := v.executorsBuiltin[name]; ok {
138-
return ctx, newExecutorRunner(ex, name, "builtin", retry, delay, timeout, info), nil
142+
return ctx, newExecutorRunner(ex, name, "builtin", retry, retryIf, delay, timeout, info), nil
139143
}
140144

141145
if err := v.registerUserExecutors(ctx, name, ts, vars); err != nil {
142146
Debug(ctx, "executor %q is not implemented as user executor - err:%v", name, err)
143147
}
144148

145149
if ex, ok := v.executorsUser[name]; ok {
146-
return ctx, newExecutorRunner(ex, name, "user", retry, delay, timeout, info), nil
150+
return ctx, newExecutorRunner(ex, name, "user", retry, retryIf, delay, timeout, info), nil
147151
}
148152

149153
if err := v.registerPlugin(ctx, name, vars); err != nil {
@@ -152,7 +156,7 @@ func (v *Venom) GetExecutorRunner(ctx context.Context, ts TestStep, h H) (contex
152156

153157
// then add the executor plugin to the map to not have to load it on each step
154158
if ex, ok := v.executorsUser[name]; ok {
155-
return ctx, newExecutorRunner(ex, name, "plugin", retry, delay, timeout, info), nil
159+
return ctx, newExecutorRunner(ex, name, "plugin", retry, retryIf, delay, timeout, info), nil
156160
}
157161
return ctx, nil, fmt.Errorf("executor %q is not implemented", name)
158162
}

0 commit comments

Comments
 (0)