From 1bd9edbee7e63970fd244f9e330adfe14745bf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Kautler?= Date: Thu, 12 Mar 2020 15:43:55 +0100 Subject: [PATCH] Make @Retry repeatable (#1030) --- docs/extensions.adoc | 76 +++--------- docs/release_notes.adoc | 2 + .../extension/builtin/RetryExtension.java | 8 +- .../src/main/java/spock/lang/Retry.java | 11 ++ .../docs/extension/RetryDocSpec.groovy | 110 ++++++++++++++++++ .../RetryFeatureExtensionSpec.groovy | 29 +++++ 6 files changed, 171 insertions(+), 65 deletions(-) create mode 100644 spock-specs/src/test/groovy/org/spockframework/docs/extension/RetryDocSpec.groovy diff --git a/docs/extensions.adoc b/docs/extensions.adoc index 96249f4075..3da8336f46 100644 --- a/docs/extensions.adoc +++ b/docs/extensions.adoc @@ -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. @@ -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. diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index c68fc8a46b..3b63a85aa2 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -16,6 +16,8 @@ include::include.adoc[] - `@Issue` is now repeatable +- `@Retry` is now repeatable + == 2.0-M3 (2020-06-11) diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/RetryExtension.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/RetryExtension.java index 4733e7a9c8..24f8f7ee8f 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/RetryExtension.java +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/RetryExtension.java @@ -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 { @Override - public void visitSpecAnnotation(Retry annotation, SpecInfo spec) { + public void visitSpecAnnotations(List annotations, SpecInfo spec) { if (noSubSpecWithRetryAnnotation(spec.getSubSpec())) { for (FeatureInfo feature : spec.getBottomSpec().getAllFeatures()) { if (noRetryAnnotation(feature.getFeatureMethod())) { - visitFeatureAnnotation(annotation, feature); + visitFeatureAnnotations(annotations, feature); } } } @@ -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 diff --git a/spock-core/src/main/java/spock/lang/Retry.java b/spock-core/src/main/java/spock/lang/Retry.java index 0a9f9ca67c..85391b4dc8 100644 --- a/spock-core/src/main/java/spock/lang/Retry.java +++ b/spock-core/src/main/java/spock/lang/Retry.java @@ -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. @@ -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(); + } } diff --git a/spock-specs/src/test/groovy/org/spockframework/docs/extension/RetryDocSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/docs/extension/RetryDocSpec.groovy new file mode 100644 index 0000000000..3d0a0a4902 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/docs/extension/RetryDocSpec.groovy @@ -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[] diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy index 656d9a79da..ba73439879 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/RetryFeatureExtensionSpec.groovy @@ -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)