identifiers = synchronizedSet(new LinkedHashSet<>());
+
+ /**
+ * Whether to serialize test execution, because we are during coverage recording which is
+ * done through static fields and thus does not support parallel test execution.
+ */
+ private final boolean serializeExecution;
+
+ /**
+ * A map that holds the locks that child tests of locked parent tests should use.
+ * For example parallel data-driven Spock features start the feature execution which is CONTAINER_AND_TEST,
+ * then wait for the parallel iteration executions to be finished which are TEST,
+ * then finish the feature execution.
+ * Due to that we cannot lock the iteration executions on the same lock as the feature executions,
+ * as the feature execution is around all the subordinate iteration executions.
+ *
+ * This logic will of course break if there is some test engine that does strange setups like
+ * having CONTAINER_AND_TEST with child CONTAINER that have child TEST and similar.
+ * If those engines happen to be used, tests will start to deadlock, as the grand-child test
+ * would not find the parent serializer and thus use the root serializer on which the grand-parent
+ * CONTAINER_AND_TEST already locks.
+ *
+ *
This setup would probably not make much sense, so should not be taken into account
+ * unless such an engine actually pops up. If it does and someone tries to use it with PIT,
+ * the logic should maybe be made more sophisticated like remembering the parent-child relationships
+ * to be able to find the grand-parent serializer which is not possible stateless, because we are
+ * only able to get the parent identifier directly, but not further up stateless.
+ */
+ private final Map> parentCoverageSerializers = new ConcurrentHashMap<>();
+
+ /**
+ * A map that holds the actual lock used for a specific test to be able to easily and safely unlock
+ * without the need to recalculate which lock to use.
+ */
+ private final Map coverageSerializers = new ConcurrentHashMap<>();
+
+ /**
+ * The root coverage serializer to be used for the top-most recorded tests.
+ */
+ private final ReentrantLock rootCoverageSerializer = new ReentrantLock();
+
+ /**
+ * Constructs a new test identifier listener.
+ *
+ * @param testClass the test class as given to the test unit finder for forwarding to the result collector
+ * @param testUnitExecutionListener the test unit execution listener to notify during test execution
+ */
+ public TestIdentifierListener(Class> testClass, TestUnitExecutionListener testUnitExecutionListener) {
this.testClass = testClass;
- this.l = l;
+ this.testUnitExecutionListener = testUnitExecutionListener;
+ // PIT gives a coverage recording listener here during coverage recording
+ // At the later stage during minion hunting a NullExecutionListener is given
+ // as PIT is only interested in the resulting list of identifiers.
+ // Serialization of test execution is only necessary during coverage calculation
+ // currently. To be on the safe side serialize test execution for any listener
+ // type except listener types where we know tests can run in parallel safely,
+ // i.e. currently the NullExecutionListener which is the only other one besides
+ // the coverage recording listener.
+ serializeExecution = !(testUnitExecutionListener instanceof NullExecutionListener);
}
- List getIdentifiers() {
+ /**
+ * Returns the collected test identifiers.
+ *
+ * @return the collected test identifiers
+ */
+ private List getIdentifiers() {
return unmodifiableList(new ArrayList<>(identifiers));
}
@@ -117,25 +216,59 @@ public void executionStarted(TestIdentifier testIdentifier) {
&& !includedTestMethods.contains(((MethodSource)testIdentifier.getSource().get()).getMethodName())) {
return;
}
- l.executionStarted(new Description(testIdentifier.getUniqueId(), testClass));
+
+ if (serializeExecution) {
+ coverageSerializers.compute(testIdentifier.getUniqueIdObject(), (uniqueId, lock) -> {
+ if (lock != null) {
+ throw new AssertionError("No lock should be present");
+ }
+
+ // find the serializer to lock the test on
+ // if there is a parent test locked, use the lock for its children if not,
+ // use the root serializer
+ return testIdentifier
+ .getParentIdObject()
+ .map(parentCoverageSerializers::get)
+ .map(lockRef -> lockRef.updateAndGet(parentLock ->
+ parentLock == null ? new ReentrantLock() : parentLock))
+ .orElse(rootCoverageSerializer);
+ }).lock();
+ // record a potential serializer for child tests to lock on
+ parentCoverageSerializers.put(testIdentifier.getUniqueIdObject(), new AtomicReference<>());
+ }
+
+ testUnitExecutionListener.executionStarted(new Description(testIdentifier.getUniqueId(), testClass), true);
identifiers.add(testIdentifier);
}
}
-
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
// Classes with failing BeforeAlls never start execution and identify as 'containers' not 'tests'
if (testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) {
- if (!identifiers.contains(testIdentifier)) {
- identifiers.add(testIdentifier);
- }
- l.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), false);
+ identifiers.add(testIdentifier);
+ testUnitExecutionListener.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), false);
} else if (testIdentifier.isTest()) {
- l.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), true);
+ testUnitExecutionListener.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), true);
}
- }
+ if (serializeExecution) {
+ // forget the potential serializer for child tests
+ parentCoverageSerializers.remove(testIdentifier.getUniqueIdObject());
+ // unlock the serializer for the finished tests to let the next test continue
+ ReentrantLock lock = coverageSerializers.remove(testIdentifier.getUniqueIdObject());
+ if (lock != null) {
+ lock.unlock();
+ }
+ }
+ }
}
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", JUnit5TestUnitFinder.class.getSimpleName() + "[", "]")
+ .add("testGroupConfig=" + testGroupConfig)
+ .add("includedTestMethods=" + includedTestMethods)
+ .toString();
+ }
}