1616
1717import java .util .ArrayList ;
1818import java .util .Collection ;
19+ import java .util .LinkedHashSet ;
1920import java .util .List ;
21+ import java .util .Map ;
22+ import java .util .Set ;
23+ import java .util .StringJoiner ;
24+ import java .util .concurrent .ConcurrentHashMap ;
25+ import java .util .concurrent .atomic .AtomicReference ;
26+ import java .util .concurrent .locks .ReentrantLock ;
2027
2128import static java .util .Collections .emptyList ;
22- import static java .util .Collections .synchronizedList ;
29+ import static java .util .Collections .synchronizedSet ;
2330import static java .util .Collections .unmodifiableList ;
2431import static java .util .stream .Collectors .toList ;
2532
2633import org .junit .platform .commons .PreconditionViolationException ;
2734import org .junit .platform .engine .Filter ;
2835import org .junit .platform .engine .TestExecutionResult ;
36+ import org .junit .platform .engine .UniqueId ;
2937import org .junit .platform .engine .discovery .DiscoverySelectors ;
3038import org .junit .platform .engine .support .descriptor .MethodSource ;
3139import org .junit .platform .launcher .Launcher ;
3543import org .junit .platform .launcher .core .LauncherDiscoveryRequestBuilder ;
3644import org .junit .platform .launcher .core .LauncherFactory ;
3745import org .pitest .testapi .Description ;
46+ import org .pitest .testapi .NullExecutionListener ;
3847import org .pitest .testapi .TestGroupConfig ;
3948import org .pitest .testapi .TestUnit ;
4049import org .pitest .testapi .TestUnitExecutionListener ;
4655 */
4756public class JUnit5TestUnitFinder implements TestUnitFinder {
4857
58+ /**
59+ * The test group config.
60+ */
4961 private final TestGroupConfig testGroupConfig ;
5062
63+ /**
64+ * Test methods that should be included.
65+ */
5166 private final Collection <String > includedTestMethods ;
5267
68+ /**
69+ * The JUnit platform launcher used to execute tests.
70+ */
5371 private final Launcher launcher ;
5472
73+ /**
74+ * Constructs a new JUnit 5 test unit finder.
75+ *
76+ * @param testGroupConfig the test group config
77+ * @param includedTestMethods test methods that should be included
78+ */
5579 public JUnit5TestUnitFinder (TestGroupConfig testGroupConfig , Collection <String > includedTestMethods ) {
5680 this .testGroupConfig = testGroupConfig ;
5781 this .includedTestMethods = includedTestMethods ;
@@ -64,7 +88,7 @@ public List<TestUnit> findTestUnits(Class<?> clazz, TestUnitExecutionListener ex
6488 return emptyList ();
6589 }
6690
67- List <Filter > filters = new ArrayList <>(2 );
91+ List <Filter <?> > filters = new ArrayList <>(2 );
6892 try {
6993 List <String > excludedGroups = testGroupConfig .getExcludedGroups ();
7094 if (excludedGroups != null && !excludedGroups .isEmpty ()) {
@@ -84,7 +108,7 @@ public List<TestUnit> findTestUnits(Class<?> clazz, TestUnitExecutionListener ex
84108 launcher .execute (LauncherDiscoveryRequestBuilder
85109 .request ()
86110 .selectors (DiscoverySelectors .selectClass (clazz ))
87- .filters (filters .toArray (new Filter [filters . size () ]))
111+ .filters (filters .toArray (new Filter [0 ]))
88112 .build (), listener );
89113
90114 return listener .getIdentifiers ()
@@ -93,17 +117,92 @@ public List<TestUnit> findTestUnits(Class<?> clazz, TestUnitExecutionListener ex
93117 .collect (toList ());
94118 }
95119
120+ /**
121+ * A test execution listener that listens for test identifiers, supporting atomic test units
122+ * and notifying the supplied test unit execution listener so that for example coverage can
123+ * be recorded right away during discovery phase already.
124+ */
96125 private class TestIdentifierListener implements TestExecutionListener {
126+ /**
127+ * The test class as given to the test unit finder for forwarding to the test unit execution listener.
128+ */
97129 private final Class <?> testClass ;
98- private final TestUnitExecutionListener l ;
99- private final List <TestIdentifier > identifiers = synchronizedList (new ArrayList <>());
100130
101- public TestIdentifierListener (Class <?> testClass , TestUnitExecutionListener l ) {
131+ /**
132+ * The test unit execution listener, that for example is used for coverage recording per test.
133+ */
134+ private final TestUnitExecutionListener testUnitExecutionListener ;
135+
136+ /**
137+ * The collected test identifiers.
138+ */
139+ private final Set <TestIdentifier > identifiers = synchronizedSet (new LinkedHashSet <>());
140+
141+ /**
142+ * Whether to serialize test execution, because we are during coverage recording which is
143+ * done through static fields and thus does not support parallel test execution.
144+ */
145+ private final boolean serializeExecution ;
146+
147+ /**
148+ * A map that holds the locks that child tests of locked parent tests should use.
149+ * For example parallel data-driven Spock features start the feature execution which is CONTAINER_AND_TEST,
150+ * then wait for the parallel iteration executions to be finished which are TEST,
151+ * then finish the feature execution.
152+ * Due to that we cannot lock the iteration executions on the same lock as the feature executions,
153+ * as the feature execution is around all the subordinate iteration executions.
154+ *
155+ * <p>This logic will of course break if there is some test engine that does strange setups like
156+ * having CONTAINER_AND_TEST with child CONTAINER that have child TEST and similar.
157+ * If those engines happen to be used, tests will start to deadlock, as the grand-child test
158+ * would not find the parent serializer and thus use the root serializer on which the grand-parent
159+ * CONTAINER_AND_TEST already locks.
160+ *
161+ * <p>This setup would probably not make much sense, so should not be taken into account
162+ * unless such an engine actually pops up. If it does and someone tries to use it with PIT,
163+ * the logic should maybe be made more sophisticated like remembering the parent-child relationships
164+ * to be able to find the grand-parent serializer which is not possible stateless, because we are
165+ * only able to get the parent identifier directly, but not further up stateless.
166+ */
167+ private final Map <UniqueId , AtomicReference <ReentrantLock >> parentCoverageSerializers = new ConcurrentHashMap <>();
168+
169+ /**
170+ * A map that holds the actual lock used for a specific test to be able to easily and safely unlock
171+ * without the need to recalculate which lock to use.
172+ */
173+ private final Map <UniqueId , ReentrantLock > coverageSerializers = new ConcurrentHashMap <>();
174+
175+ /**
176+ * The root coverage serializer to be used for the top-most recorded tests.
177+ */
178+ private final ReentrantLock rootCoverageSerializer = new ReentrantLock ();
179+
180+ /**
181+ * Constructs a new test identifier listener.
182+ *
183+ * @param testClass the test class as given to the test unit finder for forwarding to the result collector
184+ * @param testUnitExecutionListener the test unit execution listener to notify during test execution
185+ */
186+ public TestIdentifierListener (Class <?> testClass , TestUnitExecutionListener testUnitExecutionListener ) {
102187 this .testClass = testClass ;
103- this .l = l ;
188+ this .testUnitExecutionListener = testUnitExecutionListener ;
189+ // PIT gives a coverage recording listener here during coverage recording
190+ // At the later stage during minion hunting a NullExecutionListener is given
191+ // as PIT is only interested in the resulting list of identifiers.
192+ // Serialization of test execution is only necessary during coverage calculation
193+ // currently. To be on the safe side serialize test execution for any listener
194+ // type except listener types where we know tests can run in parallel safely,
195+ // i.e. currently the NullExecutionListener which is the only other one besides
196+ // the coverage recording listener.
197+ serializeExecution = !(testUnitExecutionListener instanceof NullExecutionListener );
104198 }
105199
106- List <TestIdentifier > getIdentifiers () {
200+ /**
201+ * Returns the collected test identifiers.
202+ *
203+ * @return the collected test identifiers
204+ */
205+ private List <TestIdentifier > getIdentifiers () {
107206 return unmodifiableList (new ArrayList <>(identifiers ));
108207 }
109208
@@ -117,25 +216,59 @@ public void executionStarted(TestIdentifier testIdentifier) {
117216 && !includedTestMethods .contains (((MethodSource )testIdentifier .getSource ().get ()).getMethodName ())) {
118217 return ;
119218 }
120- l .executionStarted (new Description (testIdentifier .getUniqueId (), testClass ));
219+
220+ if (serializeExecution ) {
221+ coverageSerializers .compute (testIdentifier .getUniqueIdObject (), (uniqueId , lock ) -> {
222+ if (lock != null ) {
223+ throw new AssertionError ("No lock should be present" );
224+ }
225+
226+ // find the serializer to lock the test on
227+ // if there is a parent test locked, use the lock for its children if not,
228+ // use the root serializer
229+ return testIdentifier
230+ .getParentIdObject ()
231+ .map (parentCoverageSerializers ::get )
232+ .map (lockRef -> lockRef .updateAndGet (parentLock ->
233+ parentLock == null ? new ReentrantLock () : parentLock ))
234+ .orElse (rootCoverageSerializer );
235+ }).lock ();
236+ // record a potential serializer for child tests to lock on
237+ parentCoverageSerializers .put (testIdentifier .getUniqueIdObject (), new AtomicReference <>());
238+ }
239+
240+ testUnitExecutionListener .executionStarted (new Description (testIdentifier .getUniqueId (), testClass ), true );
121241 identifiers .add (testIdentifier );
122242 }
123243 }
124244
125-
126245 @ Override
127246 public void executionFinished (TestIdentifier testIdentifier , TestExecutionResult testExecutionResult ) {
128247 // Classes with failing BeforeAlls never start execution and identify as 'containers' not 'tests'
129248 if (testExecutionResult .getStatus () == TestExecutionResult .Status .FAILED ) {
130- if (!identifiers .contains (testIdentifier )) {
131- identifiers .add (testIdentifier );
132- }
133- l .executionFinished (new Description (testIdentifier .getUniqueId (), testClass ), false );
249+ identifiers .add (testIdentifier );
250+ testUnitExecutionListener .executionFinished (new Description (testIdentifier .getUniqueId (), testClass ), false );
134251 } else if (testIdentifier .isTest ()) {
135- l .executionFinished (new Description (testIdentifier .getUniqueId (), testClass ), true );
252+ testUnitExecutionListener .executionFinished (new Description (testIdentifier .getUniqueId (), testClass ), true );
136253 }
137- }
138254
255+ if (serializeExecution ) {
256+ // forget the potential serializer for child tests
257+ parentCoverageSerializers .remove (testIdentifier .getUniqueIdObject ());
258+ // unlock the serializer for the finished tests to let the next test continue
259+ ReentrantLock lock = coverageSerializers .remove (testIdentifier .getUniqueIdObject ());
260+ if (lock != null ) {
261+ lock .unlock ();
262+ }
263+ }
264+ }
139265 }
140266
267+ @ Override
268+ public String toString () {
269+ return new StringJoiner (", " , JUnit5TestUnitFinder .class .getSimpleName () + "[" , "]" )
270+ .add ("testGroupConfig=" + testGroupConfig )
271+ .add ("includedTestMethods=" + includedTestMethods )
272+ .toString ();
273+ }
141274}
0 commit comments