Skip to content

Commit 556f9b0

Browse files
committed
Make Spock specs atomic if certain criteria are met
1 parent 310fd0b commit 556f9b0

13 files changed

+611
-30
lines changed

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<pitest.version>1.13.2</pitest.version>
2727
<cucumber.version>5.0.0</cucumber.version>
2828
<spock.version>2.3-groovy-4.0</spock.version>
29+
<junit4.version>4.13.2</junit4.version>
2930
<groovy.version>4.0.11</groovy.version>
3031
</properties>
3132

@@ -199,6 +200,12 @@
199200
<artifactId>spock-core</artifactId>
200201
<scope>test</scope>
201202
</dependency>
203+
<dependency>
204+
<groupId>junit</groupId>
205+
<artifactId>junit</artifactId>
206+
<version>${junit4.version}</version>
207+
<scope>test</scope>
208+
</dependency>
202209
</dependencies>
203210

204211
<build>

src/main/java/org/pitest/junit5/JUnit5TestUnitFinder.java

Lines changed: 199 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@
1414
*/
1515
package org.pitest.junit5;
1616

17+
import java.lang.annotation.Annotation;
18+
import java.lang.reflect.AnnotatedElement;
19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Method;
1721
import java.util.ArrayList;
22+
import java.util.Arrays;
1823
import java.util.Collection;
24+
import java.util.LinkedHashSet;
1925
import java.util.List;
2026
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.Set;
2129
import java.util.concurrent.ConcurrentHashMap;
2230
import java.util.concurrent.atomic.AtomicReference;
2331
import java.util.concurrent.locks.ReentrantLock;
32+
import java.util.function.Predicate;
2433

2534
import static java.util.Collections.emptyList;
2635
import static java.util.Collections.synchronizedList;
@@ -32,13 +41,16 @@
3241
import org.junit.platform.engine.TestExecutionResult;
3342
import org.junit.platform.engine.UniqueId;
3443
import org.junit.platform.engine.discovery.DiscoverySelectors;
44+
import org.junit.platform.engine.support.descriptor.ClassSource;
3545
import org.junit.platform.engine.support.descriptor.MethodSource;
3646
import org.junit.platform.launcher.Launcher;
3747
import org.junit.platform.launcher.TagFilter;
3848
import org.junit.platform.launcher.TestExecutionListener;
3949
import org.junit.platform.launcher.TestIdentifier;
4050
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
4151
import org.junit.platform.launcher.core.LauncherFactory;
52+
import org.pitest.functional.FCollection;
53+
import org.pitest.reflection.Reflection;
4254
import org.pitest.testapi.Description;
4355
import org.pitest.testapi.NullExecutionListener;
4456
import org.pitest.testapi.TestGroupConfig;
@@ -51,6 +63,22 @@
5163
* @author Tobias Stadler
5264
*/
5365
public class JUnit5TestUnitFinder implements TestUnitFinder {
66+
private static final Optional<Class<?>> SPECIFICATION =
67+
findClass("spock.lang.Specification");
68+
private static final Optional<Class<? extends Annotation>> BEFORE_ALL =
69+
findClass("org.junit.jupiter.api.BeforeAll");
70+
private static final Optional<Class<? extends Annotation>> BEFORE_CLASS =
71+
findClass("org.junit.BeforeClass");
72+
private static final Optional<Class<? extends Annotation>> AFTER_ALL =
73+
findClass("org.junit.jupiter.api.AfterAll");
74+
private static final Optional<Class<? extends Annotation>> AFTER_CLASS =
75+
findClass("org.junit.AfterClass");
76+
private static final Optional<Class<? extends Annotation>> CLASS_RULE =
77+
findClass("org.junit.ClassRule");
78+
private static final Optional<Class<? extends Annotation>> SHARED =
79+
findClass("spock.lang.Shared");
80+
private static final Optional<Class<? extends Annotation>> STEPWISE =
81+
findClass("spock.lang.Stepwise");
5482

5583
private final TestGroupConfig testGroupConfig;
5684

@@ -99,6 +127,15 @@ public List<TestUnit> findTestUnits(Class<?> clazz, TestUnitExecutionListener ex
99127
.collect(toList());
100128
}
101129

130+
@SuppressWarnings("unchecked")
131+
private static <T> Optional<Class<? extends T>> findClass(String className) {
132+
try {
133+
return Optional.of(((Class<? extends T>) Class.forName(className)));
134+
} catch (final ClassNotFoundException ex) {
135+
return Optional.empty();
136+
}
137+
}
138+
102139
private class TestIdentifierListener implements TestExecutionListener {
103140
private final Class<?> testClass;
104141
private final TestUnitExecutionListener l;
@@ -148,31 +185,28 @@ List<TestIdentifier> getIdentifiers() {
148185

149186
@Override
150187
public void executionStarted(TestIdentifier testIdentifier) {
188+
if (shouldTreatAsOneUnit(testIdentifier)) {
189+
if (hasClassSource(testIdentifier)) {
190+
if (serializeExecution) {
191+
lock(testIdentifier);
192+
}
193+
194+
l.executionStarted(new Description(testIdentifier.getUniqueId(), testClass), true);
195+
identifiers.add(testIdentifier);
196+
}
197+
return;
198+
}
199+
151200
if (testIdentifier.isTest()) {
152201
// filter out testMethods
153202
if (includedTestMethods != null && !includedTestMethods.isEmpty()
154-
&& testIdentifier.getSource().isPresent()
155-
&& testIdentifier.getSource().get() instanceof MethodSource
156-
&& !includedTestMethods.contains(((MethodSource)testIdentifier.getSource().get()).getMethodName())) {
203+
&& hasMethodSource(testIdentifier)
204+
&& !includedTestMethods.contains(((MethodSource) testIdentifier.getSource().get()).getMethodName())) {
157205
return;
158206
}
159207

160208
if (serializeExecution) {
161-
coverageSerializers.compute(testIdentifier.getUniqueIdObject(), (uniqueId, lock) -> {
162-
if (lock != null) {
163-
throw new AssertionError("No lock should be present");
164-
}
165-
166-
// find the serializer to lock the test on
167-
// if there is a parent test locked, use the lock for its children if not,
168-
// use the root serializer
169-
return testIdentifier
170-
.getParentIdObject()
171-
.map(parentCoverageSerializers::get)
172-
.map(lockRef -> lockRef.updateAndGet(parentLock ->
173-
parentLock == null ? new ReentrantLock() : parentLock))
174-
.orElse(rootCoverageSerializer);
175-
}).lock();
209+
lock(testIdentifier);
176210
// record a potential serializer for child tests to lock on
177211
parentCoverageSerializers.put(testIdentifier.getUniqueIdObject(), new AtomicReference<>());
178212
}
@@ -184,7 +218,17 @@ public void executionStarted(TestIdentifier testIdentifier) {
184218

185219
@Override
186220
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
187-
// Classes with failing BeforeAlls never start execution and identify as 'containers' not 'tests'
221+
if (shouldTreatAsOneUnit(testIdentifier)) {
222+
if (hasClassSource(testIdentifier)) {
223+
l.executionFinished(new Description(testIdentifier.getUniqueId(), testClass),
224+
testExecutionResult.getStatus() != TestExecutionResult.Status.FAILED);
225+
// unlock the serializer for the finished tests to let the next test continue
226+
unlock(testIdentifier);
227+
}
228+
return;
229+
}
230+
231+
// Jupiter classes with failing BeforeAlls never start execution and identify as 'containers' not 'tests'
188232
if (testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) {
189233
if (!identifiers.contains(testIdentifier)) {
190234
identifiers.add(testIdentifier);
@@ -198,11 +242,145 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult
198242
// forget the potential serializer for child tests
199243
parentCoverageSerializers.remove(testIdentifier.getUniqueIdObject());
200244
// unlock the serializer for the finished tests to let the next test continue
201-
ReentrantLock lock = coverageSerializers.remove(testIdentifier.getUniqueIdObject());
245+
unlock(testIdentifier);
246+
}
247+
}
248+
249+
public void lock(TestIdentifier testIdentifier) {
250+
coverageSerializers.compute(testIdentifier.getUniqueIdObject(), (uniqueId, lock) -> {
202251
if (lock != null) {
203-
lock.unlock();
252+
throw new AssertionError("No lock should be present");
204253
}
254+
255+
// find the serializer to lock the test on
256+
// if there is a parent test locked, use the lock for its children if not,
257+
// use the root serializer
258+
return testIdentifier
259+
.getParentIdObject()
260+
.map(parentCoverageSerializers::get)
261+
.map(lockRef -> lockRef.updateAndGet(parentLock ->
262+
parentLock == null ? new ReentrantLock() : parentLock))
263+
.orElse(rootCoverageSerializer);
264+
}).lock();
265+
}
266+
267+
public void unlock(TestIdentifier testIdentifier) {
268+
ReentrantLock lock = coverageSerializers.remove(testIdentifier.getUniqueIdObject());
269+
if (lock != null) {
270+
lock.unlock();
271+
}
272+
}
273+
274+
private boolean hasClassSource(TestIdentifier testIdentifier) {
275+
return testIdentifier.getSource().filter(ClassSource.class::isInstance).isPresent();
276+
}
277+
278+
private boolean hasMethodSource(TestIdentifier testIdentifier) {
279+
return testIdentifier.getSource().filter(MethodSource.class::isInstance).isPresent();
280+
}
281+
282+
private boolean shouldTreatAsOneUnit(TestIdentifier testIdentifier) {
283+
return shouldTreatSpockSpecificationAsOneUnit(testIdentifier);
284+
}
285+
286+
private boolean shouldTreatSpockSpecificationAsOneUnit(TestIdentifier testIdentifier) {
287+
Optional<Class<?>> optionalTestClass = getTestClass(testIdentifier);
288+
if (!optionalTestClass.isPresent()) {
289+
return false;
290+
}
291+
292+
Class<?> testClass = optionalTestClass.get();
293+
if (!isSpockSpecification(testClass)) {
294+
return false;
295+
}
296+
297+
Set<Method> methods = Reflection.allMethods(testClass);
298+
return hasBeforeAllAnnotations(methods)
299+
|| hasBeforeClassAnnotations(methods)
300+
|| hasAfterAllAnnotations(methods)
301+
|| hasAfterClassAnnotations(methods)
302+
|| hasClassRuleAnnotations(testClass, methods)
303+
|| hasAnnotation(testClass, STEPWISE.orElseThrow(AssertionError::new))
304+
|| hasAnnotation(methods, STEPWISE.orElseThrow(AssertionError::new))
305+
|| hasMethodNamed(methods, "setupSpec")
306+
|| hasMethodNamed(methods, "cleanupSpec")
307+
|| hasSharedField(testClass);
308+
}
309+
310+
private Optional<Class<?>> getTestClass(TestIdentifier testIdentifier) {
311+
if (hasClassSource(testIdentifier)) {
312+
return Optional.of(
313+
testIdentifier
314+
.getSource()
315+
.map(ClassSource.class::cast)
316+
.orElseThrow(AssertionError::new)
317+
.getJavaClass());
318+
}
319+
320+
if (hasMethodSource(testIdentifier)) {
321+
return Optional.of(
322+
testIdentifier
323+
.getSource()
324+
.map(MethodSource.class::cast)
325+
.orElseThrow(AssertionError::new)
326+
.getJavaClass());
327+
}
328+
329+
return Optional.empty();
330+
}
331+
332+
private boolean isSpockSpecification(Class<?> clazz) {
333+
return SPECIFICATION.filter(specification -> specification.isAssignableFrom(testClass)).isPresent();
334+
}
335+
336+
private boolean hasBeforeAllAnnotations(Set<Method> methods) {
337+
return BEFORE_ALL.filter(beforeAll -> hasAnnotation(methods, beforeAll)).isPresent();
338+
}
339+
340+
private boolean hasBeforeClassAnnotations(Set<Method> methods) {
341+
return BEFORE_CLASS.filter(beforeClass -> hasAnnotation(methods, beforeClass)).isPresent();
342+
}
343+
344+
private boolean hasAfterAllAnnotations(Set<Method> methods) {
345+
return AFTER_ALL.filter(afterAll -> hasAnnotation(methods, afterAll)).isPresent();
346+
}
347+
348+
private boolean hasAfterClassAnnotations(Set<Method> methods) {
349+
return AFTER_CLASS.filter(afterClass -> hasAnnotation(methods, afterClass)).isPresent();
350+
}
351+
352+
private boolean hasClassRuleAnnotations(Class<?> clazz, Set<Method> methods) {
353+
return CLASS_RULE.filter(aClass -> hasAnnotation(methods, aClass)
354+
|| hasAnnotation(Reflection.publicFields(clazz), aClass)).isPresent();
355+
}
356+
357+
private boolean hasAnnotation(AnnotatedElement annotatedElement, Class<? extends Annotation> annotation) {
358+
return annotatedElement.isAnnotationPresent(annotation);
359+
}
360+
361+
private boolean hasAnnotation(Set<? extends AnnotatedElement> methods, Class<? extends Annotation> annotation) {
362+
return FCollection.contains(methods, annotatedElement -> annotatedElement.isAnnotationPresent(annotation));
363+
}
364+
365+
private boolean hasMethodNamed(Set<Method> methods, String methodName) {
366+
return FCollection.contains(methods, havingName(methodName));
367+
}
368+
369+
private Predicate<Method> havingName(String methodName) {
370+
return method -> method.getName().equals(methodName);
371+
}
372+
373+
private boolean hasSharedField(Class<?> clazz) {
374+
return hasAnnotation(allFields(clazz), SHARED.orElseThrow(AssertionError::new));
375+
}
376+
377+
private Set<Field> allFields(Class<?> clazz) {
378+
final Set<Field> fields = new LinkedHashSet<>();
379+
if (clazz != null) {
380+
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
381+
fields.addAll(allFields(clazz.getSuperclass()));
205382
}
383+
return fields;
206384
}
207385

208386
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2023 Björn Kautler
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.pitest.junit5.repository
18+
19+
import org.junit.jupiter.api.AfterAll
20+
import spock.lang.Specification
21+
22+
class TestSpecWithAfterAll extends Specification {
23+
@AfterAll
24+
static afterAll() {
25+
}
26+
27+
def test() {
28+
expect:
29+
true
30+
31+
where:
32+
i << (1..2)
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2023 Björn Kautler
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.pitest.junit5.repository
18+
19+
import org.junit.AfterClass
20+
import spock.lang.Specification
21+
22+
class TestSpecWithAfterClass extends Specification {
23+
@AfterClass
24+
static afterClass() {
25+
}
26+
27+
def test() {
28+
expect:
29+
true
30+
31+
where:
32+
i << (1..2)
33+
}
34+
}

0 commit comments

Comments
 (0)