Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature scoped interceptors... #1844

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 7 additions & 36 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
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]
----
Comment on lines +1016 to +1019
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just leave this out?
It does not really add much value.

Copy link
Member Author

@leonard84 leonard84 Dec 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, we'd need to replace the I in the examples. It is only separate because I couldn't find a way to get rid of the indent for the other includes otherwise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to replace it?
Or do I expect too much transfer ability from the reader that he knows that is a class implementing the listener interface?
Then we could keep it of course. :-D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the worst case, they could look at the doc-spec where this is coming from. :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do I expect too much transfer ability from the reader that he knows that is a class implementing the listener interface?

Yes, I think for new readers it would be confusing.

In the worst case, they could look at the doc-spec where this is coming from. :-)

🤔 It would be nice to automatically have a link to the sources under the code snippets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your wish is my command: #1904 :-)

.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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
Vampire marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 @@
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<>();
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
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 @@
blocks.add(block);
}

/**
* @since 2.4
*/
@Beta
public List<IMethodInterceptor> getSetupInterceptors() {
Vampire marked this conversation as resolved.
Show resolved Hide resolved
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()

Check warning on line 199 in spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java

View check run for this annotation

Codecov / codecov/patch

spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java#L199

Added line #L199 was not covered by tests
);
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