Skip to content

Commit 3370650

Browse files
authored
Make @ConfineMetaClassChanges repeatable (#1187)
closes #1030
1 parent 33508ca commit 3370650

File tree

6 files changed

+102
-42
lines changed

6 files changed

+102
-42
lines changed

docs/extensions.adoc

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -377,25 +377,7 @@ To confine meta class changes to the scope of a feature method or spec class, us
377377

378378
[source,groovy]
379379
----
380-
@Stepwise
381-
class FooSpec extends Specification {
382-
@ConfineMetaClassChanges([String])
383-
def "I run first"() {
384-
when:
385-
String.metaClass.someMethod = { delegate }
386-
387-
then:
388-
String.metaClass.hasMetaMethod('someMethod')
389-
}
390-
391-
def "I run second"() {
392-
when:
393-
"Foo".someMethod()
394-
395-
then:
396-
thrown(MissingMethodException)
397-
}
398-
}
380+
include::{sourcedir}/extension/ConfineMetaClassChangesDocSpec.groovy[tag=example]
399381
----
400382

401383
When applied to a spec class, the meta classes are restored to the state that they were in before `setupSpec` was executed,
@@ -404,6 +386,9 @@ after `cleanupSpec` is executed.
404386
When applied to a feature method, the meta classes are restored to as they were after `setup` was executed,
405387
before `cleanup` is executed.
406388

389+
To confine meta class changes for multiple classes, you can either give multiple classes to the `value` attribute
390+
of the annotation or you can apply the annotation multiple times to the same target.
391+
407392
CAUTION: Temporarily changing the meta classes is only safe when specs are
408393
run in a single thread per JVM. Even though many execution environments do limit themselves to one thread
409394
per JVM, keep in mind that Spock cannot enforce this.

docs/release_notes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ include::include.adoc[]
1212
- `AbstractAnnotationDrivenExtension` is now deprecated and its logic was moved to `default` methods of
1313
`IAnnotationDrivenExtension` which should be implemented directly now instead of extending the abstract class.
1414

15+
- `@ConfineMetaClassChanges` is now repeatable
16+
1517

1618
== 2.0-M3 (2020-06-11)
1719

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,34 @@
2020
import org.spockframework.runtime.model.FeatureInfo;
2121
import org.spockframework.runtime.model.IInterceptable;
2222
import org.spockframework.runtime.model.SpecInfo;
23-
import org.spockframework.util.CollectionUtil;
24-
2523
import spock.util.mop.ConfineMetaClassChanges;
2624

25+
import java.util.Arrays;
26+
import java.util.List;
27+
28+
import static java.util.stream.Collectors.toSet;
29+
2730
/**
2831
* @author Luke Daley
2932
* @author Peter Niederwieser
3033
*/
3134
public class ConfineMetaClassChangesExtension implements IAnnotationDrivenExtension<ConfineMetaClassChanges> {
3235
@Override
33-
public void visitSpecAnnotation(ConfineMetaClassChanges annotation, SpecInfo spec) {
34-
addInterceptor(annotation, spec.getBottomSpec());
36+
public void visitSpecAnnotations(List<ConfineMetaClassChanges> annotations, SpecInfo spec) {
37+
addInterceptor(annotations, spec.getBottomSpec());
3538
}
3639

3740
@Override
38-
public void visitFeatureAnnotation(ConfineMetaClassChanges annotation, FeatureInfo feature) {
39-
addInterceptor(annotation, feature.getFeatureMethod());
41+
public void visitFeatureAnnotations(List<ConfineMetaClassChanges> annotations, FeatureInfo feature) {
42+
addInterceptor(annotations, feature.getFeatureMethod());
4043
}
4144

42-
private void addInterceptor(ConfineMetaClassChanges annotation, IInterceptable interceptable) {
43-
interceptable.addInterceptor(new ConfineMetaClassChangesInterceptor(CollectionUtil.asSet(annotation.value())));
45+
private void addInterceptor(List<ConfineMetaClassChanges> annotations, IInterceptable interceptable) {
46+
interceptable.addInterceptor(new ConfineMetaClassChangesInterceptor(
47+
annotations
48+
.stream()
49+
.map(ConfineMetaClassChanges::value)
50+
.flatMap(Arrays::stream)
51+
.collect(toSet())));
4452
}
4553
}

spock-core/src/main/java/spock/util/mop/ConfineMetaClassChanges.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.spockframework.runtime.extension.ExtensionAnnotation;
2222
import org.spockframework.runtime.extension.builtin.ConfineMetaClassChangesExtension;
23+
import org.spockframework.util.Beta;
2324

2425
/**
2526
* Confines any changes made to the meta classes of the specified classes to the
@@ -35,15 +36,26 @@
3536
* <p>If a feature method is annotated, the meta classes are restored to as they
3637
* were after <tt>setup()</tt> was executed, before <tt>cleanup() is executed.
3738
* For a data-driven feature method, meta classes are restored after each iteration.
38-
*
39+
*
3940
* @author Luke Daley
4041
*/
4142
@Retention(RetentionPolicy.RUNTIME)
4243
@Target({ElementType.TYPE, ElementType.METHOD})
4344
@ExtensionAnnotation(ConfineMetaClassChangesExtension.class)
45+
@Repeatable(ConfineMetaClassChanges.Container.class)
4446
public @interface ConfineMetaClassChanges {
4547
/**
4648
* The classes whose meta class changes are to be confined.
4749
*/
4850
Class<?>[] value();
51+
52+
/**
53+
* @since 2.0
54+
*/
55+
@Beta
56+
@Retention(RetentionPolicy.RUNTIME)
57+
@Target({ElementType.TYPE, ElementType.METHOD})
58+
@interface Container {
59+
ConfineMetaClassChanges[] value();
60+
}
4961
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.spockframework.docs.extension
2+
3+
import spock.lang.Specification
4+
import spock.lang.Stepwise
5+
import spock.util.mop.ConfineMetaClassChanges
6+
7+
// tag::example[]
8+
@Stepwise
9+
class ConfineMetaClassChangesDocSpec extends Specification {
10+
@ConfineMetaClassChanges(String)
11+
def "I run first"() {
12+
when:
13+
String.metaClass.someMethod = { delegate }
14+
15+
then:
16+
String.metaClass.hasMetaMethod('someMethod')
17+
}
18+
19+
def "I run second"() {
20+
when:
21+
"Foo".someMethod()
22+
23+
then:
24+
thrown(MissingMethodException)
25+
}
26+
}
27+
// end::example[]

spock-specs/src/test/groovy/spock/util/mop/ConfineMetaClassChangesSpec.groovy

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,28 +23,28 @@ import spock.lang.*
2323
*/
2424
@Stepwise
2525
class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
26-
26+
2727
def setupSpec() {
2828
newValue = 1
2929
}
30-
30+
3131
def "change the metaclass"() {
3232
expect: value == 1
3333
when: newValue = 2
3434
then: value == 2
3535
}
36-
36+
3737
def "value is still changed value"() {
3838
expect: value == 2
3939
}
40-
40+
4141
@ConfineMetaClassChanges(String)
4242
def "change it again, but with restore annotation"() {
4343
expect: value == 2
4444
when: newValue = 3
4545
then: value == 3
4646
}
47-
47+
4848
def "changes have been reverted"() {
4949
expect: value == 2
5050
}
@@ -56,7 +56,7 @@ class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
5656

5757
@ConfineMetaClassChanges([String, Integer])
5858
def "change more than one type"() {
59-
expect:
59+
expect:
6060
value == 2
6161

6262
when:
@@ -67,7 +67,7 @@ class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
6767
getValue("") == 3
6868
getValue(1) == 3
6969
}
70-
70+
7171
def "changes are reverted on both"() {
7272
expect:
7373
getValue("") == 2
@@ -78,7 +78,33 @@ class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
7878
then:
7979
thrown(MissingMethodException)
8080
}
81-
81+
82+
@ConfineMetaClassChanges(String)
83+
@ConfineMetaClassChanges(Integer)
84+
def "change more than one type with multiple annotations"() {
85+
expect:
86+
value == 2
87+
88+
when:
89+
setNewValue(3, String)
90+
setNewValue(3, Integer)
91+
92+
then:
93+
getValue("") == 3
94+
getValue(1) == 3
95+
}
96+
97+
def "changes are still reverted on both"() {
98+
expect:
99+
getValue("") == 2
100+
101+
when:
102+
getValue(1)
103+
104+
then:
105+
thrown(MissingMethodException)
106+
}
107+
82108
def "exercise a spec level restore"() {
83109
setup:
84110
newValue = 2
@@ -112,7 +138,7 @@ class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
112138
"" | 3
113139
"@ConfineMetaClassChanges(String)" | 2
114140
}
115-
141+
116142
@ConfineMetaClassChanges(String)
117143
def "meta classes are restored after each iteration"() {
118144
expect:
@@ -126,27 +152,27 @@ class ConfineMetaClassChangesSpec extends EmbeddedSpecification {
126152
where:
127153
i << [2,2]
128154
}
129-
155+
130156
def "meta class was restored after parameterised"() {
131157
expect:
132158
value == 2
133159
}
134-
160+
135161
@ConfineMetaClassChanges([])
136162
def "annotation with empty list/array value doesn't cause an error"(){
137163
expect: true
138164
}
139-
165+
140166
def cleanupSpec() {
141167
[String, Integer].each {
142168
GroovySystem.metaClassRegistry.removeMetaClass(it)
143169
}
144170
}
145-
171+
146172
static setNewValue(value, type = String) {
147173
type.metaClass.getIsolateMetaClassExtensionValue = { -> value }
148174
}
149-
175+
150176
static getValue(seed = "") {
151177
seed.getIsolateMetaClassExtensionValue()
152178
}

0 commit comments

Comments
 (0)