Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support some level of parallelization in junit-vintage-engine #4135

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@
import static org.junit.platform.engine.TestExecutionResult.successful;
import static org.junit.vintage.engine.descriptor.VintageTestDescriptor.ENGINE_ID;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.apiguardian.api.API;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
Expand All @@ -37,6 +47,13 @@
@API(status = INTERNAL, since = "4.12")
public final class VintageTestEngine implements TestEngine {

private static final Logger logger = LoggerFactory.getLogger(VintageTestEngine.class);

private static final int DEFAULT_THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors();
private static final int SHUTDOWN_TIMEOUT_SECONDS = 30;
private static final String PARALLEL_EXECUTION_ENABLED = "junit.jupiter.execution.parallel.enabled";
YongGoose marked this conversation as resolved.
Show resolved Hide resolved
private static final String PARALLEL_POOL_SIZE = "junit.vintage.execution.parallel.pool-size";

@Override
public String getId() {
return ENGINE_ID;
Expand Down Expand Up @@ -69,11 +86,79 @@ public void execute(ExecutionRequest request) {
EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener();
VintageEngineDescriptor engineDescriptor = (VintageEngineDescriptor) request.getRootTestDescriptor();
engineExecutionListener.executionStarted(engineDescriptor);
executeAllChildren(engineDescriptor, engineExecutionListener);
executeAllChildren(engineDescriptor, engineExecutionListener, request);
engineExecutionListener.executionFinished(engineDescriptor, successful());
}

private void executeAllChildren(VintageEngineDescriptor engineDescriptor,
EngineExecutionListener engineExecutionListener, ExecutionRequest request) {
boolean parallelExecutionEnabled = getParallelExecutionEnabled(request);

if (parallelExecutionEnabled) {
if (executeInParallel(engineDescriptor, engineExecutionListener, request)) {
Thread.currentThread().interrupt();
}
}
else {
executeSequentially(engineDescriptor, engineExecutionListener);
}
}

private boolean executeInParallel(VintageEngineDescriptor engineDescriptor,
EngineExecutionListener engineExecutionListener, ExecutionRequest request) {
ExecutorService executorService = Executors.newFixedThreadPool(getThreadPoolSize(request));
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);

List<CompletableFuture<Void>> futures = new ArrayList<>();
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
TestDescriptor descriptor = iterator.next();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
RunnerTestDescriptor testDescriptor = (RunnerTestDescriptor) descriptor;
try {
runnerExecutor.execute(testDescriptor);
}
catch (Exception e) {
engineExecutionListener.executionSkipped(testDescriptor, e.getMessage());
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
}
}, executorService);

futures.add(future);
iterator.remove();
}

CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]));
boolean wasInterrupted = false;
try {
allOf.get();
}
catch (InterruptedException e) {
logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish");
wasInterrupted = true;
}
catch (ExecutionException e) {
throw ExceptionUtils.throwAsUncheckedException(e.getCause());
}
finally {
shutdownExecutorService(executorService);
}
return wasInterrupted;
}

private void shutdownExecutorService(ExecutorService executorService) {
try {
executorService.shutdown();
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
if (!executorService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
logger.warn(() -> "Executor service did not terminate within the specified timeout");
executorService.shutdownNow();
}
}
catch (InterruptedException e) {
logger.warn(e, () -> "Interruption while waiting for executor service to shut down");
marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
Thread.currentThread().interrupt();
}
}

private void executeSequentially(VintageEngineDescriptor engineDescriptor,
EngineExecutionListener engineExecutionListener) {
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
Expand All @@ -82,4 +167,21 @@ private void executeAllChildren(VintageEngineDescriptor engineDescriptor,
}
}

private boolean getParallelExecutionEnabled(ExecutionRequest request) {
return request.getConfigurationParameters().getBoolean(PARALLEL_EXECUTION_ENABLED).orElse(false);
}

private int getThreadPoolSize(ExecutionRequest request) {
Optional<String> poolSize = request.getConfigurationParameters().get(PARALLEL_POOL_SIZE);
if (poolSize.isPresent()) {
try {
return Integer.parseInt(poolSize.get());
}
catch (NumberFormatException e) {
logger.warn(() -> "Invalid value for parallel pool size: " + poolSize.get());
}
}
return DEFAULT_THREAD_POOL_SIZE;
}

}
Loading