Skip to content

Commit

Permalink
Make @Retry repeatable (#1030)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Jul 13, 2020
1 parent 80996fd commit 3b4d2e3
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 65 deletions.
76 changes: 14 additions & 62 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -263,59 +263,21 @@ It also provides special support for data driven features, offering to either re

[source,groovy]
----
class FlakyIntegrationSpec extends Specification {
@Retry
def retry3Times() { ... }
@Retry(count = 5)
def retry5Times() { ... }
@Retry(exceptions=[IOException])
def onlyRetryIOException() { ... }
@Retry(condition = { failure.message.contains('foo') })
def onlyRetryIfConditionOnFailureHolds() { ... }
@Retry(condition = { instance.field != null })
def onlyRetryIfConditionOnInstanceHolds() { ... }
@Retry
def retryFailingIterations() {
...
where:
data << sql.select()
}
@Retry(mode = Retry.Mode.FEATURE)
def retryWholeFeature() {
...
where:
data << sql.select()
}
@Retry(delay = 1000)
def retryAfter1000MsDelay() { ... }
}
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-common]
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-a]
----

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

[source,groovy]
----
@Retry
class FlakyIntegrationSpec extends Specification {
def "will be retried with config from class"() {
...
}
@Retry(count = 5)
def "will be retried using its own config"() {
...
}
}
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-b1]
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-common]
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-b2]
----

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

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

[source,groovy]
----
@Retry(count = 1)
abstract class AbstractIntegrationSpec extends Specification {
def inherited() {
...
}
}
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-c]
----

class FooIntegrationSpec extends AbstractIntegrationSpec {
def foo() {
...
}
}
If multiple `@Retry` annotations are present, they can be used to have different retry settings
for different situations:

@Retry(count = 2)
class BarIntegrationSpec extends AbstractIntegrationSpec {
def bar() {
...
}
}
[source,groovy,indent=0]
----
include::{sourcedir}/extension/RetryDocSpec.groovy[tag=example-d]
----

Check https://github.com/spockframework/spock/blob/master/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy[RetryFeatureExtensionSpec] for more examples.
Expand Down
2 changes: 2 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ include::include.adoc[]

- `@ConfineMetaClassChanges` is now repeatable

- `@Retry` is now repeatable


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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@
import org.spockframework.runtime.model.*;
import spock.lang.Retry;

import java.util.List;

/**
* @author Leonard Brünings
* @since 1.2
*/
public class RetryExtension implements IAnnotationDrivenExtension<Retry> {
@Override
public void visitSpecAnnotation(Retry annotation, SpecInfo spec) {
public void visitSpecAnnotations(List<Retry> annotations, SpecInfo spec) {
if (noSubSpecWithRetryAnnotation(spec.getSubSpec())) {
for (FeatureInfo feature : spec.getBottomSpec().getAllFeatures()) {
if (noRetryAnnotation(feature.getFeatureMethod())) {
visitFeatureAnnotation(annotation, feature);
visitFeatureAnnotations(annotations, feature);
}
}
}
Expand All @@ -44,7 +46,7 @@ private boolean noSubSpecWithRetryAnnotation(SpecInfo spec) {
}

private boolean noRetryAnnotation(NodeInfo node) {
return !node.getReflection().isAnnotationPresent(Retry.class);
return !node.isAnnotationPresent(Retry.class);
}

@Override
Expand Down
11 changes: 11 additions & 0 deletions spock-core/src/main/java/spock/lang/Retry.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@ExtensionAnnotation(RetryExtension.class)
@Repeatable(Retry.Container.class)
public @interface Retry {
/**
* Configures which types of Exceptions should be retried.
Expand Down Expand Up @@ -103,4 +104,14 @@ enum Mode {
*/
SETUP_FEATURE_CLEANUP
}

/**
* @since 2.0
*/
@Beta
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Container {
Retry[] value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.spockframework.docs.extension

import groovy.sql.Sql
import spock.lang.Retry
import spock.lang.Shared
import spock.lang.Specification

abstract
// tag::example-common[]
class FlakyIntegrationSpec extends Specification {
// end::example-common[]
@Shared
def sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")

// tag::example-d[]
@Retry(exceptions = IllegalArgumentException, count = 2)
@Retry(exceptions = IllegalAccessException, count = 4)
def retryDependingOnException() {
// end::example-d[]
expect: true
}
}

class FlakyIntegrationSpecA extends FlakyIntegrationSpec {
// tag::example-a[]
@Retry
def retry3Times() {
expect: true
}

@Retry(count = 5)
def retry5Times() {
expect: true
}

@Retry(exceptions = [IOException])
def onlyRetryIOException() {
expect: true
}

@Retry(condition = { failure.message.contains('foo') })
def onlyRetryIfConditionOnFailureHolds() {
expect: true
}

@Retry(condition = { instance.field != null })
def onlyRetryIfConditionOnInstanceHolds() {
expect: true
}

@Retry
def retryFailingIterations() {
expect: true

where:
data << sql.execute('')
}

@Retry(mode = Retry.Mode.SETUP_FEATURE_CLEANUP)
def retryWholeFeature() {
expect: true

where:
data << sql.execute('')
}

@Retry(delay = 1000)
def retryAfter1000MsDelay() {
expect: true
}
}
// end::example-a[]

// tag::example-b1[]
@Retry
// end::example-b1[]
class FlakyIntegrationSpecB extends FlakyIntegrationSpec {
// tag::example-b2[]
def "will be retried with config from class"() {
expect: true
}

@Retry(count = 5)
def "will be retried using its own config"() {
expect: true
}
}
// end::example-b2[]

// tag::example-c[]
@Retry(count = 1)
abstract class AbstractIntegrationSpec extends Specification {
def inherited() {
expect: true
}
}

class FooIntegrationSpec extends AbstractIntegrationSpec {
def foo() {
expect: true
}
}

@Retry(count = 2)
class BarIntegrationSpec extends AbstractIntegrationSpec {
def bar() {
expect: true
}
}
// end::example-c[]
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,35 @@ def bar() {
featureCounter.get() == 4
}

def "@Retry works properly if applied multiple times"() {
when:
def result = runner.runSpecBody("""
@Retry(exceptions = IllegalArgumentException, count = 2)
@Retry(exceptions = IllegalAccessException, count = 4)
def bar() {
featureCounter.incrementAndGet()
expect:
throw new ${exception.simpleName}()
}
""")

then:
result.testsStartedCount == 1
result.testsSucceededCount == 0
result.testsFailedCount == 1
with(result.failures.exception[0], MultipleFailuresError) {
failures.size() == expectedCount
failures.every { exception.isInstance(it) }
}
result.testsSkippedCount == 0
featureCounter.get() == expectedCount

where:
exception || expectedCount
IllegalArgumentException || 3
IllegalAccessException || 5
}

def "@Retry mode #mode executes setup and cleanup #expectedCount times"(String mode, int expectedCount) {
given:
setupCounter.set(0)
Expand Down

0 comments on commit 3b4d2e3

Please sign in to comment.