From 0cd9f4da4bdf428819c22c6daa8d805491905e65 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 11 Jan 2025 21:29:51 +0900 Subject: [PATCH 1/7] Add parameters for parallel execution of classes and methods Issue: #4238 --- .../org/junit/vintage/engine/Constants.java | 22 +++++++++++++++++++ .../vintage/engine/VintageTestEngine.java | 8 +++++++ 2 files changed, 30 insertions(+) diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java index abb85e3eb61d..803db1e5f051 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/Constants.java @@ -46,6 +46,28 @@ public final class Constants { @API(status = EXPERIMENTAL, since = "5.12") public static final String PARALLEL_POOL_SIZE = "junit.vintage.execution.parallel.pool-size"; + /** + * Indicates whether parallel execution is enabled for test classes in the JUnit Vintage engine. + * + *

Set this property to {@code true} to enable parallel execution of test classes. + * Defaults to {@code false}. + * + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public static final String PARALLEL_CLASS_EXECUTION = "junit.vintage.execution.parallel.classes"; + + /** + * Indicates whether parallel execution is enabled for test methods in the JUnit Vintage engine. + * + *

Set this property to {@code true} to enable parallel execution of test methods. + * Defaults to {@code false}. + * + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public static final String PARALLEL_METHOD_EXECUTION = "junit.vintage.execution.parallel.methods"; + private Constants() { /* no-op */ } diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index 3d6840592891..9cec4c20fb9d 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -165,6 +165,14 @@ private boolean getParallelExecutionEnabled(ExecutionRequest request) { return request.getConfigurationParameters().getBoolean(PARALLEL_EXECUTION_ENABLED).orElse(false); } + private boolean getParallelClassExecutionEnabled(ExecutionRequest request) { + return request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false); + } + + private boolean getParallelMethodExecutionEnabled(ExecutionRequest request) { + return request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false); + } + private int getThreadPoolSize(ExecutionRequest request) { Optional poolSize = request.getConfigurationParameters().get(PARALLEL_POOL_SIZE); if (poolSize.isPresent()) { From be5234f9ba6820ecf5b84a58482483e6bdbf550d Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Tue, 14 Jan 2025 19:09:56 +0900 Subject: [PATCH 2/7] Apply comment Issue: #4058 --- .../vintage/engine/VintageTestEngine.java | 32 ++++++++++++------- .../ParallelExecutionIntegrationTests.java | 4 +++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index 9cec4c20fb9d..fee4f5b2bc9a 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -54,6 +54,9 @@ public final class VintageTestEngine implements TestEngine { private static final int DEFAULT_THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors(); private static final int SHUTDOWN_TIMEOUT_SECONDS = 30; + private boolean classes; + private boolean methods; + @Override public String getId() { return ENGINE_ID; @@ -92,15 +95,23 @@ public void execute(ExecutionRequest request) { private void executeAllChildren(VintageEngineDescriptor engineDescriptor, EngineExecutionListener engineExecutionListener, ExecutionRequest request) { - boolean parallelExecutionEnabled = getParallelExecutionEnabled(request); + initializeParallelExecution(request); - if (parallelExecutionEnabled) { - if (executeInParallel(engineDescriptor, engineExecutionListener, request)) { - Thread.currentThread().interrupt(); - } + boolean parallelExecutionEnabled = getParallelExecutionEnabled(request); + if (!parallelExecutionEnabled) { + executeSequentially(engineDescriptor, engineExecutionListener); + return; } - else { + + if (!classes && !methods) { + logger.warn(() -> "Parallel execution is enabled but no scope is defined. " + + "Falling back to sequential execution."); executeSequentially(engineDescriptor, engineExecutionListener); + return; + } + + if (executeInParallel(engineDescriptor, engineExecutionListener, request)) { + Thread.currentThread().interrupt(); } } @@ -165,12 +176,9 @@ private boolean getParallelExecutionEnabled(ExecutionRequest request) { return request.getConfigurationParameters().getBoolean(PARALLEL_EXECUTION_ENABLED).orElse(false); } - private boolean getParallelClassExecutionEnabled(ExecutionRequest request) { - return request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false); - } - - private boolean getParallelMethodExecutionEnabled(ExecutionRequest request) { - return request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false); + private void initializeParallelExecution(ExecutionRequest request) { + classes = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false); + methods = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false); } private int getThreadPoolSize(ExecutionRequest request) { diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java index 9f535007ab77..a062dc57e105 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java @@ -15,7 +15,9 @@ import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; import static org.junit.platform.testkit.engine.EventConditions.started; +import static org.junit.vintage.engine.Constants.PARALLEL_CLASS_EXECUTION; import static org.junit.vintage.engine.Constants.PARALLEL_EXECUTION_ENABLED; +import static org.junit.vintage.engine.Constants.PARALLEL_METHOD_EXECUTION; import static org.junit.vintage.engine.Constants.PARALLEL_POOL_SIZE; import static org.junit.vintage.engine.descriptor.VintageTestDescriptor.SEGMENT_TYPE_RUNNER; import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelTestCase.AbstractBlockingTestCase; @@ -100,6 +102,8 @@ private static LauncherDiscoveryRequest request(int poolSize, Class... testCl .selectors(classSelectors) // .configurationParameter(PARALLEL_EXECUTION_ENABLED, String.valueOf(true)) // .configurationParameter(PARALLEL_POOL_SIZE, String.valueOf(poolSize)) // + .configurationParameter(PARALLEL_CLASS_EXECUTION, String.valueOf(true)) // + .configurationParameter(PARALLEL_METHOD_EXECUTION, String.valueOf(true)) // .build(); } From 0b706157bfcac19ab295e5881147a5bb51c389f6 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Tue, 14 Jan 2025 23:23:36 +0900 Subject: [PATCH 3/7] Implement parallel execution at class and method level Issue: #4058 --- .../vintage/engine/VintageTestEngine.java | 54 ++++++++++++++++--- .../engine/descriptor/RunnerScheduler.java | 36 +++++++++++++ .../descriptor/RunnerTestDescriptor.java | 14 +++++ 3 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index fee4f5b2bc9a..d63908bec66f 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -36,6 +36,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; +import org.junit.vintage.engine.descriptor.RunnerScheduler; import org.junit.vintage.engine.descriptor.RunnerTestDescriptor; import org.junit.vintage.engine.descriptor.VintageEngineDescriptor; import org.junit.vintage.engine.discovery.VintageDiscoverer; @@ -120,15 +121,21 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, ExecutorService executorService = Executors.newFixedThreadPool(getThreadPoolSize(request)); RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener); + List runnerTestDescriptors = collectRunnerTestDescriptors(engineDescriptor, + executorService); + List> futures = new ArrayList<>(); - for (Iterator iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) { - TestDescriptor descriptor = iterator.next(); - CompletableFuture future = CompletableFuture.runAsync(() -> { - runnerExecutor.execute((RunnerTestDescriptor) descriptor); - }, executorService); + if (!classes) { + for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) { + runnerExecutor.execute(runnerTestDescriptor); + } + return false; + } + for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) { + CompletableFuture future = CompletableFuture.runAsync( + () -> runnerExecutor.execute(runnerTestDescriptor), executorService); futures.add(future); - iterator.remove(); } CompletableFuture allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); @@ -149,6 +156,41 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, return wasInterrupted; } + private RunnerTestDescriptor parallelMethodExecutor(RunnerTestDescriptor runnerTestDescriptor, + ExecutorService executorService) { + runnerTestDescriptor.setScheduler(new RunnerScheduler() { + @Override + public void schedule(Runnable childStatement) { + executorService.submit(childStatement); + } + + @Override + public void finished() { + try { + executorService.shutdown(); + executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } + catch (InterruptedException e) { + logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish"); + } + } + }); + + return runnerTestDescriptor; + } + + private List collectRunnerTestDescriptors(VintageEngineDescriptor engineDescriptor, + ExecutorService executorService) { + List runnerTestDescriptors = new ArrayList<>(); + for (TestDescriptor descriptor : engineDescriptor.getModifiableChildren()) { + RunnerTestDescriptor runnerTestDescriptor = (RunnerTestDescriptor) descriptor; + if (methods) { + runnerTestDescriptors.add(parallelMethodExecutor(runnerTestDescriptor, executorService)); + } + } + return runnerTestDescriptors; + } + private void shutdownExecutorService(ExecutorService executorService) { try { executorService.shutdown(); diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java new file mode 100644 index 000000000000..ab4acc34e217 --- /dev/null +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.vintage.engine.descriptor; + +import static org.apiguardian.api.API.Status.INTERNAL; + +import org.apiguardian.api.API; + +/** + * Represents a strategy for scheduling when individual test methods + * should be run (in serial or parallel) + * + * @since 5.13 + */ +@API(status = INTERNAL, since = "5.13") +public interface RunnerScheduler { + /** + * Schedule a child statement to run + */ + void schedule(Runnable childStatement); + + /** + * Override to implement any behavior that must occur + * after all children have been scheduled (for example, + * waiting for them all to finish) + */ + void finished(); +} diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java index 8d12fa032a8d..08e29a2629c5 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java @@ -47,6 +47,16 @@ public class RunnerTestDescriptor extends VintageTestDescriptor { private boolean wasFiltered; private List filters = new ArrayList<>(); + private volatile RunnerScheduler scheduler = new RunnerScheduler() { + public void schedule(Runnable childStatement) { + childStatement.run(); + } + + public void finished() { + // do nothing + } + }; + public RunnerTestDescriptor(UniqueId uniqueId, Class testClass, Runner runner, boolean ignored) { super(uniqueId, runner.getDescription(), testClass.getSimpleName(), ClassSource.from(testClass)); this.runner = runner; @@ -161,6 +171,10 @@ public boolean isIgnored() { return ignored; } + public void setScheduler(RunnerScheduler scheduler) { + this.scheduler = scheduler; + } + private static class ExcludeDescriptionFilter extends Filter { private final Description description; From 6e679dcec1aed018c10515bcc43ddf182e21c6fe Mon Sep 17 00:00:00 2001 From: Yongjun Hong Date: Wed, 15 Jan 2025 18:38:46 +0900 Subject: [PATCH 4/7] Update junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java Co-authored-by: Marc Philipp --- .../vintage/engine/descriptor/RunnerTestDescriptor.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java index 08e29a2629c5..d23f5b488d90 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java @@ -171,8 +171,11 @@ public boolean isIgnored() { return ignored; } - public void setScheduler(RunnerScheduler scheduler) { - this.scheduler = scheduler; + public void setScheduler(org.junit.runners.model.RunnerScheduler scheduler) { + Runner runner = getRunnerToReport(); + if (runner instanceof ParentRunner) { + ((ParentRunner) runner).setScheduler(scheduler); + } } private static class ExcludeDescriptionFilter extends Filter { From e92e834db7b0ce6f960f35ece05404108c05f110 Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Wed, 15 Jan 2025 19:43:17 +0900 Subject: [PATCH 5/7] Apply comment Issue: #4238 --- .../vintage/engine/VintageTestEngine.java | 12 ++++--- .../engine/descriptor/RunnerScheduler.java | 36 ------------------- .../descriptor/RunnerTestDescriptor.java | 16 +++------ 3 files changed, 12 insertions(+), 52 deletions(-) delete mode 100644 junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index d63908bec66f..231a986996d8 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -36,7 +36,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; -import org.junit.vintage.engine.descriptor.RunnerScheduler; +import org.junit.runners.model.RunnerScheduler; import org.junit.vintage.engine.descriptor.RunnerTestDescriptor; import org.junit.vintage.engine.descriptor.VintageEngineDescriptor; import org.junit.vintage.engine.discovery.VintageDiscoverer; @@ -124,7 +124,6 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, List runnerTestDescriptors = collectRunnerTestDescriptors(engineDescriptor, executorService); - List> futures = new ArrayList<>(); if (!classes) { for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) { runnerExecutor.execute(runnerTestDescriptor); @@ -132,6 +131,7 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, return false; } + List> futures = new ArrayList<>(); for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) { CompletableFuture future = CompletableFuture.runAsync( () -> runnerExecutor.execute(runnerTestDescriptor), executorService); @@ -167,8 +167,9 @@ public void schedule(Runnable childStatement) { @Override public void finished() { try { - executorService.shutdown(); - executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + if (!executorService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + logger.warn(() -> "Executor service did not terminate within the specified timeout"); + } } catch (InterruptedException e) { logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish"); @@ -184,9 +185,12 @@ private List collectRunnerTestDescriptors(VintageEngineDes List runnerTestDescriptors = new ArrayList<>(); for (TestDescriptor descriptor : engineDescriptor.getModifiableChildren()) { RunnerTestDescriptor runnerTestDescriptor = (RunnerTestDescriptor) descriptor; + if (methods) { runnerTestDescriptors.add(parallelMethodExecutor(runnerTestDescriptor, executorService)); + continue; } + runnerTestDescriptors.add(runnerTestDescriptor); } return runnerTestDescriptors; } diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java deleted file mode 100644 index ab4acc34e217..000000000000 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerScheduler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-2025 the original author or authors. - * - * All rights reserved. This program and the accompanying materials are - * made available under the terms of the Eclipse Public License v2.0 which - * accompanies this distribution and is available at - * - * https://www.eclipse.org/legal/epl-v20.html - */ - -package org.junit.vintage.engine.descriptor; - -import static org.apiguardian.api.API.Status.INTERNAL; - -import org.apiguardian.api.API; - -/** - * Represents a strategy for scheduling when individual test methods - * should be run (in serial or parallel) - * - * @since 5.13 - */ -@API(status = INTERNAL, since = "5.13") -public interface RunnerScheduler { - /** - * Schedule a child statement to run - */ - void schedule(Runnable childStatement); - - /** - * Override to implement any behavior that must occur - * after all children have been scheduled (for example, - * waiting for them all to finish) - */ - void finished(); -} diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java index d23f5b488d90..1ee684109c94 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java @@ -32,6 +32,8 @@ import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runners.ParentRunner; +import org.junit.runners.model.RunnerScheduler; /** * @since 4.12 @@ -47,16 +49,6 @@ public class RunnerTestDescriptor extends VintageTestDescriptor { private boolean wasFiltered; private List filters = new ArrayList<>(); - private volatile RunnerScheduler scheduler = new RunnerScheduler() { - public void schedule(Runnable childStatement) { - childStatement.run(); - } - - public void finished() { - // do nothing - } - }; - public RunnerTestDescriptor(UniqueId uniqueId, Class testClass, Runner runner, boolean ignored) { super(uniqueId, runner.getDescription(), testClass.getSimpleName(), ClassSource.from(testClass)); this.runner = runner; @@ -171,8 +163,8 @@ public boolean isIgnored() { return ignored; } - public void setScheduler(org.junit.runners.model.RunnerScheduler scheduler) { - Runner runner = getRunnerToReport(); + public void setScheduler(RunnerScheduler scheduler) { + Runner runner = getRunnerToReport(); if (runner instanceof ParentRunner) { ((ParentRunner) runner).setScheduler(scheduler); } From 96951cebdc2a8316f575c1971921c2528f1f5cd7 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Thu, 16 Jan 2025 11:20:37 +0100 Subject: [PATCH 6/7] Don't reference RunnerScheduler in VintageTestEngine --- .../vintage/engine/VintageTestEngine.java | 20 +----------- .../descriptor/RunnerTestDescriptor.java | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index 231a986996d8..fb530dce30a5 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -36,7 +36,6 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; -import org.junit.runners.model.RunnerScheduler; import org.junit.vintage.engine.descriptor.RunnerTestDescriptor; import org.junit.vintage.engine.descriptor.VintageEngineDescriptor; import org.junit.vintage.engine.discovery.VintageDiscoverer; @@ -158,24 +157,7 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, private RunnerTestDescriptor parallelMethodExecutor(RunnerTestDescriptor runnerTestDescriptor, ExecutorService executorService) { - runnerTestDescriptor.setScheduler(new RunnerScheduler() { - @Override - public void schedule(Runnable childStatement) { - executorService.submit(childStatement); - } - - @Override - public void finished() { - try { - if (!executorService.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { - logger.warn(() -> "Executor service did not terminate within the specified timeout"); - } - } - catch (InterruptedException e) { - logger.warn(e, () -> "Interruption while waiting for parallel test execution to finish"); - } - } - }); + runnerTestDescriptor.setExecutorService(executorService); return runnerTestDescriptor; } diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java index 1ee684109c94..d4baacce032e 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java @@ -18,12 +18,17 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; 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.UniqueId; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.runner.Description; @@ -163,10 +168,32 @@ public boolean isIgnored() { return ignored; } - public void setScheduler(RunnerScheduler scheduler) { + public void setExecutorService(ExecutorService executorService) { Runner runner = getRunnerToReport(); if (runner instanceof ParentRunner) { - ((ParentRunner) runner).setScheduler(scheduler); + ((ParentRunner) runner).setScheduler(new RunnerScheduler() { + + private final List> futures = new CopyOnWriteArrayList<>(); + + @Override + public void schedule(Runnable childStatement) { + futures.add(CompletableFuture.runAsync(childStatement, executorService)); + } + + @Override + public void finished() { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(); + } + catch (ExecutionException e) { + throw ExceptionUtils.throwAsUncheckedException(e.getCause()); + } + catch (InterruptedException e) { + logger.warn(e, () -> "Interrupted while waiting for runner to finish"); + Thread.currentThread().interrupt(); + } + } + }); } } From 78c5c806fc2e463c5d926aec7a6194bfa0dbe01e Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 18 Jan 2025 14:41:15 +0900 Subject: [PATCH 7/7] Add test code Issue: #4238 --- .../vintage/engine/VintageTestEngine.java | 17 ++-- .../ParallelExecutionIntegrationTests.java | 98 ++++++++++++++++--- ...ava => JUnit4ParallelClassesTestCase.java} | 8 +- .../junit4/JUnit4ParallelMethodsTestCase.java | 77 +++++++++++++++ 4 files changed, 171 insertions(+), 29 deletions(-) rename junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/{JUnit4ParallelTestCase.java => JUnit4ParallelClassesTestCase.java} (86%) create mode 100644 junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelMethodsTestCase.java diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java index fb530dce30a5..c9a145cab98a 100644 --- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java +++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java @@ -95,7 +95,7 @@ public void execute(ExecutionRequest request) { private void executeAllChildren(VintageEngineDescriptor engineDescriptor, EngineExecutionListener engineExecutionListener, ExecutionRequest request) { - initializeParallelExecution(request); + initializeParallelExecutionParameters(request); boolean parallelExecutionEnabled = getParallelExecutionEnabled(request); if (!parallelExecutionEnabled) { @@ -155,13 +155,6 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor, return wasInterrupted; } - private RunnerTestDescriptor parallelMethodExecutor(RunnerTestDescriptor runnerTestDescriptor, - ExecutorService executorService) { - runnerTestDescriptor.setExecutorService(executorService); - - return runnerTestDescriptor; - } - private List collectRunnerTestDescriptors(VintageEngineDescriptor engineDescriptor, ExecutorService executorService) { List runnerTestDescriptors = new ArrayList<>(); @@ -177,6 +170,12 @@ private List collectRunnerTestDescriptors(VintageEngineDes return runnerTestDescriptors; } + private RunnerTestDescriptor parallelMethodExecutor(RunnerTestDescriptor runnerTestDescriptor, + ExecutorService executorService) { + runnerTestDescriptor.setExecutorService(executorService); + return runnerTestDescriptor; + } + private void shutdownExecutorService(ExecutorService executorService) { try { executorService.shutdown(); @@ -204,7 +203,7 @@ private boolean getParallelExecutionEnabled(ExecutionRequest request) { return request.getConfigurationParameters().getBoolean(PARALLEL_EXECUTION_ENABLED).orElse(false); } - private void initializeParallelExecution(ExecutionRequest request) { + private void initializeParallelExecutionParameters(ExecutionRequest request) { classes = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false); methods = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false); } diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java index a062dc57e105..2c8a5aebc701 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/execution/ParallelExecutionIntegrationTests.java @@ -15,14 +15,19 @@ import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; import static org.junit.platform.testkit.engine.EventConditions.started; +import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.vintage.engine.Constants.PARALLEL_CLASS_EXECUTION; import static org.junit.vintage.engine.Constants.PARALLEL_EXECUTION_ENABLED; import static org.junit.vintage.engine.Constants.PARALLEL_METHOD_EXECUTION; import static org.junit.vintage.engine.Constants.PARALLEL_POOL_SIZE; import static org.junit.vintage.engine.descriptor.VintageTestDescriptor.SEGMENT_TYPE_RUNNER; -import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelTestCase.AbstractBlockingTestCase; -import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelTestCase.FirstTestCase; -import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelTestCase.ThirdTestCase; +import static org.junit.vintage.engine.descriptor.VintageTestDescriptor.SEGMENT_TYPE_TEST; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelClassesTestCase.FirstClassTestCase; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelClassesTestCase.SecondClassTestCase; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelClassesTestCase.ThirdClassTestCase; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelMethodsTestCase.FirstMethodTestCase; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelMethodsTestCase.SecondMethodTestCase; +import static org.junit.vintage.engine.samples.junit4.JUnit4ParallelMethodsTestCase.ThirdMethodTestCase; import java.time.Instant; import java.util.Arrays; @@ -42,22 +47,23 @@ import org.junit.platform.testkit.engine.Event; import org.junit.platform.testkit.engine.Events; import org.junit.vintage.engine.VintageTestEngine; -import org.junit.vintage.engine.samples.junit4.JUnit4ParallelTestCase.SecondTestCase; +import org.junit.vintage.engine.samples.junit4.JUnit4ParallelClassesTestCase; +import org.junit.vintage.engine.samples.junit4.JUnit4ParallelMethodsTestCase; class ParallelExecutionIntegrationTests { @Test void executesTestClassesInParallel(TestReporter reporter) { - AbstractBlockingTestCase.threadNames.clear(); - AbstractBlockingTestCase.countDownLatch = new CountDownLatch(3); + JUnit4ParallelClassesTestCase.AbstractBlockingTestCase.threadNames.clear(); + JUnit4ParallelClassesTestCase.AbstractBlockingTestCase.countDownLatch = new CountDownLatch(3); - var events = executeInParallelSuccessfully(3, FirstTestCase.class, SecondTestCase.class, - ThirdTestCase.class).list(); + var events = executeInParallelSuccessfully(3, true, false, FirstClassTestCase.class, SecondClassTestCase.class, + ThirdClassTestCase.class).list(); var startedTimestamps = getTimestampsFor(events, event(container(SEGMENT_TYPE_RUNNER), started())); var finishedTimestamps = getTimestampsFor(events, event(container(SEGMENT_TYPE_RUNNER), finishedSuccessfully())); - var threadNames = new HashSet<>(AbstractBlockingTestCase.threadNames); + var threadNames = new HashSet<>(JUnit4ParallelClassesTestCase.AbstractBlockingTestCase.threadNames); reporter.publishEntry("startedTimestamps", startedTimestamps.toString()); reporter.publishEntry("finishedTimestamps", finishedTimestamps.toString()); @@ -69,6 +75,62 @@ void executesTestClassesInParallel(TestReporter reporter) { assertThat(threadNames).hasSize(3); } + @Test + void executesTestMethodsInParallel(TestReporter reporter) { + JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.threadNames.clear(); + JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.countDownLatch = new CountDownLatch(3); + + var events = executeInParallelSuccessfully(3, false, true, FirstMethodTestCase.class).list(); + + var startedTimestamps = getTimestampsFor(events, event(test(SEGMENT_TYPE_TEST), started())); + var finishedTimestamps = getTimestampsFor(events, event(test(SEGMENT_TYPE_TEST), finishedSuccessfully())); + var threadNames = new HashSet<>(JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.threadNames); + + reporter.publishEntry("startedTimestamps", startedTimestamps.toString()); + reporter.publishEntry("finishedTimestamps", finishedTimestamps.toString()); + + assertThat(startedTimestamps).hasSize(3); + assertThat(finishedTimestamps).hasSize(3); + assertThat(startedTimestamps).allMatch(startTimestamp -> finishedTimestamps.stream().noneMatch( + finishedTimestamp -> finishedTimestamp.isBefore(startTimestamp))); + assertThat(threadNames).hasSize(3); + } + + @Test + void executesTestClassesAndMethodsInParallel(TestReporter reporter) { + JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.threadNames.clear(); + JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.countDownLatch = new CountDownLatch(9); + + var events = executeInParallelSuccessfully(3, true, true, FirstMethodTestCase.class, SecondMethodTestCase.class, + ThirdMethodTestCase.class).list(); + + var startedClassesTimestamps = getTimestampsFor(events, event(container(SEGMENT_TYPE_RUNNER), started())); + var finishedClassesTimestamps = getTimestampsFor(events, + event(container(SEGMENT_TYPE_RUNNER), finishedSuccessfully())); + var startedMethodsTimestamps = getTimestampsFor(events, event(test(SEGMENT_TYPE_TEST), started())); + var finishedMethodsTimestamps = getTimestampsFor(events, + event(test(SEGMENT_TYPE_TEST), finishedSuccessfully())); + + var threadNames = new HashSet<>(JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase.threadNames); + + reporter.publishEntry("startedClassesTimestamps", startedClassesTimestamps.toString()); + reporter.publishEntry("finishedClassesTimestamps", finishedClassesTimestamps.toString()); + reporter.publishEntry("startedMethodsTimestamps", startedMethodsTimestamps.toString()); + reporter.publishEntry("finishedMethodsTimestamps", finishedMethodsTimestamps.toString()); + + assertThat(startedClassesTimestamps).hasSize(3); + assertThat(finishedClassesTimestamps).hasSize(3); + assertThat(startedMethodsTimestamps).hasSize(9); + assertThat(finishedMethodsTimestamps).hasSize(9); + + assertThat(startedClassesTimestamps).allMatch(startTimestamp -> finishedClassesTimestamps.stream().noneMatch( + finishedTimestamp -> finishedTimestamp.isBefore(startTimestamp))); + assertThat(startedMethodsTimestamps).allMatch(startTimestamp -> finishedMethodsTimestamps.stream().noneMatch( + finishedTimestamp -> finishedTimestamp.isBefore(startTimestamp))); + + assertThat(threadNames).hasSize(3); + } + private List getTimestampsFor(List events, Condition condition) { // @formatter:off return events.stream() @@ -78,8 +140,9 @@ private List getTimestampsFor(List events, Condition cond // @formatter:on } - private Events executeInParallelSuccessfully(int poolSize, Class... testClasses) { - var events = execute(poolSize, testClasses).allEvents(); + private Events executeInParallelSuccessfully(int poolSize, boolean parallelClasses, boolean parallelMethods, + Class... testClasses) { + var events = execute(poolSize, parallelClasses, parallelMethods, testClasses).allEvents(); try { return events.assertStatistics(it -> it.failed(0)); } @@ -89,11 +152,14 @@ private Events executeInParallelSuccessfully(int poolSize, Class... testClass } } - private static EngineExecutionResults execute(int poolSize, Class... testClass) { - return EngineTestKit.execute(new VintageTestEngine(), request(poolSize, testClass)); + private static EngineExecutionResults execute(int poolSize, boolean parallelClasses, boolean parallelMethods, + Class... testClass) { + return EngineTestKit.execute(new VintageTestEngine(), + request(poolSize, parallelClasses, parallelMethods, testClass)); } - private static LauncherDiscoveryRequest request(int poolSize, Class... testClasses) { + private static LauncherDiscoveryRequest request(int poolSize, boolean parallelClasses, boolean parallelMethods, + Class... testClasses) { var classSelectors = Arrays.stream(testClasses) // .map(DiscoverySelectors::selectClass) // .toArray(ClassSelector[]::new); @@ -102,8 +168,8 @@ private static LauncherDiscoveryRequest request(int poolSize, Class... testCl .selectors(classSelectors) // .configurationParameter(PARALLEL_EXECUTION_ENABLED, String.valueOf(true)) // .configurationParameter(PARALLEL_POOL_SIZE, String.valueOf(poolSize)) // - .configurationParameter(PARALLEL_CLASS_EXECUTION, String.valueOf(true)) // - .configurationParameter(PARALLEL_METHOD_EXECUTION, String.valueOf(true)) // + .configurationParameter(PARALLEL_CLASS_EXECUTION, String.valueOf(parallelClasses)) // + .configurationParameter(PARALLEL_METHOD_EXECUTION, String.valueOf(parallelMethods)) // .build(); } diff --git a/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelTestCase.java b/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelClassesTestCase.java similarity index 86% rename from junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelTestCase.java rename to junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelClassesTestCase.java index 4e8e0c62c8c2..d84a6fe34871 100644 --- a/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelTestCase.java +++ b/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelClassesTestCase.java @@ -24,7 +24,7 @@ import org.junit.runner.RunWith; @RunWith(Enclosed.class) -public class JUnit4ParallelTestCase { +public class JUnit4ParallelClassesTestCase { public static class AbstractBlockingTestCase { @@ -56,12 +56,12 @@ private static long estimateSimulatedTestDurationInMilliseconds() { } } - public static class FirstTestCase extends AbstractBlockingTestCase { + public static class FirstClassTestCase extends AbstractBlockingTestCase { } - public static class SecondTestCase extends AbstractBlockingTestCase { + public static class SecondClassTestCase extends AbstractBlockingTestCase { } - public static class ThirdTestCase extends AbstractBlockingTestCase { + public static class ThirdClassTestCase extends AbstractBlockingTestCase { } } diff --git a/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelMethodsTestCase.java b/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelMethodsTestCase.java new file mode 100644 index 000000000000..b9e553d8276f --- /dev/null +++ b/junit-vintage-engine/src/testFixtures/java/org/junit/vintage/engine/samples/junit4/JUnit4ParallelMethodsTestCase.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2025 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.vintage.engine.samples.junit4; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runner.RunWith; + +@RunWith(Enclosed.class) +public class JUnit4ParallelMethodsTestCase { + + public static class AbstractBlockingTestCase { + + public static final Set threadNames = ConcurrentHashMap.newKeySet(); + public static CountDownLatch countDownLatch; + + @Rule + public final TestWatcher testWatcher = new TestWatcher() { + @Override + protected void starting(Description description) { + AbstractBlockingTestCase.threadNames.add(Thread.currentThread().getName()); + } + }; + + @Test + public void fistTest() throws Exception { + countDownAndBlock(countDownLatch); + } + + @Test + public void secondTest() throws Exception { + countDownAndBlock(countDownLatch); + } + + @Test + public void thirdTest() throws Exception { + countDownAndBlock(countDownLatch); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void countDownAndBlock(CountDownLatch countDownLatch) throws InterruptedException { + countDownLatch.countDown(); + countDownLatch.await(estimateSimulatedTestDurationInMilliseconds(), MILLISECONDS); + } + + private static long estimateSimulatedTestDurationInMilliseconds() { + var runningInCi = Boolean.parseBoolean(System.getenv("CI")); + return runningInCi ? 1000 : 100; + } + } + + public static class FirstMethodTestCase extends JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase { + } + + public static class SecondMethodTestCase extends JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase { + } + + public static class ThirdMethodTestCase extends JUnit4ParallelMethodsTestCase.AbstractBlockingTestCase { + } +}