Skip to content

Commit 6020822

Browse files
committed
Serialize tests during coverage calculation
This generically fixes hcoles/pitest#760 and #73 for all platform engines, removing the Jupiter specific work-around from #74 and serializing test execution during coverage calculation using locks. This way the tests can also properly run in parallel later on during mutant hunting.
1 parent 7f563c9 commit 6020822

3 files changed

Lines changed: 152 additions & 20 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<junit.platform.version>1.9.2</junit.platform.version>
2424
<junit.version>5.9.2</junit.version>
2525
<mockito.version>2.7.6</mockito.version>
26-
<pitest.version>1.9.0</pitest.version>
26+
<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>
2929
<groovy.version>4.0.11</groovy.version>

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@
2727
public class JUnit5TestPluginFactory implements TestPluginFactory {
2828

2929
@Override
30-
public Configuration createTestFrameworkConfiguration(TestGroupConfig config,
31-
ClassByteArraySource source,
30+
public Configuration createTestFrameworkConfiguration(TestGroupConfig config,
31+
ClassByteArraySource source,
3232
Collection<String> excludedRunners,
3333
Collection<String> includedTestMethods) {
34-
System.setProperty("junit.jupiter.execution.parallel.enabled", "false");
3534
return new JUnit5Configuration(config, includedTestMethods);
3635
}
3736

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

Lines changed: 149 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,24 @@
1616

1717
import java.util.ArrayList;
1818
import java.util.Collection;
19+
import java.util.LinkedHashSet;
1920
import 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

2128
import static java.util.Collections.emptyList;
22-
import static java.util.Collections.synchronizedList;
29+
import static java.util.Collections.synchronizedSet;
2330
import static java.util.Collections.unmodifiableList;
2431
import static java.util.stream.Collectors.toList;
2532

2633
import org.junit.platform.commons.PreconditionViolationException;
2734
import org.junit.platform.engine.Filter;
2835
import org.junit.platform.engine.TestExecutionResult;
36+
import org.junit.platform.engine.UniqueId;
2937
import org.junit.platform.engine.discovery.DiscoverySelectors;
3038
import org.junit.platform.engine.support.descriptor.MethodSource;
3139
import org.junit.platform.launcher.Launcher;
@@ -35,6 +43,7 @@
3543
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
3644
import org.junit.platform.launcher.core.LauncherFactory;
3745
import org.pitest.testapi.Description;
46+
import org.pitest.testapi.NullExecutionListener;
3847
import org.pitest.testapi.TestGroupConfig;
3948
import org.pitest.testapi.TestUnit;
4049
import org.pitest.testapi.TestUnitExecutionListener;
@@ -46,12 +55,27 @@
4655
*/
4756
public 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

Comments
 (0)