Skip to content
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>

@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