Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add virtual thread support #3443

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/actions/setup-test-jdk/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ runs:
java-version: 8
- shell: bash
run: echo "JDK8=$JAVA_HOME" >> $GITHUB_ENV
- uses: oracle-actions/setup-java@v1
with:
website: jdk.java.net
release: 21
- shell: bash
run: echo "JDK21=$JAVA_HOME" >> $GITHUB_ENV
2 changes: 2 additions & 0 deletions gradle/config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
files="junit-platform-commons[\\/]src[\\/]main[\\/]java.+?[\\/]org[\\/]junit[\\/]platform[\\/]commons[\\/]util[\\/]*"/>
<suppress checks="JavadocPackage"
files="junit-platform-console[\\/]src[\\/]main[\\/]java.+?[\\/]org[\\/]junit[\\/]platform[\\/]console[\\/]*"/>
<suppress checks="JavadocPackage"
files="junit-platform-engine[\\/]src[\\/]main[\\/]java.+[\\/]org[\\/]junit[\\/]platform[\\/]engine[\\/]support[\\/]hierarchical[\\/]*"/>
</suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {

val mavenizedProjects: List<Project> 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
Expand All @@ -27,6 +27,11 @@ listOf(9, 17).forEach { javaVersion ->

named<JavaCompile>(sourceSet.get().compileJavaTaskName).configure {
options.release = javaVersion
if (javaVersion == 21) {
javaCompiler.set(javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(javaVersion))
})
}
}

named<Checkstyle>("checkstyle${sourceSet.name.capitalized()}").configure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ abstract class RunConsoleLauncher @Inject constructor(private val execOperations
@get:Classpath
abstract val runtimeClasspath: ConfigurableFileCollection

@get:Input
abstract val jvmArgs: ListProperty<String>

marcphilipp marked this conversation as resolved.
Show resolved Hide resolved
@get:Input
abstract val args: ListProperty<String>

Expand Down Expand Up @@ -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([email protected]())
classpath = runtimeClasspath
mainClass.set("org.junit.platform.console.ConsoleLauncher")
args([email protected]())
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ needs an @API annotation and Javadoc


/**
* Property name used to set the default test execution mode: {@value}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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}.
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
@API(status = INTERNAL, since = "5.4")
public class DefaultJupiterConfiguration implements JupiterConfiguration {

private static final EnumConfigurationParameterConverter<ParallelExecutor> parallelExecutorConverter = //
new EnumConfigurationParameterConverter<>(ParallelExecutor.class, "parallel executor");

private static final EnumConfigurationParameterConverter<ExecutionMode> executionModeConverter = //
new EnumConfigurationParameterConverter<>(ExecutionMode.class, "parallel execution mode");

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -52,6 +53,8 @@ public interface JupiterConfiguration {

boolean isExtensionAutoDetectionEnabled();

ParallelExecutor getParallelExecutor();

ExecutionMode getDefaultExecutionMode();

ExecutionMode getDefaultClassesExecutionMode();
Expand All @@ -70,4 +73,7 @@ public interface JupiterConfiguration {

Supplier<TempDirFactory> getDefaultTempDirFactorySupplier();

enum ParallelExecutor {
FORK_JOIN_POOL, VIRTUAL
}
}
26 changes: 26 additions & 0 deletions junit-platform-engine/junit-platform-engine.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("junitbuild.java-library-conventions")
id("junitbuild.java-multi-release-sources")
`java-test-fixtures`
}

Expand All @@ -17,3 +18,28 @@ 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.ExecJarAction::class).apply {
javaLauncher.set(project.javaToolchains.launcherFor {
languageVersion.set(java.toolchain.languageVersion.map {
if (it.canCompileOrRun(21)) it else JavaLanguageVersion.of(21)
})
})
args.addAll(
"--update",
"--file", archiveFile.get().asFile.absolutePath,
"--release", "21",
"-C", release21ClassesDir.absolutePath, "."
)
})
}


eclipse {
classpath {
sourceSets -= project.sourceSets.mainRelease21.get()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Doesn't need to be public

return valueOf(
configurationParameters.get(CONFIG_STRATEGY_PROPERTY_NAME).orElse("dynamic").toUpperCase(Locale.ROOT));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,13 @@ public void compute() {

}

static class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
public static class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Doesn't need to be public


private final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

public WorkerThreadFactory() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Can be removed?

}

@Override
public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
return new WorkerThread(pool, contextClassLoader);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 org.junit.platform.engine.ConfigurationParameters;

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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Void> 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<CompletableFuture<Void>> submitAll(List<? extends TestTask> testTasks, ExecutionMode mode) {
return testTasks.stream().filter(where(TestTask::getExecutionMode, isEqual(mode))).map(this::submit);
}

private CompletableFuture<Void> allOf(List<CompletableFuture<Void>> futures) {
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
}

@Override
public void close() {
executorService.close();
forkJoinPool.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 org.junit.platform.engine.ConfigurationParameters;

public class VirtualThreadHierarchicalTestExecutorServiceFactory {

public static HierarchicalTestExecutorService create(ConfigurationParameters configurationParameters) {
return new VirtualThreadHierarchicalTestExecutorService(configurationParameters);
}

private VirtualThreadHierarchicalTestExecutorServiceFactory() {
throw new AssertionError();
}
}
Loading