Skip to content

Commit

Permalink
Add feature scoped interceptors... (#1844)
Browse files Browse the repository at this point in the history
of initializer/setup/cleanup methods.
  • Loading branch information
leonard84 committed Feb 20, 2024
1 parent ce21741 commit 31fd34c
Show file tree
Hide file tree
Showing 10 changed files with 993 additions and 503 deletions.
43 changes: 7 additions & 36 deletions docs/extensions.adoc
Expand Up @@ -1013,45 +1013,16 @@ interceptor`. If you attach your interceptor to both of them and need a differen
can of course build two different interceptors or add a parameter to your interceptor and create two instances, telling
each at addition time whether it is attached to the method interceptor or the other one.

[source,groovy,indent=0]
----
include::{sourcedir}/extension/InterceptorSpec.groovy[tag=interceptor-class]
----
.Add All Interceptors
[source,groovy]
[source,groovy,indent=0]
----
class I extends AbstractMethodInterceptor { I(def s) {} }
// DISCLAIMER: The following shows all possible injection points that you could use
// depending on need and situation. You should normally not need to
// register a listener to all these places.
//
// Also, when building an annotation driven local extension, you should
// consider where you want the effects to be present, for example only
// for the features in the same class (specInfo.features), or for features
// in the same and superclasses (specInfo.allFeatures), or also for
// features in subclasses (specInfo.bottomSpec.allFeatures), and so on.
// on SpecInfo
specInfo.specsBottomToTop*.addSharedInitializerInterceptor new I('shared initializer')
specInfo.allSharedInitializerMethods*.addInterceptor new I('shared initializer method')
specInfo.addInterceptor new I('specification')
specInfo.specsBottomToTop*.addSetupSpecInterceptor new I('setup spec')
specInfo.allSetupSpecMethods*.addInterceptor new I('setup spec method')
specInfo.allFeatures*.addInterceptor new I('feature')
specInfo.specsBottomToTop*.addInitializerInterceptor new I('initializer')
specInfo.allInitializerMethods*.addInterceptor new I('initializer method')
specInfo.allFeatures*.addIterationInterceptor new I('iteration')
specInfo.specsBottomToTop*.addSetupInterceptor new I('setup')
specInfo.allSetupMethods*.addInterceptor new I('setup method')
specInfo.allFeatures*.featureMethod*.addInterceptor new I('feature method')
specInfo.specsBottomToTop*.addCleanupInterceptor new I('cleanup')
specInfo.allCleanupMethods*.addInterceptor new I('cleanup method')
specInfo.specsBottomToTop*.addCleanupSpecInterceptor new I('cleanup spec')
specInfo.allCleanupSpecMethods*.addInterceptor new I('cleanup spec method')
specInfo.allFixtureMethods*.addInterceptor new I('fixture method')
// on FeatureInfo (already included above, handling all features)
featureInfo.addInterceptor new I('feature')
featureInfo.addIterationInterceptor new I('iteration')
featureInfo.featureMethod.addInterceptor new I('feature method')
include::{sourcedir}/extension/InterceptorSpec.groovy[tag=interceptor-register-spec]
include::{sourcedir}/extension/InterceptorSpec.groovy[tag=interceptor-register-feature]
----

==== Injecting Method Parameters
Expand Down
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Expand Up @@ -31,6 +31,7 @@ include::include.adoc[]
* Add support for <<extensions.adoc#extension-store,keeping state in extensions>>
* Add support for parameter injection of `@TempDir`
* Add `@Snapshot` extension for <<extensions.adoc#snapshot-testing,snapshot testing>>
* Add <<extensions.adoc#spock-interceptors,feature-scoped interceptors>> spockPull:1844[]
* Improve `@TempDir` field injection, now it happens before field initialization, so it can be used by other field initializers.
* Fix exception when configured `baseDir` was not existing, now `@TempDir` will create the baseDir directory if it is missing.
* Fix bad error message for collection conditions, when one of the operands is `null`
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,12 +15,16 @@

package org.spockframework.runtime;

import org.spockframework.runtime.extension.*;
import org.spockframework.runtime.extension.IMethodInterceptor;
import org.spockframework.runtime.extension.MethodInvocation;
import org.spockframework.runtime.model.*;
import org.spockframework.util.*;
import org.spockframework.util.CollectionUtil;
import org.spockframework.util.InternalSpockError;
import spock.lang.Specification;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static java.lang.System.arraycopy;
import static java.util.Arrays.copyOfRange;
Expand Down Expand Up @@ -66,8 +70,7 @@ private MethodInfo createMethodInfoForDoRunSpec(SpockExecutionContext context, R
SpecInfo spec = context.getSpec();
result.setParent(spec);
result.setKind(MethodKind.SPEC_EXECUTION);
for (IMethodInterceptor interceptor : spec.getInterceptors())
result.addInterceptor(interceptor);
spec.getInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -108,8 +111,7 @@ private MethodInfo createMethodInfoForDoRunSharedInitializer(SpockExecutionConte
);
result.setParent(spec);
result.setKind(MethodKind.SHARED_INITIALIZER);
for (IMethodInterceptor interceptor : spec.getSharedInitializerInterceptors())
result.addInterceptor(interceptor);
spec.getSharedInitializerInterceptors().forEach(result::addInterceptor);
return result;
}

Expand All @@ -136,8 +138,7 @@ private MethodInfo createMethodInfoForDoRunSetupSpec(SpockExecutionContext conte
);
result.setParent(spec);
result.setKind(MethodKind.SETUP_SPEC);
for (IMethodInterceptor interceptor : spec.getSetupSpecInterceptors())
result.addInterceptor(interceptor);
spec.getSetupSpecInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -166,8 +167,7 @@ private MethodInfo createMethodForDoRunCleanupSpec(SpockExecutionContext context
);
result.setParent(spec);
result.setKind(MethodKind.CLEANUP_SPEC);
for (IMethodInterceptor interceptor : spec.getCleanupSpecInterceptors())
result.addInterceptor(interceptor);
spec.getCleanupSpecInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -207,8 +207,7 @@ private MethodInfo createMethodInfoForDoRunFeature(SpockExecutionContext context
result.setParent(currentFeature.getParent());
result.setKind(MethodKind.FEATURE_EXECUTION);
result.setFeature(currentFeature);
for (IMethodInterceptor interceptor : currentFeature.getInterceptors())
result.addInterceptor(interceptor);
currentFeature.getInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -247,8 +246,7 @@ private MethodInfo createMethodInfoForDoRunIteration(SpockExecutionContext conte
result.setKind(MethodKind.ITERATION_EXECUTION);
result.setFeature(currentFeature);
result.setIteration(context.getCurrentIteration());
for (IMethodInterceptor interceptor : currentFeature.getIterationInterceptors())
result.addInterceptor(interceptor);
currentFeature.getIterationInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -277,8 +275,10 @@ private MethodInfo createMethodInfoForDoRunInitializer(SpockExecutionContext con
result.setParent(currentFeature.getParent());
result.setKind(MethodKind.INITIALIZER);
result.setFeature(currentFeature);
for (IMethodInterceptor interceptor : spec.getInitializerInterceptors())
result.addInterceptor(interceptor);
if (spec.getIsBottomSpec()) {
currentFeature.getInitializerInterceptors().forEach(result::addInterceptor);
}
spec.getInitializerInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -308,8 +308,10 @@ private MethodInfo createMethodInfoForDoRunSetup(SpockExecutionContext context,
result.setKind(MethodKind.SETUP);
result.setFeature(currentFeature);
result.setIteration(context.getCurrentIteration());
for (IMethodInterceptor interceptor : spec.getSetupInterceptors())
result.addInterceptor(interceptor);
if (spec.getIsBottomSpec()) {
currentFeature.getSetupInterceptors().forEach(result::addInterceptor);
}
spec.getSetupInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -352,8 +354,10 @@ private MethodInfo createMethodInfoForDoRunCleanup(SpockExecutionContext context
result.setKind(MethodKind.CLEANUP);
result.setFeature(currentFeature);
result.setIteration(context.getCurrentIteration());
for (IMethodInterceptor interceptor : spec.getCleanupInterceptors())
result.addInterceptor(interceptor);
if (spec.getIsBottomSpec()) {
currentFeature.getCleanupInterceptors().forEach(result::addInterceptor);
}
spec.getCleanupInterceptors().forEach(result::addInterceptor);
return result;
}

Expand Down Expand Up @@ -408,15 +412,20 @@ protected void invoke(SpockExecutionContext context, Object target, MethodInfo m
Arrays.fill(methodArguments, arguments.length, parameterCount, MISSING_ARGUMENT);
}

List<IMethodInterceptor> scopedInterceptors = Collections.emptyList();
if (context.getCurrentFeature() != null) {
scopedInterceptors = context.getCurrentFeature().getScopedMethodInterceptors(method);
}

// fast lane
if (method.getInterceptors().isEmpty()) {
if (method.getInterceptors().isEmpty() && scopedInterceptors.isEmpty()) {
invokeRaw(context, target, method, methodArguments);
return;
}

// slow lane
MethodInvocation invocation = new MethodInvocation(context.getCurrentFeature(),
context.getCurrentIteration(), context.getStoreProvider(), context.getSharedInstance(), context.getCurrentInstance(), target, method, methodArguments);
context.getCurrentIteration(), context.getStoreProvider(), context.getSharedInstance(), context.getCurrentInstance(), target, method, scopedInterceptors, methodArguments);
try {
invocation.proceed();
} catch (Throwable throwable) {
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2023 the original author or authors.
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,10 +16,15 @@
package org.spockframework.runtime.extension;

import org.spockframework.runtime.StoreProvider;
import org.spockframework.runtime.model.*;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.IterationInfo;
import org.spockframework.runtime.model.MethodInfo;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.Checks;
import org.spockframework.util.CollectionUtil;

import java.util.Iterator;
import java.util.List;

import static org.spockframework.runtime.model.MethodInfo.MISSING_ARGUMENT;

Expand All @@ -40,7 +45,7 @@ public class MethodInvocation implements IMethodInvocation {
private final StoreProvider storeProvider;

public MethodInvocation(FeatureInfo feature, IterationInfo iteration, StoreProvider storeProvider, Object sharedInstance,
Object instance, Object target, MethodInfo method, Object[] arguments) {
Object instance, Object target, MethodInfo method, List<IMethodInterceptor> scopedInterceptors, Object[] arguments) {
this.feature = feature;
this.iteration = iteration;
this.storeProvider = storeProvider;
Expand All @@ -49,7 +54,9 @@ public MethodInvocation(FeatureInfo feature, IterationInfo iteration, StoreProvi
this.target = target;
this.method = method;
this.arguments = arguments;
interceptors = method.getInterceptors().iterator();
interceptors = scopedInterceptors.isEmpty()
? method.getInterceptors().iterator()
: CollectionUtil.concat(scopedInterceptors, method.getInterceptors()).iterator();
}

@Override
Expand Down
@@ -1,9 +1,26 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package org.spockframework.runtime.model;

import org.spockframework.runtime.extension.IDataDriver;
import org.spockframework.runtime.extension.IMethodInterceptor;
import org.spockframework.runtime.model.parallel.*;
import org.spockframework.runtime.model.parallel.ExclusiveResource;
import org.spockframework.runtime.model.parallel.ExecutionMode;
import org.spockframework.util.Beta;
import org.spockframework.util.Checks;
import org.spockframework.util.Nullable;

import java.lang.reflect.AnnotatedElement;
Expand All @@ -20,6 +37,10 @@ public class FeatureInfo extends SpecElementInfo<SpecInfo, AnnotatedElement> imp
private final List<String> dataVariables = new ArrayList<>();
private final List<BlockInfo> blocks = new ArrayList<>();
private final List<IMethodInterceptor> iterationInterceptors = new ArrayList<>();
private final List<IMethodInterceptor> setupInterceptors = new ArrayList<>();
private final List<IMethodInterceptor> cleanupInterceptors = new ArrayList<>();
private final List<IMethodInterceptor> initializerInterceptors = new ArrayList<>();
private final Map<MethodInfo, List<IMethodInterceptor>> scopedMethodInterceptors = new HashMap<>();

private final Set<ExclusiveResource> exclusiveResources = new HashSet<>();

Expand Down Expand Up @@ -91,6 +112,103 @@ public void addBlock(BlockInfo block) {
blocks.add(block);
}

/**
* @since 2.4
*/
@Beta
public List<IMethodInterceptor> getSetupInterceptors() {
return setupInterceptors;
}

/**
* Adds a setup interceptor for this feature.
* <p>
* The feature-scoped interceptors will execute before the spec interceptors.
*
* @since 2.4
*/
@Beta
public void addSetupInterceptor(IMethodInterceptor interceptor) {
setupInterceptors.add(interceptor);
}

/**
* @since 2.4
*/
@Beta
public List<IMethodInterceptor> getCleanupInterceptors() {
return cleanupInterceptors;
}

/**
* Adds a cleanup interceptor for this feature.
* <p>
* The feature-scoped interceptors will execute before the spec interceptors.
*
* @since 2.4
*/
@Beta
public void addCleanupInterceptor(IMethodInterceptor interceptor) {
cleanupInterceptors.add(interceptor);
}

/**
* @since 2.4
*/
@Beta
public List<IMethodInterceptor> getInitializerInterceptors() {
return initializerInterceptors;
}

/**
* Adds a initializer interceptor for this feature.
* <p>
* The feature-scoped interceptors will execute before the spec interceptors.
*
* @since 2.4
*/
@Beta
public void addInitializerInterceptor(IMethodInterceptor interceptor) {
initializerInterceptors.add(interceptor);
}

/**
* Allows to intercept initializer, setup, and cleanup methods in the scope of a single feature.
* <p>
* You need to locate the method you want to intercept from the {@link SpecInfo} or its parent and use its {@link MethodInfo} as key.
* <pre>{@code
* visitFeatureAnnotations(MyAnnotation an, FeatureInfo featureInfo) {
* featureInfo.addScopedMethodInterceptor(featureInfo.getParent().getInitializerMethod(), invocation -> invocation.proceed());
* }
* }
* </pre>
* <p>
* Only use this if you absolutely must intercept the method invocation itself, otherwise prefer to use one of
* {@link #addInitializerInterceptor(IMethodInterceptor)},
* {@link #addSetupInterceptor(IMethodInterceptor)},
* {@link #addCleanupInterceptor(IMethodInterceptor)}.
* <p>
* The feature-scoped interceptors will execute before the spec interceptors.
*
* @since 2.4
*/
@Beta
public void addScopedMethodInterceptor(MethodInfo targetMethod, IMethodInterceptor interceptor) {
Checks.checkArgument(
targetMethod.getKind().isFeatureScopedFixtureMethod() || targetMethod.getKind() == MethodKind.INITIALIZER,
() -> "Only feature scoped initializer and fixture methods can be intercepted, but was: " + targetMethod.getKind()
);
scopedMethodInterceptors.computeIfAbsent(targetMethod, __ -> new ArrayList<>()).add(interceptor);
}

/**
* @since 2.4
*/
@Beta
public List<IMethodInterceptor> getScopedMethodInterceptors(MethodInfo targetMethod) {
return scopedMethodInterceptors.getOrDefault(targetMethod, Collections.emptyList());
}

public List<IMethodInterceptor> getIterationInterceptors() {
return iterationInterceptors;
}
Expand Down

0 comments on commit 31fd34c

Please sign in to comment.