Skip to content

Commit f9eb44e

Browse files
authored
Fix extensions calling invocation.proceed multiple times (#1862) (#2239)
1 parent 9145198 commit f9eb44e

3 files changed

Lines changed: 66 additions & 2 deletions

File tree

docs/release_notes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ will now require that the spec is annotated with <<parallel_execution.adoc#isola
7878
* Fix: Prevent NPE in SpecRunHistory.sortFeatures when duration is missing spockIssue:2234[]
7979
* `Retry` extension does not mesh with `TestAbortedException` and `PendingFeature` spockIssue:1863[]
8080
* Fix display of caught exceptions within `verifyEach` blocks spockPull:2163[]
81+
* Fix extensions that call `invocation.proceed()` multiple times like `@Retry` does spockIssue:1862[]
8182

8283
Thanks to all the contributors to this release: Andreas Turban, Björn Kautler, Christoph Loy, Leonard Brünings, Thanos Tsiamis
8384

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class MethodInvocation implements IMethodInvocation {
4141
private final MethodInfo method;
4242
private Object[] arguments;
4343
private final Iterator<IMethodInterceptor> interceptors;
44+
private IMethodInterceptor nextInterceptor;
4445

4546
private final StoreProvider storeProvider;
4647

@@ -59,6 +60,19 @@ public MethodInvocation(FeatureInfo feature, IterationInfo iteration, StoreProvi
5960
: CollectionUtil.concat(scopedInterceptors, method.getInterceptors()).iterator();
6061
}
6162

63+
private MethodInvocation(FeatureInfo feature, IterationInfo iteration, StoreProvider storeProvider, Object sharedInstance,
64+
Object instance, Object target, MethodInfo method, Iterator<IMethodInterceptor> interceptors, Object[] arguments) {
65+
this.feature = feature;
66+
this.iteration = iteration;
67+
this.storeProvider = storeProvider;
68+
this.sharedInstance = sharedInstance;
69+
this.instance = instance;
70+
this.target = target;
71+
this.method = method;
72+
this.interceptors = interceptors;
73+
this.arguments = arguments;
74+
}
75+
6276
@Override
6377
public SpecInfo getSpec() {
6478
return method.getParent();
@@ -118,8 +132,12 @@ public void resolveArgument(int index, Object value) {
118132

119133
@Override
120134
public void proceed() throws Throwable {
121-
if (interceptors.hasNext())
122-
interceptors.next().intercept(this);
135+
if ((nextInterceptor == null) && interceptors.hasNext()) {
136+
nextInterceptor = interceptors.next();
137+
}
138+
if (nextInterceptor != null)
139+
nextInterceptor.intercept(new MethodInvocation(feature, iteration, storeProvider,
140+
sharedInstance, instance, target, method, interceptors, arguments.clone()));
123141
else method.invoke(target, arguments);
124142
}
125143
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ class RetryFeatureExtensionSpec extends EmbeddedSpecification {
2727
static AtomicInteger setupCounter = new AtomicInteger()
2828
static AtomicInteger cleanupCounter = new AtomicInteger()
2929
static AtomicInteger featureCounter = new AtomicInteger()
30+
static AtomicInteger extensionCounter = new AtomicInteger()
3031
static StringBuffer iterationBuffer
3132

3233
def setup() {
3334
runner.throwFailure = false
3435
setupCounter.set(0)
3536
cleanupCounter.set(0)
3637
featureCounter.set(0)
38+
extensionCounter.set(0)
3739
iterationBuffer = new StringBuffer()
3840
}
3941

@@ -772,6 +774,33 @@ class Foo extends Specification {
772774
)
773775
}
774776
777+
def "@Retry interceptor chains to enclosed interceptors each time"() {
778+
when:
779+
def result = runner.runWithImports("""
780+
import spock.lang.Retry
781+
import org.spockframework.smoke.extension.CountExecution
782+
783+
class Foo extends Specification {
784+
@Retry
785+
@CountExecution
786+
def bar(baz) {
787+
expect: false
788+
}
789+
}
790+
""")
791+
792+
then:
793+
result.testsStartedCount == 1
794+
result.testsSucceededCount == 0
795+
result.testsFailedCount == 1
796+
with(result.failures.exception[0], MultipleFailuresError) {
797+
failures.size() == 4
798+
failures.every { it instanceof ConditionNotSatisfiedError }
799+
}
800+
result.testsSkippedCount == 0
801+
extensionCounter.get() == 4
802+
}
803+
775804
private def withParallelExecution(boolean enableParallelExecution) {
776805
runner.configurationScript {
777806
runner {
@@ -795,9 +824,25 @@ class Foo extends Specification {
795824
}
796825
}
797826
}
827+
828+
static class CountExecutionExtension implements IAnnotationDrivenExtension<CountExecution> {
829+
@Override
830+
void visitFeatureAnnotation(CountExecution annotation, FeatureInfo feature) {
831+
feature.featureMethod.addInterceptor { invocation ->
832+
org.spockframework.smoke.extension.RetryFeatureExtensionSpec.extensionCounter.incrementAndGet()
833+
invocation.resolveArgument(0, "BAZ")
834+
invocation.proceed()
835+
}
836+
}
837+
}
798838
}
799839
800840
@Retention(RetentionPolicy.RUNTIME)
801841
@ExtensionAnnotation(RetryFeatureExtensionSpec.ChangeThreadExtension)
802842
@interface ChangeThread {
803843
}
844+
845+
@Retention(RetentionPolicy.RUNTIME)
846+
@ExtensionAnnotation(RetryFeatureExtensionSpec.CountExecutionExtension)
847+
@interface CountExecution {
848+
}

0 commit comments

Comments
 (0)