diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml
index a4b1bc129f6c..0127f231d44b 100644
--- a/.github/actions/run-gradle/action.yml
+++ b/.github/actions/run-gradle/action.yml
@@ -13,6 +13,12 @@ runs:
with:
distribution: temurin
java-version: 17
+ - uses: oracle-actions/setup-java@v1
+ with:
+ website: jdk.java.net
+ release: 21
+ - shell: bash
+ run: echo "JDK21=$JAVA_HOME" >> $GITHUB_ENV
- uses: gradle/gradle-build-action@v2
env:
JAVA_HOME: ${{ steps.setup-gradle-jdk.outputs.path }}
diff --git a/.github/workflows/reproducible-build.yml b/.github/workflows/reproducible-build.yml
index a32a51ac0002..84b2f6ddae49 100644
--- a/.github/workflows/reproducible-build.yml
+++ b/.github/workflows/reproducible-build.yml
@@ -26,6 +26,8 @@ jobs:
with:
arguments: --quiet
- name: Build and compare checksums
+ env:
+ JAVA_HOME: ${{env.JAVA_HOME_17_X64}}
shell: bash
run: |
./gradle/scripts/checkBuildReproducibility.sh
diff --git a/gradle/config/checkstyle/suppressions.xml b/gradle/config/checkstyle/suppressions.xml
index 05801fccce21..aec4637950ab 100644
--- a/gradle/config/checkstyle/suppressions.xml
+++ b/gradle/config/checkstyle/suppressions.xml
@@ -4,4 +4,6 @@
files="junit-platform-commons[\\/]src[\\/]main[\\/]java.+?[\\/]org[\\/]junit[\\/]platform[\\/]commons[\\/]util[\\/]*"/>
+
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts
index 6eb7eb308ec8..992b59509871 100644
--- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts
+++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts
@@ -147,9 +147,8 @@ val allMainClasses by tasks.registering {
val prepareModuleSourceDir by tasks.registering(Sync::class) {
from(moduleSourceDir)
- from(sourceSets.matching { it.name.startsWith("main") }.map { it.allJava })
+ from(sourceSets.main.map { it.allJava })
into(combinedModuleSourceDir.map { it.dir(javaModuleName) })
- duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
val compileModule by tasks.registering(JavaCompile::class) {
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-multi-release-sources.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-multi-release-sources.gradle.kts
index d1b05c6e00ce..2d8677d9c12b 100644
--- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-multi-release-sources.gradle.kts
+++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-multi-release-sources.gradle.kts
@@ -6,7 +6,7 @@ plugins {
val mavenizedProjects: List by rootProject.extra
-listOf(9, 17).forEach { javaVersion ->
+listOf(9, 17, 21).forEach { javaVersion ->
val sourceSet = sourceSets.register("mainRelease${javaVersion}") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
@@ -27,6 +27,11 @@ listOf(9, 17).forEach { javaVersion ->
named(sourceSet.get().compileJavaTaskName).configure {
options.release = javaVersion
+ if (javaVersion == 21) {
+ javaCompiler.set(javaToolchains.compilerFor {
+ languageVersion.set(JavaLanguageVersion.of(javaVersion))
+ })
+ }
}
named("checkstyle${sourceSet.name.capitalized()}").configure {
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-repackage-jars.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-repackage-jars.gradle.kts
deleted file mode 100644
index a000989b0327..000000000000
--- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-repackage-jars.gradle.kts
+++ /dev/null
@@ -1,52 +0,0 @@
-import java.util.jar.JarEntry
-import java.util.jar.JarFile
-import java.util.jar.JarOutputStream
-import org.gradle.api.internal.file.archive.ZipCopyAction
-import java.nio.file.Files
-
-// This registers a `doLast` action to rewrite the timestamps of the project's output JAR
-afterEvaluate {
- val jarTask = (tasks.findByName("shadowJar") ?: tasks["jar"]) as Jar
-
- jarTask.doLast {
-
- val newFile = Files.createTempFile("rewrite-timestamp", null).toFile()
- val originalOutput = jarTask.archiveFile.get().asFile
-
- newFile.outputStream().use { os ->
-
- val newJarStream = JarOutputStream(os)
- val oldJar = JarFile(originalOutput)
-
- fun sortAlwaysFirst(name: String): Comparator =
- Comparator { a, b ->
- when {
- a.name == name -> -1
- b.name == name -> 1
- else -> 0
- }
- }
-
- oldJar.entries()
- .toList()
- .distinctBy { it.name }
- .sortedWith(sortAlwaysFirst("META-INF/")
- .then(sortAlwaysFirst("META-INF/MANIFEST.MF"))
- .thenBy { it.name })
- .forEach { entry ->
- val jarEntry = JarEntry(entry.name)
-
- // Use the same constant as the fixed timestamps in normal copy actions
- jarEntry.time = ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES
-
- newJarStream.putNextEntry(jarEntry)
-
- oldJar.getInputStream(entry).copyTo(newJarStream)
- }
-
- newJarStream.finish()
- }
-
- newFile.renameTo(originalOutput)
- }
-}
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt b/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt
index a92f31195ad2..05f0a5b90397 100644
--- a/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt
+++ b/gradle/plugins/common/src/main/kotlin/junitbuild/exec/RunConsoleLauncher.kt
@@ -25,6 +25,9 @@ abstract class RunConsoleLauncher @Inject constructor(private val execOperations
@get:Classpath
abstract val runtimeClasspath: ConfigurableFileCollection
+ @get:Input
+ abstract val jvmArgs: ListProperty
+
@get:Input
abstract val args: ListProperty
@@ -62,6 +65,7 @@ abstract class RunConsoleLauncher @Inject constructor(private val execOperations
val output = ByteArrayOutputStream()
val result = execOperations.javaexec {
executable = javaLauncher.get().executablePath.asFile.absolutePath
+ jvmArgs(this@RunConsoleLauncher.jvmArgs.get())
classpath = runtimeClasspath
mainClass.set("org.junit.platform.console.ConsoleLauncher")
args(this@RunConsoleLauncher.args.get())
@@ -82,6 +86,12 @@ abstract class RunConsoleLauncher @Inject constructor(private val execOperations
result.rethrowFailure().assertNormalExitValue()
}
+ @Suppress("unused")
+ @Option(option = "jvm-args", description = "JVM args for the console launcher")
+ fun setVMArgs(args: String) {
+ jvmArgs.set(Commandline.translateCommandline(args).toList())
+ }
+
@Suppress("unused")
@Option(option = "args", description = "Additional command line arguments for the console launcher")
fun setCliArgs(args: String) {
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild/java/ExecJarAction.kt b/gradle/plugins/common/src/main/kotlin/junitbuild/java/ExecJarAction.kt
deleted file mode 100644
index 8a3cf49ad70e..000000000000
--- a/gradle/plugins/common/src/main/kotlin/junitbuild/java/ExecJarAction.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package junitbuild.java
-
-import org.gradle.api.Action
-import org.gradle.api.Task
-import org.gradle.api.provider.ListProperty
-import org.gradle.api.provider.Property
-import org.gradle.jvm.toolchain.JavaLauncher
-import org.gradle.process.ExecOperations
-import javax.inject.Inject
-
-abstract class ExecJarAction @Inject constructor(private val operations: ExecOperations): Action {
-
- abstract val javaLauncher: Property
-
- abstract val args: ListProperty
-
- override fun execute(t: Task) {
- operations.exec {
- executable = javaLauncher.get()
- .metadata.installationPath.file("bin/jar").asFile.absolutePath
- args = this@ExecJarAction.args.get()
- }
- }
-}
diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt b/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt
new file mode 100644
index 000000000000..41ddfdf3339c
--- /dev/null
+++ b/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt
@@ -0,0 +1,37 @@
+package junitbuild.java
+
+import org.gradle.api.Action
+import org.gradle.api.Task
+import org.gradle.api.internal.file.archive.ZipCopyAction
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.bundling.AbstractArchiveTask
+import org.gradle.jvm.toolchain.JavaLauncher
+import org.gradle.process.ExecOperations
+import java.time.Instant
+import javax.inject.Inject
+
+abstract class UpdateJarAction @Inject constructor(private val operations: ExecOperations) : Action {
+
+ abstract val javaLauncher: Property
+
+ abstract val args: ListProperty
+
+ abstract val date: Property
+
+ init {
+ date.convention(Instant.ofEpochMilli(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES))
+ }
+
+ override fun execute(t: Task) {
+ operations.exec {
+ executable = javaLauncher.get()
+ .metadata.installationPath.file("bin/jar").asFile.absolutePath
+ args = listOf(
+ "--update",
+ "--file", (t as AbstractArchiveTask).archiveFile.get().asFile.absolutePath,
+ "--date=${date.get()}"
+ ) + this@UpdateJarAction.args.get()
+ }
+ }
+}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java
index 2c7b509c9964..49692893bf80 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java
@@ -124,6 +124,8 @@ public final class Constants {
@API(status = STABLE, since = "5.10")
public static final String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME;
+ public static final String PARALLEL_EXECUTOR_PROPERTY_NAME = JupiterConfiguration.PARALLEL_EXECUTOR_PROPERTY_NAME;
+
/**
* Property name used to set the default test execution mode: {@value}
*
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
index dbd799b7f5b2..a00f0690f4c6 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java
@@ -11,6 +11,7 @@
package org.junit.jupiter.engine;
import static org.apiguardian.api.API.Status.INTERNAL;
+import static org.junit.jupiter.engine.config.JupiterConfiguration.ParallelExecutor.VIRTUAL;
import java.util.Optional;
@@ -22,6 +23,7 @@
import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory;
+import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
@@ -31,6 +33,7 @@
import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine;
import org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutorService;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
+import org.junit.platform.engine.support.hierarchical.VirtualThreadHierarchicalTestExecutorServiceFactory;
/**
* The JUnit Jupiter {@link org.junit.platform.engine.TestEngine TestEngine}.
@@ -74,8 +77,12 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId
protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest request) {
JupiterConfiguration configuration = getJupiterConfiguration(request);
if (configuration.isParallelExecutionEnabled()) {
- return new ForkJoinPoolHierarchicalTestExecutorService(new PrefixedConfigurationParameters(
- request.getConfigurationParameters(), Constants.PARALLEL_CONFIG_PREFIX));
+ ConfigurationParameters configurationParameters = new PrefixedConfigurationParameters(
+ request.getConfigurationParameters(), Constants.PARALLEL_CONFIG_PREFIX);
+ if (configuration.getParallelExecutor() == VIRTUAL) {
+ return VirtualThreadHierarchicalTestExecutorServiceFactory.create(configurationParameters);
+ }
+ return new ForkJoinPoolHierarchicalTestExecutorService(configurationParameters);
}
return super.createExecutorService(request);
}
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java
index 2d61b58c1c32..ce3a482718ac 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java
@@ -67,6 +67,12 @@ public boolean isExtensionAutoDetectionEnabled() {
key -> delegate.isExtensionAutoDetectionEnabled());
}
+ @Override
+ public ParallelExecutor getParallelExecutor() {
+ return (ParallelExecutor) cache.computeIfAbsent(PARALLEL_EXECUTOR_PROPERTY_NAME,
+ key -> delegate.getParallelExecutor());
+ }
+
@Override
public ExecutionMode getDefaultExecutionMode() {
return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java
index d64c4ceee318..8696eb50648f 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java
@@ -41,6 +41,9 @@
@API(status = INTERNAL, since = "5.4")
public class DefaultJupiterConfiguration implements JupiterConfiguration {
+ private static final EnumConfigurationParameterConverter parallelExecutorConverter = //
+ new EnumConfigurationParameterConverter<>(ParallelExecutor.class, "parallel executor");
+
private static final EnumConfigurationParameterConverter executionModeConverter = //
new EnumConfigurationParameterConverter<>(ExecutionMode.class, "parallel execution mode");
@@ -89,6 +92,12 @@ public boolean isExtensionAutoDetectionEnabled() {
return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false);
}
+ @Override
+ public ParallelExecutor getParallelExecutor() {
+ return parallelExecutorConverter.get(configurationParameters, PARALLEL_EXECUTOR_PROPERTY_NAME,
+ ParallelExecutor.FORK_JOIN_POOL);
+ }
+
@Override
public ExecutionMode getDefaultExecutionMode() {
return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java
index 559b4d7d5715..0303c08009b2 100644
--- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java
+++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java
@@ -36,6 +36,7 @@ public interface JupiterConfiguration {
String DEACTIVATE_CONDITIONS_PATTERN_PROPERTY_NAME = "junit.jupiter.conditions.deactivate";
String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.parallel.enabled";
+ String PARALLEL_EXECUTOR_PROPERTY_NAME = "junit.jupiter.execution.parallel.executor";
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
@@ -52,6 +53,8 @@ public interface JupiterConfiguration {
boolean isExtensionAutoDetectionEnabled();
+ ParallelExecutor getParallelExecutor();
+
ExecutionMode getDefaultExecutionMode();
ExecutionMode getDefaultClassesExecutionMode();
@@ -70,4 +73,7 @@ public interface JupiterConfiguration {
Supplier getDefaultTempDirFactorySupplier();
+ enum ParallelExecutor {
+ FORK_JOIN_POOL, VIRTUAL
+ }
}
diff --git a/junit-platform-commons/junit-platform-commons.gradle.kts b/junit-platform-commons/junit-platform-commons.gradle.kts
index bb6a7c336eab..696912b7ed9b 100644
--- a/junit-platform-commons/junit-platform-commons.gradle.kts
+++ b/junit-platform-commons/junit-platform-commons.gradle.kts
@@ -1,9 +1,6 @@
-import junitbuild.java.ExecJarAction
-
plugins {
id("junitbuild.java-library-conventions")
id("junitbuild.java-multi-release-sources")
- id("junitbuild.java-repackage-jars")
`java-test-fixtures`
}
@@ -18,11 +15,9 @@ dependencies {
tasks.jar {
val release9ClassesDir = sourceSets.mainRelease9.get().output.classesDirs.singleFile
inputs.dir(release9ClassesDir).withPathSensitivity(PathSensitivity.RELATIVE)
- doLast(objects.newInstance(ExecJarAction::class).apply {
+ doLast(objects.newInstance(junitbuild.java.UpdateJarAction::class).apply {
javaLauncher = javaToolchains.launcherFor(java.toolchain)
args.addAll(
- "--update",
- "--file", archiveFile.get().asFile.absolutePath,
"--release", "9",
"-C", release9ClassesDir.absolutePath, "."
)
diff --git a/junit-platform-console/junit-platform-console.gradle.kts b/junit-platform-console/junit-platform-console.gradle.kts
index 91acfd91b6d7..43e2aa56bbea 100644
--- a/junit-platform-console/junit-platform-console.gradle.kts
+++ b/junit-platform-console/junit-platform-console.gradle.kts
@@ -2,7 +2,6 @@ plugins {
id("junitbuild.java-library-conventions")
id("junitbuild.shadow-conventions")
id("junitbuild.java-multi-release-sources")
- id("junitbuild.java-repackage-jars")
}
description = "JUnit Platform Console"
@@ -40,11 +39,9 @@ tasks {
into("META-INF")
}
from(sourceSets.mainRelease9.get().output.classesDirs)
- doLast(objects.newInstance(junitbuild.java.ExecJarAction::class).apply {
+ doLast(objects.newInstance(junitbuild.java.UpdateJarAction::class).apply {
javaLauncher = project.javaToolchains.launcherFor(java.toolchain)
args.addAll(
- "--update",
- "--file", archiveFile.get().asFile.absolutePath,
"--main-class", "org.junit.platform.console.ConsoleLauncher",
"--release", "17",
"-C", release17ClassesDir.absolutePath, "."
diff --git a/junit-platform-engine/junit-platform-engine.gradle.kts b/junit-platform-engine/junit-platform-engine.gradle.kts
index 416b227b00c1..c5c6edcd7465 100644
--- a/junit-platform-engine/junit-platform-engine.gradle.kts
+++ b/junit-platform-engine/junit-platform-engine.gradle.kts
@@ -1,5 +1,6 @@
plugins {
id("junitbuild.java-library-conventions")
+ id("junitbuild.java-multi-release-sources")
`java-test-fixtures`
}
@@ -17,3 +18,25 @@ dependencies {
osgiVerification(projects.junitJupiterEngine)
osgiVerification(projects.junitPlatformLauncher)
}
+
+tasks.jar {
+ val release21ClassesDir = project.sourceSets.mainRelease21.get().output.classesDirs.singleFile
+ inputs.dir(release21ClassesDir).withPathSensitivity(PathSensitivity.RELATIVE)
+ doLast(objects.newInstance(junitbuild.java.UpdateJarAction::class).apply {
+ javaLauncher.set(project.javaToolchains.launcherFor {
+ languageVersion.set(java.toolchain.languageVersion.map {
+ if (it.canCompileOrRun(21)) it else JavaLanguageVersion.of(21)
+ })
+ })
+ args.addAll(
+ "--release", "21",
+ "-C", release21ClassesDir.absolutePath, "."
+ )
+ })
+}
+
+eclipse {
+ classpath {
+ sourceSets -= project.sourceSets.mainRelease21.get()
+ }
+}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java
index a37d9ea35c1c..1dd6dbdbb0cd 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/DefaultParallelExecutionConfigurationStrategy.java
@@ -216,7 +216,7 @@ public ParallelExecutionConfiguration createConfiguration(ConfigurationParameter
*/
public static final String CONFIG_CUSTOM_CLASS_PROPERTY_NAME = "custom.class";
- static ParallelExecutionConfigurationStrategy getStrategy(ConfigurationParameters configurationParameters) {
+ public static ParallelExecutionConfigurationStrategy getStrategy(ConfigurationParameters configurationParameters) {
return valueOf(
configurationParameters.get(CONFIG_STRATEGY_PROPERTY_NAME).orElse("dynamic").toUpperCase(Locale.ROOT));
}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java
index 7f3fc2214c08..61307fd5e030 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolHierarchicalTestExecutorService.java
@@ -208,10 +208,13 @@ public void compute() {
}
- static class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
+ public static class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ public WorkerThreadFactory() {
+ }
+
@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return new WorkerThread(pool, contextClassLoader);
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java
new file mode 100644
index 000000000000..9e3bad09c9cb
--- /dev/null
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2015-2023 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.platform.engine.support.hierarchical;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+import org.junit.platform.engine.ConfigurationParameters;
+
+@API(status = EXPERIMENTAL, since = "1.10.1")
+public class VirtualThreadHierarchicalTestExecutorServiceFactory {
+
+ public static HierarchicalTestExecutorService create(
+ @SuppressWarnings("unused") ConfigurationParameters configurationParameters) {
+ throw new IllegalArgumentException("The virtual executor is only supported on Java 21 and above");
+ }
+
+ private VirtualThreadHierarchicalTestExecutorServiceFactory() {
+ throw new AssertionError();
+ }
+}
diff --git a/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorService.java b/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorService.java
new file mode 100644
index 000000000000..85069057b270
--- /dev/null
+++ b/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorService.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015-2023 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.platform.engine.support.hierarchical;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.function.Predicate.isEqual;
+import static java.util.stream.Collectors.toCollection;
+import static org.junit.platform.commons.util.FunctionUtils.where;
+import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.CONCURRENT;
+import static org.junit.platform.engine.support.hierarchical.Node.ExecutionMode.SAME_THREAD;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+import org.junit.platform.commons.util.ExceptionUtils;
+import org.junit.platform.engine.ConfigurationParameters;
+import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode;
+
+class VirtualThreadHierarchicalTestExecutorService implements HierarchicalTestExecutorService {
+
+ private final ClassLoader contextClassLoader;
+ private final ForkJoinPool forkJoinPool;
+ private final ExecutorService executorService;
+
+ VirtualThreadHierarchicalTestExecutorService(ConfigurationParameters configurationParameters) {
+ contextClassLoader = Thread.currentThread().getContextClassLoader();
+ var strategy = DefaultParallelExecutionConfigurationStrategy.getStrategy(configurationParameters);
+ var configuration = strategy.createConfiguration(configurationParameters);
+ var systemThreadFactory = new ForkJoinPoolHierarchicalTestExecutorService.WorkerThreadFactory();
+ forkJoinPool = new ForkJoinPool(configuration.getParallelism(), systemThreadFactory, null, false,
+ configuration.getCorePoolSize(), configuration.getMaxPoolSize(), configuration.getMinimumRunnable(), null,
+ configuration.getKeepAliveSeconds(), TimeUnit.SECONDS);
+ var virtualThreadFactory = Thread.ofVirtual().name("junit-executor", 1).factory();
+ executorService = Executors.newThreadPerTaskExecutor(virtualThreadFactory);
+ }
+
+ @Override
+ public CompletableFuture submit(TestTask testTask) {
+ if (testTask.getExecutionMode() == CONCURRENT) {
+ return CompletableFuture.runAsync(() -> executeWithLocksAndContextClassLoader(testTask), executorService);
+ }
+ executeWithLocks(testTask);
+ return completedFuture(null);
+ }
+
+ private void executeWithLocksAndContextClassLoader(TestTask testTask) {
+ Thread.currentThread().setContextClassLoader(contextClassLoader);
+ executeWithLocks(testTask);
+ }
+
+ private void executeWithLocks(TestTask testTask) {
+ var lock = testTask.getResourceLock();
+ try {
+ lock.acquire();
+ testTask.execute();
+ }
+ catch (InterruptedException e) {
+ ExceptionUtils.throwAsUncheckedException(e);
+ }
+ finally {
+ lock.release();
+ }
+ }
+
+ @Override
+ public void invokeAll(List extends TestTask> testTasks) {
+ var futures = submitAll(testTasks, CONCURRENT).collect(toCollection(ArrayList::new));
+ submitAll(testTasks, SAME_THREAD).forEach(futures::add);
+ allOf(futures).join();
+ }
+
+ private Stream> submitAll(List extends TestTask> testTasks, ExecutionMode mode) {
+ return testTasks.stream().filter(where(TestTask::getExecutionMode, isEqual(mode))).map(this::submit);
+ }
+
+ private CompletableFuture allOf(List> futures) {
+ return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
+ }
+
+ @Override
+ public void close() {
+ executorService.close();
+ forkJoinPool.close();
+ }
+}
diff --git a/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java b/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java
new file mode 100644
index 000000000000..b01c9432c5e1
--- /dev/null
+++ b/junit-platform-engine/src/main/java21/org/junit/platform/engine/support/hierarchical/VirtualThreadHierarchicalTestExecutorServiceFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015-2023 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.platform.engine.support.hierarchical;
+
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+
+import org.apiguardian.api.API;
+import org.junit.platform.engine.ConfigurationParameters;
+
+@API(status = EXPERIMENTAL, since = "1.10.1")
+public class VirtualThreadHierarchicalTestExecutorServiceFactory {
+
+ public static HierarchicalTestExecutorService create(ConfigurationParameters configurationParameters) {
+ return new VirtualThreadHierarchicalTestExecutorService(configurationParameters);
+ }
+
+ private VirtualThreadHierarchicalTestExecutorServiceFactory() {
+ throw new AssertionError();
+ }
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolParallelExecutionIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolParallelExecutionIntegrationTests.java
new file mode 100644
index 000000000000..912b76027d61
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ForkJoinPoolParallelExecutionIntegrationTests.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2015-2023 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.platform.engine.support.hierarchical;
+
+public class ForkJoinPoolParallelExecutionIntegrationTests extends ParallelExecutionIntegrationTests {
+ @Override
+ protected String getParallelExecutor() {
+ return "FORK_JOIN_POOL";
+ }
+}
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
index ca7433377bf1..fa140a4b6331 100644
--- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/ParallelExecutionIntegrationTests.java
@@ -23,6 +23,7 @@
import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME;
import static org.junit.jupiter.engine.Constants.PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME;
import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME;
+import static org.junit.jupiter.engine.Constants.PARALLEL_EXECUTOR_PROPERTY_NAME;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.event;
@@ -74,7 +75,9 @@
/**
* @since 1.3
*/
-class ParallelExecutionIntegrationTests {
+abstract class ParallelExecutionIntegrationTests {
+
+ protected abstract String getParallelExecutor();
@Test
void successfulParallelTest(TestReporter reporter) {
@@ -89,7 +92,7 @@ void successfulParallelTest(TestReporter reporter) {
assertThat(finishedTimestamps).hasSize(3);
assertThat(startedTimestamps).allMatch(startTimestamp -> finishedTimestamps.stream().noneMatch(
finishedTimestamp -> finishedTimestamp.isBefore(startTimestamp)));
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
+ assertThreadNamesCount(events, 3);
}
@Test
@@ -103,7 +106,7 @@ void successfulTestWithMethodLock() {
var events = executeConcurrently(3, SuccessfulWithMethodLockTestCase.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
+ assertThreadNamesCount(events, 3);
}
@Test
@@ -111,7 +114,7 @@ void successfulTestWithClassLock() {
var events = executeConcurrently(3, SuccessfulWithClassLockTestCase.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
+ assertThreadNamesCount(events, 1);
}
@Test
@@ -119,7 +122,7 @@ void testCaseWithFactory() {
var events = executeConcurrently(3, TestCaseWithTestFactory.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
+ assertThreadNamesCount(events, 1);
}
@Test
@@ -132,8 +135,8 @@ void customContextClassLoader() {
var events = executeConcurrently(3, SuccessfulWithMethodLockTestCase.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(3);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(3);
assertThat(ThreadReporter.getLoaderNames(events)).containsExactly("(-:");
+ assertThreadNamesCount(events, 3);
}
finally {
currentThread.setContextClassLoader(currentLoader);
@@ -153,7 +156,7 @@ void locksOnNestedTests() {
var events = executeConcurrently(3, TestCaseWithNestedLocks.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(6);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
+ assertThreadNamesCount(events, 1);
}
@Test
@@ -181,7 +184,7 @@ void executesTestTemplatesWithResourceLocksInSameThread() {
var events = executeConcurrently(2, ConcurrentTemplateTestCase.class);
assertThat(events.stream().filter(event(test(), finishedSuccessfully())::matches)).hasSize(10);
- assertThat(ThreadReporter.getThreadNames(events)).hasSize(1);
+ assertThreadNamesCount(events, 1);
}
@Test
@@ -193,13 +196,13 @@ void executesClassesInParallelIfEnabledViaConfigurationParameter() {
ParallelClassesTestCaseB.class, ParallelClassesTestCaseC.class);
results.testEvents().assertStatistics(stats -> stats.succeeded(9));
- assertThat(ThreadReporter.getThreadNames(results.allEvents().list())).hasSize(3);
+ assertThreadNamesCount(results.allEvents().list(), 3);
var testClassA = findFirstTestDescriptor(results, container(ParallelClassesTestCaseA.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassA))).hasSize(1);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassA), 1);
var testClassB = findFirstTestDescriptor(results, container(ParallelClassesTestCaseB.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassB))).hasSize(1);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassB), 1);
var testClassC = findFirstTestDescriptor(results, container(ParallelClassesTestCaseC.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassC))).hasSize(1);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassC), 1);
}
@Test
@@ -215,11 +218,11 @@ void executesMethodsInParallelIfEnabledViaConfigurationParameter() {
results.testEvents().assertStatistics(stats -> stats.succeeded(9));
assertThat(ThreadReporter.getThreadNames(results.allEvents().list())).hasSizeGreaterThanOrEqualTo(3);
var testClassA = findFirstTestDescriptor(results, container(ParallelMethodsTestCaseA.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassA))).hasSize(3);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassA), 3);
var testClassB = findFirstTestDescriptor(results, container(ParallelMethodsTestCaseB.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassB))).hasSize(3);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassB), 3);
var testClassC = findFirstTestDescriptor(results, container(ParallelMethodsTestCaseC.class));
- assertThat(ThreadReporter.getThreadNames(getEventsOfChildren(results, testClassC))).hasSize(3);
+ assertThreadNamesCount(getEventsOfChildren(results, testClassC), 3);
}
@Test
@@ -394,6 +397,7 @@ private EngineExecutionResults executeWithFixedParallelism(int parallelism, Map<
var discoveryRequest = request()
.selectors(Arrays.stream(testClasses).map(DiscoverySelectors::selectClass).collect(toList()))
.configurationParameter(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME, String.valueOf(true))
+ .configurationParameter(PARALLEL_EXECUTOR_PROPERTY_NAME, getParallelExecutor())
.configurationParameter(PARALLEL_CONFIG_STRATEGY_PROPERTY_NAME, "fixed")
.configurationParameter(PARALLEL_CONFIG_FIXED_PARALLELISM_PROPERTY_NAME, String.valueOf(parallelism))
.configurationParameters(configParams)
@@ -402,6 +406,10 @@ private EngineExecutionResults executeWithFixedParallelism(int parallelism, Map<
return EngineTestKit.execute("junit-jupiter", discoveryRequest);
}
+ protected void assertThreadNamesCount(List events, int expectedCount) {
+ assertThat(ThreadReporter.getThreadNames(events)).hasSize(expectedCount);
+ }
+
// -------------------------------------------------------------------------
@ExtendWith(ThreadReporter.class)
diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/VirtualParallelExecutionIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/VirtualParallelExecutionIntegrationTests.java
new file mode 100644
index 000000000000..952e41dec58e
--- /dev/null
+++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/VirtualParallelExecutionIntegrationTests.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2015-2023 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.platform.engine.support.hierarchical;
+
+import static org.junit.jupiter.api.condition.JRE.JAVA_21;
+
+import org.junit.jupiter.api.condition.EnabledOnJre;
+
+@EnabledOnJre(value = JAVA_21, disabledReason = "Use Java 21 features")
+public class VirtualParallelExecutionIntegrationTests extends ParallelExecutionIntegrationTests {
+
+ @Override
+ protected String getParallelExecutor() {
+ return "virtual";
+ }
+
+}