Skip to content

Commit d2e2b5e

Browse files
committed
Make @Retry repeatable (#1030)
1 parent 38c6a8d commit d2e2b5e

File tree

6 files changed

+171
-65
lines changed

6 files changed

+171
-65
lines changed

docs/extensions.adoc

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -263,59 +263,21 @@ It also provides special support for data driven features, offering to either re
263263

264264
[source,groovy]
265265
----
266-
class FlakyIntegrationSpec extends Specification {
267-
@Retry
268-
def retry3Times() { ... }
269-
270-
@Retry(count = 5)
271-
def retry5Times() { ... }
272-
273-
@Retry(exceptions=[IOException])
274-
def onlyRetryIOException() { ... }
275-
276-
@Retry(condition = { failure.message.contains('foo') })
277-
def onlyRetryIfConditionOnFailureHolds() { ... }
278-
279-
@Retry(condition = { instance.field != null })
280-
def onlyRetryIfConditionOnInstanceHolds() { ... }
281-
282-
@Retry
283-
def retryFailingIterations() {
284-
...
285-
where:
286-
data << sql.select()
287-
}
288-
289-
@Retry(mode = Retry.Mode.FEATURE)
290-
def retryWholeFeature() {
291-
...
292-
where:
293-
data << sql.select()
294-
}
295-
296-
@Retry(delay = 1000)
297-
def retryAfter1000MsDelay() { ... }
298-
}
266+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-common]
267+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-a]
299268
----
300269

301270
Retries can also be applied to spec classes which has the same effect as applying it to each feature method that isn't
302-
already annotated with {@code Retry}.
271+
already annotated with `Retry`.
303272

304273
[source,groovy]
305274
----
306-
@Retry
307-
class FlakyIntegrationSpec extends Specification {
308-
def "will be retried with config from class"() {
309-
...
310-
}
311-
@Retry(count = 5)
312-
def "will be retried using its own config"() {
313-
...
314-
}
315-
}
275+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-b1]
276+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-common]
277+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-b2]
316278
----
317279

318-
A {@code @Retry} annotation that is declared on a spec class is applied to all features in all subclasses as well,
280+
A `@Retry` annotation that is declared on a spec class is applied to all features in all subclasses as well,
319281
unless a subclass declares its own annotation. If so, the retries defined in the subclass are applied to all feature
320282
methods declared in the subclass as well as inherited ones.
321283

@@ -324,25 +286,15 @@ Running `BarIntegrationSpec` will execute `inherited` and `bar` with two retries
324286

325287
[source,groovy]
326288
----
327-
@Retry(count = 1)
328-
abstract class AbstractIntegrationSpec extends Specification {
329-
def inherited() {
330-
...
331-
}
332-
}
289+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-c]
290+
----
333291

334-
class FooIntegrationSpec extends AbstractIntegrationSpec {
335-
def foo() {
336-
...
337-
}
338-
}
292+
If multiple `@Retry` annotations are present, they can be used to have different retry settings
293+
for different situations:
339294

340-
@Retry(count = 2)
341-
class BarIntegrationSpec extends AbstractIntegrationSpec {
342-
def bar() {
343-
...
344-
}
345-
}
295+
[source,groovy,indent=0]
296+
----
297+
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-d]
346298
----
347299

348300
Check https://github.com/spockframework/spock/blob/master/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy[RetryFeatureExtensionSpec] for more examples.

docs/release_notes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ include::include.adoc[]
1212
- `AbstractAnnotationDrivenExtension` is now deprecated and its logic was moved to `default` methods of
1313
`IAnnotationDrivenExtension` which should be implemented directly now instead of extending the abstract class.
1414

15+
- `@Retry` is now repeatable
16+
1517

1618
== 2.0-M3 (2020-06-11)
1719

spock-core/src/main/java/org/spockframework/runtime/extension/builtin/RetryExtension.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@
2020
import org.spockframework.runtime.model.*;
2121
import spock.lang.Retry;
2222

23+
import java.util.List;
24+
2325
/**
2426
* @author Leonard Brünings
2527
* @since 1.2
2628
*/
2729
public class RetryExtension implements IAnnotationDrivenExtension<Retry> {
2830
@Override
29-
public void visitSpecAnnotation(Retry annotation, SpecInfo spec) {
31+
public void visitSpecAnnotations(List<Retry> annotations, SpecInfo spec) {
3032
if (noSubSpecWithRetryAnnotation(spec.getSubSpec())) {
3133
for (FeatureInfo feature : spec.getBottomSpec().getAllFeatures()) {
3234
if (noRetryAnnotation(feature.getFeatureMethod())) {
33-
visitFeatureAnnotation(annotation, feature);
35+
visitFeatureAnnotations(annotations, feature);
3436
}
3537
}
3638
}
@@ -44,7 +46,7 @@ private boolean noSubSpecWithRetryAnnotation(SpecInfo spec) {
4446
}
4547

4648
private boolean noRetryAnnotation(NodeInfo node) {
47-
return !node.getReflection().isAnnotationPresent(Retry.class);
49+
return !node.isAnnotationPresent(Retry.class);
4850
}
4951

5052
@Override

spock-core/src/main/java/spock/lang/Retry.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
@Retention(RetentionPolicy.RUNTIME)
4545
@Target({ElementType.TYPE, ElementType.METHOD})
4646
@ExtensionAnnotation(RetryExtension.class)
47+
@Repeatable(Retry.Container.class)
4748
public @interface Retry {
4849
/**
4950
* Configures which types of Exceptions should be retried.
@@ -103,4 +104,14 @@ enum Mode {
103104
*/
104105
SETUP_FEATURE_CLEANUP
105106
}
107+
108+
/**
109+
* @since 2.0
110+
*/
111+
@Beta
112+
@Retention(RetentionPolicy.RUNTIME)
113+
@Target({ElementType.TYPE, ElementType.METHOD})
114+
@interface Container {
115+
Retry[] value();
116+
}
106117
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.spockframework.docs.extension
2+
3+
import groovy.sql.Sql
4+
import spock.lang.Retry
5+
import spock.lang.Shared
6+
import spock.lang.Specification
7+
8+
abstract
9+
// tag::example-common[]
10+
class FlakyIntegrationSpec extends Specification {
11+
// end::example-common[]
12+
@Shared
13+
def sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
14+
15+
// tag::example-d[]
16+
@Retry(exceptions = IllegalArgumentException, count = 2)
17+
@Retry(exceptions = IllegalAccessException, count = 4)
18+
def retryDependingOnException() {
19+
// end::example-d[]
20+
expect: true
21+
}
22+
}
23+
24+
class FlakyIntegrationSpecA extends FlakyIntegrationSpec {
25+
// tag::example-a[]
26+
@Retry
27+
def retry3Times() {
28+
expect: true
29+
}
30+
31+
@Retry(count = 5)
32+
def retry5Times() {
33+
expect: true
34+
}
35+
36+
@Retry(exceptions = [IOException])
37+
def onlyRetryIOException() {
38+
expect: true
39+
}
40+
41+
@Retry(condition = { failure.message.contains('foo') })
42+
def onlyRetryIfConditionOnFailureHolds() {
43+
expect: true
44+
}
45+
46+
@Retry(condition = { instance.field != null })
47+
def onlyRetryIfConditionOnInstanceHolds() {
48+
expect: true
49+
}
50+
51+
@Retry
52+
def retryFailingIterations() {
53+
expect: true
54+
55+
where:
56+
data << sql.execute('')
57+
}
58+
59+
@Retry(mode = Retry.Mode.SETUP_FEATURE_CLEANUP)
60+
def retryWholeFeature() {
61+
expect: true
62+
63+
where:
64+
data << sql.execute('')
65+
}
66+
67+
@Retry(delay = 1000)
68+
def retryAfter1000MsDelay() {
69+
expect: true
70+
}
71+
}
72+
// end::example-a[]
73+
74+
// tag::example-b1[]
75+
@Retry
76+
// end::example-b1[]
77+
class FlakyIntegrationSpecB extends FlakyIntegrationSpec {
78+
// tag::example-b2[]
79+
def "will be retried with config from class"() {
80+
expect: true
81+
}
82+
83+
@Retry(count = 5)
84+
def "will be retried using its own config"() {
85+
expect: true
86+
}
87+
}
88+
// end::example-b2[]
89+
90+
// tag::example-c[]
91+
@Retry(count = 1)
92+
abstract class AbstractIntegrationSpec extends Specification {
93+
def inherited() {
94+
expect: true
95+
}
96+
}
97+
98+
class FooIntegrationSpec extends AbstractIntegrationSpec {
99+
def foo() {
100+
expect: true
101+
}
102+
}
103+
104+
@Retry(count = 2)
105+
class BarIntegrationSpec extends AbstractIntegrationSpec {
106+
def bar() {
107+
expect: true
108+
}
109+
}
110+
// end::example-c[]

spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,35 @@ def bar() {
6666
featureCounter.get() == 4
6767
}
6868

69+
def "@Retry works properly if applied multiple times"() {
70+
when:
71+
def result = runner.runSpecBody("""
72+
@Retry(exceptions = IllegalArgumentException, count = 2)
73+
@Retry(exceptions = IllegalAccessException, count = 4)
74+
def bar() {
75+
featureCounter.incrementAndGet()
76+
expect:
77+
throw new ${exception.simpleName}()
78+
}
79+
""")
80+
81+
then:
82+
result.testsStartedCount == 1
83+
result.testsSucceededCount == 0
84+
result.testsFailedCount == 1
85+
with(result.failures.exception[0], MultipleFailuresError) {
86+
failures.size() == expectedCount
87+
failures.every { exception.isInstance(it) }
88+
}
89+
result.testsSkippedCount == 0
90+
featureCounter.get() == expectedCount
91+
92+
where:
93+
exception || expectedCount
94+
IllegalArgumentException || 3
95+
IllegalAccessException || 5
96+
}
97+
6998
def "@Retry mode #mode executes setup and cleanup #expectedCount times"(String mode, int expectedCount) {
7099
given:
71100
setupCounter.set(0)

0 commit comments

Comments
 (0)