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 testTasks) { + var futures = submitAll(testTasks, CONCURRENT).collect(toCollection(ArrayList::new)); + submitAll(testTasks, SAME_THREAD).forEach(futures::add); + allOf(futures).join(); + } + + private Stream> submitAll(List 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"; + } + +}