Skip to content

Commit 310fd0b

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 310fd0b

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

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: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
import java.util.ArrayList;
1818
import java.util.Collection;
1919
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
import java.util.concurrent.locks.ReentrantLock;
2024

2125
import static java.util.Collections.emptyList;
2226
import static java.util.Collections.synchronizedList;
@@ -26,6 +30,7 @@
2630
import org.junit.platform.commons.PreconditionViolationException;
2731
import org.junit.platform.engine.Filter;
2832
import org.junit.platform.engine.TestExecutionResult;
33+
import org.junit.platform.engine.UniqueId;
2934
import org.junit.platform.engine.discovery.DiscoverySelectors;
3035
import org.junit.platform.engine.support.descriptor.MethodSource;
3136
import org.junit.platform.launcher.Launcher;
@@ -35,6 +40,7 @@
3540
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
3641
import org.junit.platform.launcher.core.LauncherFactory;
3742
import org.pitest.testapi.Description;
43+
import org.pitest.testapi.NullExecutionListener;
3844
import org.pitest.testapi.TestGroupConfig;
3945
import org.pitest.testapi.TestUnit;
4046
import org.pitest.testapi.TestUnitExecutionListener;
@@ -97,10 +103,43 @@ private class TestIdentifierListener implements TestExecutionListener {
97103
private final Class<?> testClass;
98104
private final TestUnitExecutionListener l;
99105
private final List<TestIdentifier> identifiers = synchronizedList(new ArrayList<>());
106+
private final boolean serializeExecution;
107+
// This map holds the locks that child tests of locked parent tests should use.
108+
// For example parallel data-driven Spock features start the feature execution which is CONTAINER_AND_TEST,
109+
// then wait for the parallel iteration executions to be finished which are TEST,
110+
// then finish the feature execution.
111+
// Due to that we cannot lock the iteration executions on the same lock as the feature executions,
112+
// as the feature execution is around all the subordinate iteration executions.
113+
//
114+
// This logic will of course break if there is some test engine that does strange setups like
115+
// having CONTAINER_AND_TEST with child CONTAINER that have child TEST and similar.
116+
// If those engines happen to be used, tests will start to deadlock, as the grand-child test
117+
// would not find the parent serializer and thus use the root serializer on which the grand-parent
118+
// CONTAINER_AND_TEST already locks.
119+
//
120+
// This setup would probably not make much sense, so should not be taken into account
121+
// unless such an engine actually pops up. If it does and someone tries to use it with PIT,
122+
// the logic should maybe be made more sophisticated like remembering the parent-child relationships
123+
// to be able to find the grand-parent serializer which is not possible stateless, because we are
124+
// only able to get the parent identifier directly, but not further up stateless.
125+
private final Map<UniqueId, AtomicReference<ReentrantLock>> parentCoverageSerializers = new ConcurrentHashMap<>();
126+
// This map holds the actual lock used for a specific test to be able to easily and safely unlock
127+
// without the need to recalculate which lock to use.
128+
private final Map<UniqueId, ReentrantLock> coverageSerializers = new ConcurrentHashMap<>();
129+
private final ReentrantLock rootCoverageSerializer = new ReentrantLock();
100130

101131
public TestIdentifierListener(Class<?> testClass, TestUnitExecutionListener l) {
102132
this.testClass = testClass;
103133
this.l = l;
134+
// PIT gives a coverage recording listener here during coverage recording
135+
// At the later stage during minion hunting a NullExecutionListener is given
136+
// as PIT is only interested in the resulting list of identifiers.
137+
// Serialization of test execution is only necessary during coverage calculation
138+
// currently. To be on the safe side serialize test execution for any listener
139+
// type except listener types where we know tests can run in parallel safely,
140+
// i.e. currently the NullExecutionListener which is the only other one besides
141+
// the coverage recording listener.
142+
serializeExecution = !(l instanceof NullExecutionListener);
104143
}
105144

106145
List<TestIdentifier> getIdentifiers() {
@@ -117,12 +156,32 @@ public void executionStarted(TestIdentifier testIdentifier) {
117156
&& !includedTestMethods.contains(((MethodSource)testIdentifier.getSource().get()).getMethodName())) {
118157
return;
119158
}
120-
l.executionStarted(new Description(testIdentifier.getUniqueId(), testClass));
159+
160+
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();
176+
// record a potential serializer for child tests to lock on
177+
parentCoverageSerializers.put(testIdentifier.getUniqueIdObject(), new AtomicReference<>());
178+
}
179+
180+
l.executionStarted(new Description(testIdentifier.getUniqueId(), testClass), true);
121181
identifiers.add(testIdentifier);
122182
}
123183
}
124184

125-
126185
@Override
127186
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
128187
// Classes with failing BeforeAlls never start execution and identify as 'containers' not 'tests'
@@ -134,6 +193,16 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult
134193
} else if (testIdentifier.isTest()) {
135194
l.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), true);
136195
}
196+
197+
if (serializeExecution) {
198+
// forget the potential serializer for child tests
199+
parentCoverageSerializers.remove(testIdentifier.getUniqueIdObject());
200+
// unlock the serializer for the finished tests to let the next test continue
201+
ReentrantLock lock = coverageSerializers.remove(testIdentifier.getUniqueIdObject());
202+
if (lock != null) {
203+
lock.unlock();
204+
}
205+
}
137206
}
138207

139208
}

0 commit comments

Comments
 (0)