Skip to content

Commit 33508ca

Browse files
authored
Enable batch-processing for repeatable annotations (#1198)
1 parent 6ddae64 commit 33508ca

27 files changed

+248
-116
lines changed

docs/extensions.adoc

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -665,46 +665,66 @@ methods in the interface, so that only the needed ones need to be overridden.
665665

666666
=== Annotation Driven Local Extensions
667667

668-
To create an annotation driven local extension you need to create a class that implements the interface
669-
`IAnnotationDrivenExtension`. As type argument to the interface you need to supply an annotation class that has
670-
`@Retention` set to `RUNTIME`, `@Target` set to one or more of `FIELD`, `METHOD` and `TYPE` - depending on where you
668+
To create an annotation driven local extension you need to create a class implementing the interface
669+
`IAnnotationDrivenExtension`. As type argument to the interface you need to supply an annotation class having
670+
`@Retention` set to `RUNTIME`, `@Target` set to one or more of `FIELD`, `METHOD`, and `TYPE` - depending on where you
671671
want your annotation to be applicable - and `@ExtensionAnnotation` applied, with the `IAnnotationDrivenExtension` class
672672
as argument. Of course the annotation class can have some attributes with which the user can further configure the
673-
behaviour of the extension for each annotation application. For convenience there is also the class
674-
`AbstractAnnotationDrivenExtension`, which provides empty implementations for all methods in the interface, so that only
675-
the needed ones need to be overridden.
673+
behaviour of the extension for each annotation application.
676674

677675
Your annotation can be applied to a specification, a feature method, a fixture method or a field. On all other places
678676
like helper methods or other places if the `@Target` is set accordingly, the annotation will be ignored and has no
679-
effect other than being visible in the source code.
677+
effect other than being visible in the source code, except you check its existence in other places yourself.
680678

681679
Since Spock 2.0 your annotation can also be defined as `@Repeatable` and applied multiple times to the same target.
682-
Spock will handle this appropriately and call your extension once for each annotation. If you want a repeatable
683-
annotation that is compatible with Spock before 2.0 you need to make the container annotation an extension annotation
684-
itself and handle all cases accordingly, but you need to make sure to only handle the container annotation if Spock
685-
version is before 2.0 or your annotations might be handled twice. Be aware that the repeatable annotation can be
686-
attached to the target directly, inside the container annotation or even both if the user added the container
687-
annotation manually and also attached one annotation directly.
688-
689-
`IAnnotationDrivenExtension` has the following five methods, where in each you can prepare a specification with your
680+
`IAnnotationDrivenExtension` has `visit...Annotations` methods that are called by Spock with all annotations of the
681+
extension applied to the same target. Their default implementations will then call the respective singular
682+
`visit...Annotation` method once for each annotation. If you want a repeatable annotation that is compatible with
683+
Spock before 2.0 you need to make the container annotation an extension annotation itself and handle all cases
684+
accordingly, but you need to make sure to only handle the container annotation if Spock version is before 2.0,
685+
or your annotations might be handled twice. Be aware that the repeatable annotation can be attached to the target
686+
directly, inside the container annotation or even both if the user added the container annotation manually
687+
and also attached one annotation directly.
688+
689+
`IAnnotationDrivenExtension` has the following nine methods, where in each you can prepare a specification with your
690690
extension magic, like attaching interceptors to various interception points as described in the chapter
691691
<<Interceptors>>:
692692

693+
`visitSpecAnnotations(List<T> annotations, SpecInfo spec)`::
694+
This is called once for each specification where the annotation is applied one or multiple times
695+
with the annotation instances as first parameter and the specification info object as second parameter.
696+
The default implementation calls `visitSpecAnnotation` once for each given annotation.
697+
693698
`visitSpecAnnotation(T annotation, SpecInfo spec)`::
694-
This is called once for each specification where the annotation is applied with the annotation instance as first
695-
parameter and the specification info object as second parameter.
699+
This is used as singular delegate for `visitSpecAnnotations` and is otherwise not called by Spock directly.
700+
The default implementation throws an exception.
696701

697-
`visitFeatureAnnotation(T annotation, FeatureInfo feature)`::
698-
This is called once for each feature method where the annotation is applied with the annotation instance as first
699-
parameter and the feature info object as second parameter.
702+
`visitFieldAnnotations(List<T> annotations, FieldInfo field)`::
703+
This is called once for each field where the annotation is applied one or multiple times
704+
with the annotation instances as first parameter and the field info object as second parameter.
705+
The default implementation calls `visitFieldAnnotation` once for each given annotation.
706+
707+
`visitFieldAnnotation(T annotation, FieldInfo field)`::
708+
This is used as singular delegate for `visitFieldAnnotations` and is otherwise not called by Spock directly.
709+
The default implementation throws an exception.
710+
711+
`visitFixtureAnnotations(List<T> annotations, MethodInfo fixtureMethod)`::
712+
This is called once for each fixture method where the annotation is applied one or multiple times
713+
with the annotation instances as first parameter and the fixture method info object as second parameter.
714+
The default implementation calls `visitFixtureAnnotation` once for each given annotation.
700715

701716
`visitFixtureAnnotation(T annotation, MethodInfo fixtureMethod)`::
702-
This is called once for each fixture method where the annotation is applied with the annotation instance as first
703-
parameter and the fixture method info object as second parameter.
717+
This is used as singular delegate for `visitFixtureAnnotations` and is otherwise not called by Spock directly.
718+
The default implementation throws an exception.
704719

705-
`visitFieldAnnotation(T annotation, FieldInfo field)`::
706-
This is called once for each field where the annotation is applied with the annotation instance as first parameter and
707-
the field info object as second parameter.
720+
`visitFeatureAnnotations(List<T> annotations, FeatureInfo feature)`::
721+
This is called once for each feature method where the annotation is applied one or multiple times
722+
with the annotation instances as first parameter and the feature info object as second parameter.
723+
The default implementation calls `visitFeatureAnnotation` once for each given annotation.
724+
725+
`visitFeatureAnnotation(T annotation, FeatureInfo feature)`::
726+
This is used as singular delegate for `visitFeatureAnnotations` and is otherwise not called by Spock directly.
727+
The default implementation throws an exception.
708728

709729
`visitSpec(SpecInfo spec)`::
710730
This is called once for each specification within which the annotation is applied to at least one of the supported

docs/release_notes.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ include::include.adoc[]
33

44
== 2.0-M4 (tbd)
55

6-
- Add a way to register `ConfigurationObject` globally without the need for a Global Extension
6+
- Add a way to register `ConfigurationObject` globally without the need for a global extension.
77

88
- Annotations for local extensions can now be defined as `@Repeatable` and applied multiple times to the same target.
9-
Spock will handle this appropriately and call the extension once for each annotation.
9+
Spock will handle this appropriately and call the extensions `visitSpecAnnotations` method with all annotations.
10+
If these methods are not overwritten, they forward to the usual `visitSpecAnnotation` methods once for each annotation.
11+
12+
- `AbstractAnnotationDrivenExtension` is now deprecated and its logic was moved to `default` methods of
13+
`IAnnotationDrivenExtension` which should be implemented directly now instead of extending the abstract class.
1014

1115

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

spock-core/src/main/java/org/spockframework/runtime/ExtensionRunner.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
import java.lang.annotation.Annotation;
2121
import java.util.*;
22+
import java.util.stream.Collectors;
23+
24+
import static java.util.stream.Collectors.toList;
2225

2326
/**
2427
* Runs global and local extensions for a spec.
@@ -84,9 +87,12 @@ private void doRunAnnotationDrivenExtensions(Iterable<MethodInfo> nodes) {
8487
private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node) {
8588
RepeatedExtensionAnnotations repeatedExtensionAnnotations = node.getAnnotation(RepeatedExtensionAnnotations.class);
8689

87-
List<Annotation> annotations;
90+
List<List<Annotation>> annotations;
8891
if (repeatedExtensionAnnotations == null) {
89-
annotations = Arrays.asList(node.getAnnotations());
92+
annotations = Arrays
93+
.stream(node.getAnnotations())
94+
.map(Collections::singletonList)
95+
.collect(toList());
9096
} else {
9197
annotations = new ArrayList<>();
9298
List<Class<? extends Annotation>> repeatedAnnotations = Arrays.asList(repeatedExtensionAnnotations.value());
@@ -96,34 +102,35 @@ private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node) {
96102
.filter(annotation -> repeatedAnnotations
97103
.stream()
98104
.noneMatch(repeatedAnnotation -> repeatedAnnotation.isInstance(annotation)))
105+
.map(Collections::singletonList)
99106
.forEach(annotations::add);
100107

101108
// add all annotations marked as repeated
102109
for (Class<? extends Annotation> repeatedAnnotation : repeatedAnnotations) {
103-
annotations.addAll(Arrays.asList(node.getAnnotationsByType(repeatedAnnotation)));
110+
annotations.add(Arrays.asList(node.getAnnotationsByType(repeatedAnnotation)));
104111
}
105112
}
106113

107114
doRunAnnotationDrivenExtensions(node, annotations);
108115
}
109116

110117
@SuppressWarnings("unchecked")
111-
private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node, List<Annotation> annotations) {
112-
for (Annotation ann : annotations) {
113-
ExtensionAnnotation extAnn = ann.annotationType().getAnnotation(ExtensionAnnotation.class);
118+
private void doRunAnnotationDrivenExtensions(NodeInfo<?, ?> node, List<List<Annotation>> annotations) {
119+
for (List<Annotation> ann : annotations) {
120+
ExtensionAnnotation extAnn = ann.get(0).annotationType().getAnnotation(ExtensionAnnotation.class);
114121
if (extAnn == null) continue;
115122
IAnnotationDrivenExtension extension = getOrCreateExtension(extAnn.value());
116123
if (node instanceof SpecInfo) {
117-
extension.visitSpecAnnotation(ann, (SpecInfo) node);
124+
extension.visitSpecAnnotations(ann, (SpecInfo) node);
118125
} else if (node instanceof MethodInfo) {
119126
MethodInfo method = (MethodInfo) node;
120127
if (method.getKind() == MethodKind.FEATURE) {
121-
extension.visitFeatureAnnotation(ann, method.getFeature());
128+
extension.visitFeatureAnnotations(ann, method.getFeature());
122129
} else {
123-
extension.visitFixtureAnnotation(ann, method);
130+
extension.visitFixtureAnnotations(ann, method);
124131
}
125132
} else {
126-
extension.visitFieldAnnotation(ann, (FieldInfo) node);
133+
extension.visitFieldAnnotations(ann, (FieldInfo) node);
127134
}
128135
}
129136
}

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

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,14 @@
1616

1717
package org.spockframework.runtime.extension;
1818

19-
import org.spockframework.runtime.InvalidSpecException;
20-
import org.spockframework.runtime.model.*;
21-
2219
import java.lang.annotation.Annotation;
2320

2421
/**
2522
*
2623
* @author Peter Niederwieser
24+
* @deprecated The logic of this class has moved to default methods in {@link IAnnotationDrivenExtension},
25+
* implement that interface directly instead of extending this class
2726
*/
27+
@Deprecated
2828
public abstract class AbstractAnnotationDrivenExtension<T extends Annotation> implements IAnnotationDrivenExtension<T> {
29-
@Override
30-
public void visitSpecAnnotation(T annotation, SpecInfo spec) {
31-
throw new InvalidSpecException("@%s may not be applied to Specs")
32-
.withArgs(annotation.annotationType().getSimpleName());
33-
}
34-
35-
@Override
36-
public void visitFeatureAnnotation(T annotation, FeatureInfo feature) {
37-
throw new InvalidSpecException("@%s may not be applied to feature methods")
38-
.withArgs(annotation.annotationType().getSimpleName());
39-
}
40-
41-
@Override
42-
public void visitFixtureAnnotation(T annotation, MethodInfo fixtureMethod) {
43-
throw new InvalidSpecException("@%s may not be applied to fixture methods")
44-
.withArgs(annotation.annotationType().getSimpleName());
45-
}
46-
47-
@Override
48-
public void visitFieldAnnotation(T annotation, FieldInfo field) {
49-
throw new InvalidSpecException("@%s may not be applied to fields")
50-
.withArgs(annotation.annotationType().getSimpleName());
51-
}
52-
53-
@Override
54-
public void visitSpec(SpecInfo spec) {} // do nothing
5529
}

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

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,143 @@
1717
package org.spockframework.runtime.extension;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.util.List;
2021

22+
import org.spockframework.runtime.InvalidSpecException;
2123
import org.spockframework.runtime.model.*;
2224

2325
/**
24-
*
2526
* @author Peter Niederwieser
2627
*/
2728
public interface IAnnotationDrivenExtension<T extends Annotation> {
28-
void visitSpecAnnotation(T annotation, SpecInfo spec);
29-
void visitFeatureAnnotation(T annotation, FeatureInfo feature);
30-
void visitFixtureAnnotation(T annotation, MethodInfo fixtureMethod);
31-
void visitFieldAnnotation(T annotation, FieldInfo field);
32-
void visitSpec(SpecInfo spec);
29+
/**
30+
* Handles the annotation when applied to a specification one or multiple times.
31+
* The default implementation of this method calls {@link #visitSpecAnnotation(Annotation, SpecInfo)}
32+
* once for each given annotation.
33+
*
34+
* @since 2.0
35+
* @param annotations the annotations found on the specification
36+
* @param spec the annotated specification
37+
*/
38+
default void visitSpecAnnotations(List<T> annotations, SpecInfo spec) {
39+
for (T annotation : annotations) {
40+
visitSpecAnnotation(annotation, spec);
41+
}
42+
}
43+
44+
/**
45+
* Handles the annotation when applied to a specification.
46+
* The default implementation of this method throws an {@link InvalidSpecException}.
47+
* This method is not called by Spock directly, but only by the default implementation of
48+
* {@link #visitSpecAnnotations(List, SpecInfo)}, so if you have overwritten that method,
49+
* there is no need to overwrite this one too except if you want to call it yourself.
50+
*
51+
* @param annotation the annotation found on the specification
52+
* @param spec the annotated specification
53+
*/
54+
default void visitSpecAnnotation(T annotation, SpecInfo spec) {
55+
throw new InvalidSpecException("@%s may not be applied to Specs")
56+
.withArgs(annotation.annotationType().getSimpleName());
57+
}
58+
59+
/**
60+
* Handles the annotation when applied to a field of a specification one or multiple times.
61+
* The default implementation of this method calls {@link #visitFieldAnnotation(Annotation, FieldInfo)}
62+
* once for each given annotation.
63+
*
64+
* @since 2.0
65+
* @param annotations the annotations found on the field
66+
* @param field the annotated field
67+
*/
68+
default void visitFieldAnnotations(List<T> annotations, FieldInfo field) {
69+
for (T annotation : annotations) {
70+
visitFieldAnnotation(annotation, field);
71+
}
72+
}
73+
74+
/**
75+
* Handles the annotation when applied to a field of a specification.
76+
* The default implementation of this method throws an {@link InvalidSpecException}.
77+
* This method is not called by Spock directly, but only by the default implementation of
78+
* {@link #visitFieldAnnotations(List, FieldInfo)}, so if you have overwritten that method,
79+
* there is no need to overwrite this one too except if you want to call it yourself.
80+
*
81+
* @param annotation the annotation found on the field
82+
* @param field the annotated field
83+
*/
84+
default void visitFieldAnnotation(T annotation, FieldInfo field) {
85+
throw new InvalidSpecException("@%s may not be applied to fields")
86+
.withArgs(annotation.annotationType().getSimpleName());
87+
}
88+
89+
/**
90+
* Handles the annotation when applied to a fixture method of a specification one or multiple times.
91+
* The default implementation of this method calls {@link #visitFixtureAnnotation(Annotation, MethodInfo)}
92+
* once for each given annotation.
93+
*
94+
* @since 2.0
95+
* @param annotations the annotations found on the fixture method
96+
* @param fixtureMethod the annotated fixture method
97+
*/
98+
default void visitFixtureAnnotations(List<T> annotations, MethodInfo fixtureMethod) {
99+
for (T annotation : annotations) {
100+
visitFixtureAnnotation(annotation, fixtureMethod);
101+
}
102+
}
103+
104+
/**
105+
* Handles the annotation when applied to a fixture method of a specification.
106+
* The default implementation of this method throws an {@link InvalidSpecException}.
107+
* This method is not called by Spock directly, but only by the default implementation of
108+
* {@link #visitFixtureAnnotations(List, MethodInfo)}, so if you have overwritten that method,
109+
* there is no need to overwrite this one too except if you want to call it yourself.
110+
*
111+
* @param annotation the annotation found on the fixture method
112+
* @param fixtureMethod the annotated fixture method
113+
*/
114+
default void visitFixtureAnnotation(T annotation, MethodInfo fixtureMethod) {
115+
throw new InvalidSpecException("@%s may not be applied to fixture methods")
116+
.withArgs(annotation.annotationType().getSimpleName());
117+
}
118+
119+
/**
120+
* Handles the annotation when applied to a feature method of a specification one or multiple times.
121+
* The default implementation of this method calls {@link #visitFeatureAnnotation(Annotation, FeatureInfo)}
122+
* once for each given annotation.
123+
*
124+
* @since 2.0
125+
* @param annotations the annotations found on the feature method
126+
* @param feature the annotated feature method
127+
*/
128+
default void visitFeatureAnnotations(List<T> annotations, FeatureInfo feature) {
129+
for (T annotation : annotations) {
130+
visitFeatureAnnotation(annotation, feature);
131+
}
132+
}
133+
134+
/**
135+
* Handles the annotation when applied to a feature method of a specification.
136+
* The default implementation of this method throws an {@link InvalidSpecException}.
137+
* This method is not called by Spock directly, but only by the default implementation of
138+
* {@link #visitFeatureAnnotations(List, FeatureInfo)}, so if you have overwritten that method,
139+
* there is no need to overwrite this one too except if you want to call it yourself.
140+
*
141+
* @param annotation the annotation found on the feature method
142+
* @param feature the annotated feature method
143+
*/
144+
default void visitFeatureAnnotation(T annotation, FeatureInfo feature) {
145+
throw new InvalidSpecException("@%s may not be applied to feature methods")
146+
.withArgs(annotation.annotationType().getSimpleName());
147+
}
148+
149+
/**
150+
* Does concluding actions after the single annotations were handled.
151+
* This method is called after all the other {@code visit...} methods of this interface to do any concluding
152+
* or combining actions that are necessary. It is called once for each extension that has at least one
153+
* of its annotation somewhere on the given specification.
154+
*
155+
* @param spec the annotated specification
156+
*/
157+
default void visitSpec(SpecInfo spec) {
158+
}
33159
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
package org.spockframework.runtime.extension.builtin;
1616

17-
import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension;
17+
import org.spockframework.runtime.extension.IAnnotationDrivenExtension;
1818
import org.spockframework.runtime.model.FieldInfo;
1919
import org.spockframework.runtime.model.SpecInfo;
2020

@@ -23,7 +23,7 @@
2323
/**
2424
* @author Peter Niederwieser
2525
*/
26-
public class AutoCleanupExtension extends AbstractAnnotationDrivenExtension<AutoCleanup> {
26+
public class AutoCleanupExtension implements IAnnotationDrivenExtension<AutoCleanup> {
2727
private final AutoCleanupInterceptor sharedFieldInterceptor = new AutoCleanupInterceptor();
2828
private final AutoCleanupInterceptor instanceFieldInterceptor = new AutoCleanupInterceptor();
2929

0 commit comments

Comments
 (0)