From 0cd9f4da4bdf428819c22c6daa8d805491905e65 Mon Sep 17 00:00:00 2001
From: yongjunhong <dev.yongjunh@gmail.com>
Date: Sat, 11 Jan 2025 21:29:51 +0900
Subject: [PATCH 1/8] 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.
+	 *
+	 * <p>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.
+	 *
+	 * <p>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<String> poolSize = request.getConfigurationParameters().get(PARALLEL_POOL_SIZE);
 		if (poolSize.isPresent()) {

From be5234f9ba6820ecf5b84a58482483e6bdbf550d Mon Sep 17 00:00:00 2001
From: yongjunhong <dev.yongjunh@gmail.com>
Date: Tue, 14 Jan 2025 19:09:56 +0900
Subject: [PATCH 2/8] 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 <dev.yongjunh@gmail.com>
Date: Tue, 14 Jan 2025 23:23:36 +0900
Subject: [PATCH 3/8] 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<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(engineDescriptor,
+			executorService);
+
 		List<CompletableFuture<Void>> futures = new ArrayList<>();
-		for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
-			TestDescriptor descriptor = iterator.next();
-			CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
-				runnerExecutor.execute((RunnerTestDescriptor) descriptor);
-			}, executorService);
+		if (!classes) {
+			for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) {
+				runnerExecutor.execute(runnerTestDescriptor);
+			}
+			return false;
+		}
 
+		for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) {
+			CompletableFuture<Void> future = CompletableFuture.runAsync(
+				() -> runnerExecutor.execute(runnerTestDescriptor), executorService);
 			futures.add(future);
-			iterator.remove();
 		}
 
 		CompletableFuture<Void> 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<RunnerTestDescriptor> collectRunnerTestDescriptors(VintageEngineDescriptor engineDescriptor,
+			ExecutorService executorService) {
+		List<RunnerTestDescriptor> 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<Filter> 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 <kevin0928@naver.com>
Date: Wed, 15 Jan 2025 18:38:46 +0900
Subject: [PATCH 4/8] Update
 junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java

Co-authored-by: Marc Philipp <mail@marcphilipp.de>
---
 .../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 <dev.yongjunh@gmail.com>
Date: Wed, 15 Jan 2025 19:43:17 +0900
Subject: [PATCH 5/8] 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<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(engineDescriptor,
 			executorService);
 
-		List<CompletableFuture<Void>> futures = new ArrayList<>();
 		if (!classes) {
 			for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) {
 				runnerExecutor.execute(runnerTestDescriptor);
@@ -132,6 +131,7 @@ private boolean executeInParallel(VintageEngineDescriptor engineDescriptor,
 			return false;
 		}
 
+		List<CompletableFuture<Void>> futures = new ArrayList<>();
 		for (RunnerTestDescriptor runnerTestDescriptor : runnerTestDescriptors) {
 			CompletableFuture<Void> 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<RunnerTestDescriptor> collectRunnerTestDescriptors(VintageEngineDes
 		List<RunnerTestDescriptor> 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<Filter> 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 <mail@marcphilipp.de>
Date: Thu, 16 Jan 2025 11:20:37 +0100
Subject: [PATCH 6/8] 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<CompletableFuture<Void>> 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 <dev.yongjunh@gmail.com>
Date: Sat, 18 Jan 2025 14:41:15 +0900
Subject: [PATCH 7/8] 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<RunnerTestDescriptor> collectRunnerTestDescriptors(VintageEngineDescriptor engineDescriptor,
 			ExecutorService executorService) {
 		List<RunnerTestDescriptor> runnerTestDescriptors = new ArrayList<>();
@@ -177,6 +170,12 @@ private List<RunnerTestDescriptor> 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<Instant> getTimestampsFor(List<Event> events, Condition<Event> condition) {
 		// @formatter:off
 		return events.stream()
@@ -78,8 +140,9 @@ private List<Instant> getTimestampsFor(List<Event> events, Condition<Event> 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<String> 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 {
+	}
+}

From 582df86ceb1441e73a5874d7846fcbd38058fac9 Mon Sep 17 00:00:00 2001
From: yongjunhong <dev.yongjunh@gmail.com>
Date: Wed, 29 Jan 2025 13:46:58 +0900
Subject: [PATCH 8/8] Apply comment

Issue: #4238
---
 .../java/org/junit/vintage/engine/VintageTestEngine.java   | 2 +-
 .../execution/ParallelExecutionIntegrationTests.java       | 7 +------
 2 files changed, 2 insertions(+), 7 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 c9a145cab98a..0de42fbaf684 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
@@ -117,7 +117,7 @@ private void executeAllChildren(VintageEngineDescriptor engineDescriptor,
 
 	private boolean executeInParallel(VintageEngineDescriptor engineDescriptor,
 			EngineExecutionListener engineExecutionListener, ExecutionRequest request) {
-		ExecutorService executorService = Executors.newFixedThreadPool(getThreadPoolSize(request));
+		ExecutorService executorService = Executors.newWorkStealingPool(getThreadPoolSize(request));
 		RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
 
 		List<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(engineDescriptor,
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 2c8a5aebc701..62fb836617c9 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
@@ -123,12 +123,7 @@ void executesTestClassesAndMethodsInParallel(TestReporter reporter) {
 		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);
+		assertThat(threadNames).hasSizeGreaterThanOrEqualTo(2);
 	}
 
 	private List<Instant> getTimestampsFor(List<Event> events, Condition<Event> condition) {