1414 */
1515package 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 ;
1721import java .util .ArrayList ;
22+ import java .util .Arrays ;
1823import java .util .Collection ;
24+ import java .util .LinkedHashSet ;
1925import java .util .List ;
2026import java .util .Map ;
27+ import java .util .Optional ;
28+ import java .util .Set ;
2129import java .util .concurrent .ConcurrentHashMap ;
2230import java .util .concurrent .atomic .AtomicReference ;
2331import java .util .concurrent .locks .ReentrantLock ;
32+ import java .util .function .Predicate ;
2433
2534import static java .util .Collections .emptyList ;
2635import static java .util .Collections .synchronizedList ;
3241import org .junit .platform .engine .TestExecutionResult ;
3342import org .junit .platform .engine .UniqueId ;
3443import org .junit .platform .engine .discovery .DiscoverySelectors ;
44+ import org .junit .platform .engine .support .descriptor .ClassSource ;
3545import org .junit .platform .engine .support .descriptor .MethodSource ;
3646import org .junit .platform .launcher .Launcher ;
3747import org .junit .platform .launcher .TagFilter ;
3848import org .junit .platform .launcher .TestExecutionListener ;
3949import org .junit .platform .launcher .TestIdentifier ;
4050import org .junit .platform .launcher .core .LauncherDiscoveryRequestBuilder ;
4151import org .junit .platform .launcher .core .LauncherFactory ;
52+ import org .pitest .functional .FCollection ;
53+ import org .pitest .reflection .Reflection ;
4254import org .pitest .testapi .Description ;
4355import org .pitest .testapi .NullExecutionListener ;
4456import org .pitest .testapi .TestGroupConfig ;
5163 * @author Tobias Stadler
5264 */
5365public 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 }
0 commit comments