Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ val vintageProjects by extra(listOf(
dependencyProject(projects.junitVintageEngine)
))

val mavenizedProjects by extra(platformProjects + jupiterProjects + vintageProjects)
val mavenizedProjects by extra(listOf(dependencyProject(projects.junitOnramp)) + platformProjects + jupiterProjects + vintageProjects)
val modularProjects by extra(mavenizedProjects - setOf(dependencyProject(projects.junitPlatformConsoleStandalone)))

dependencies {
Expand Down
24 changes: 24 additions & 0 deletions junit-onramp/junit-onramp.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import junitbuild.java.UpdateJarAction

plugins {
id("junitbuild.java-library-conventions")
id("junitbuild.java-nullability-conventions")
}

description = "JUnit On-Ramp Module"

dependencies {
api(platform(projects.junitBom))
api(projects.junitJupiter)

compileOnlyApi(libs.apiguardian)
compileOnlyApi(libs.jspecify)
compileOnlyApi(projects.junitJupiterEngine)

implementation(projects.junitPlatformLauncher)
implementation(projects.junitPlatformConsole)
}

japicmp {
enabled = false // no previous version, yet
}
37 changes: 37 additions & 0 deletions junit-onramp/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

/**
* Defines the API of the JUnit On-Ramp module for writing and running tests.
* <p>
* Usage example:
* <pre>{@code
* import module org.junit.onramp;
*
* void main() {
* JUnit.run();
* }
*
* @Test
* void addition() {
* Assertions.assertEquals(2, 1 + 1, "Addition error detected!");
* }
* }</pre>
*/
module org.junit.onramp {
requires static transitive org.apiguardian.api;
requires static transitive org.jspecify;

requires transitive org.junit.jupiter;
requires org.junit.platform.launcher;
requires org.junit.platform.console;

exports org.junit.onramp;
}
80 changes: 80 additions & 0 deletions junit-onramp/src/main/java/org/junit/onramp/JUnit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.onramp;

import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectModule;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;

import java.io.PrintWriter;
import java.nio.charset.Charset;

import org.apiguardian.api.API;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.console.output.ColorPalette;
import org.junit.platform.console.output.Theme;
import org.junit.platform.console.output.TreePrintingListener;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;

@API(status = EXPERIMENTAL, since = "6.0")
public final class JUnit {
/**
* Run all tests defined in the caller class.
*/
public static void run() {
var walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
run(selectClass(walker.getCallerClass()));
}

/**
* Run all tests defined in the given test class.
* @param testClass the class to discover and execute tests in
*/
public static void run(Class<?> testClass) {
run(selectClass(testClass));
}

/**
* Run all tests defined in the given module.
* @param testModule the module to discover and execute tests in
*/
public static void run(Module testModule) {
run(selectModule(testModule));
}

private static void run(DiscoverySelector selector) {
var listener = new SummaryGeneratingListener();
var charset = Charset.defaultCharset();
var writer = new PrintWriter(System.out, true, charset);
var printer = new TreePrintingListener(writer, ColorPalette.DEFAULT, Theme.valueOf(charset));
var request = request().selectors(selector).forExecution() //
.listeners(listener, printer) //
.build();
var launcher = LauncherFactory.create();
launcher.execute(request);
var summary = listener.getSummary();

if (summary.getTotalFailureCount() == 0)
return;

summary.printFailuresTo(new PrintWriter(System.err, true, charset));
throw new JUnitException("JUnit run finished with %d failure%s".formatted( //
summary.getTotalFailureCount(), //
summary.getTotalFailureCount() == 1 ? "" : "s"));
}

private JUnit() {
}
}
8 changes: 8 additions & 0 deletions junit-onramp/src/main/java/org/junit/onramp/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Contains JUnit On-Ramp API for writing and running tests.
*/

@NullMarked
package org.junit.onramp;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,21 @@
class ClasspathFilters {

static final String CLASS_FILE_SUFFIX = ".class";
static final String SOURCE_FILE_SUFFIX = ".java";

// System property defined since Java 12: https://bugs.java/bugdatabase/JDK-8210877
private static final boolean SOURCE_MODE = System.getProperty("jdk.launcher.sourcefile") != null;

private static final String PACKAGE_INFO_FILE_NAME = "package-info" + CLASS_FILE_SUFFIX;
private static final String MODULE_INFO_FILE_NAME = "module-info" + CLASS_FILE_SUFFIX;

static boolean isClassOrSourceFileName(String name) {
return name.endsWith(CLASS_FILE_SUFFIX) || (SOURCE_MODE && name.endsWith(SOURCE_FILE_SUFFIX));
}

static Predicate<Path> classFiles() {
return file -> isNotPackageInfo(file) && isNotModuleInfo(file) && isClassFile(file);
return file -> isNotPackageInfo(file) && isNotModuleInfo(file)
&& (isClassFile(file) || (SOURCE_MODE && isSourceFile(file)));
}

static Predicate<Path> resourceFiles() {
Expand All @@ -42,6 +52,10 @@ private static boolean isClassFile(Path file) {
return file.getFileName().toString().endsWith(CLASS_FILE_SUFFIX);
}

private static boolean isSourceFile(Path file) {
return file.getFileName().toString().endsWith(SOURCE_FILE_SUFFIX);
}

private ClasspathFilters() {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
package org.junit.platform.commons.util;

import static java.util.stream.Collectors.joining;
import static org.junit.platform.commons.util.ClasspathFilters.CLASS_FILE_SUFFIX;
import static org.junit.platform.commons.util.StringUtils.isNotBlank;

import java.io.IOException;
Expand All @@ -32,6 +31,7 @@
import java.util.function.Supplier;
import java.util.stream.Stream;

import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.function.Try;
import org.junit.platform.commons.io.Resource;
Expand Down Expand Up @@ -182,10 +182,10 @@ private static void walkFilesForUri(URI baseUri, Predicate<Path> filter, BiConsu
}
}

private void processClassFileSafely(Path baseDir, String basePackageName, ClassFilter classFilter, Path classFile,
private void processClassFileSafely(Path baseDir, String basePackageName, ClassFilter classFilter, Path file,
Consumer<Class<?>> classConsumer) {
try {
String fullyQualifiedClassName = determineFullyQualifiedClassName(baseDir, basePackageName, classFile);
String fullyQualifiedClassName = determineFullyQualifiedClassName(baseDir, basePackageName, file);
if (classFilter.match(fullyQualifiedClassName)) {
try {
// @formatter:off
Expand All @@ -196,12 +196,12 @@ private void processClassFileSafely(Path baseDir, String basePackageName, ClassF
// @formatter:on
}
catch (InternalError internalError) {
handleInternalError(classFile, fullyQualifiedClassName, internalError);
handleInternalError(file, fullyQualifiedClassName, internalError);
}
}
}
catch (Throwable throwable) {
handleThrowable(classFile, throwable);
handleThrowable(file, throwable);
}
}

Expand All @@ -221,12 +221,12 @@ private void processResourceFileSafely(Path baseDir, String basePackageName, Res
}
}

private String determineFullyQualifiedClassName(Path baseDir, String basePackageName, Path classFile) {
private String determineFullyQualifiedClassName(Path baseDir, String basePackageName, Path file) {
// @formatter:off
return Stream.of(
basePackageName,
determineSubpackageName(baseDir, classFile),
determineSimpleClassName(classFile)
determineSubpackageName(baseDir, file),
determineSimpleClassName(file)
)
.filter(value -> !value.isEmpty()) // Handle default package appropriately.
.collect(joining(PACKAGE_SEPARATOR_STRING));
Expand All @@ -253,21 +253,29 @@ private String determineFullyQualifiedResourceName(Path baseDir, String basePack
// @formatter:on
}

private String determineSimpleClassName(Path classFile) {
String fileName = classFile.getFileName().toString();
return fileName.substring(0, fileName.length() - CLASS_FILE_SUFFIX.length());
private String determineSimpleClassName(Path file) {
String fileName = file.getFileName().toString();
return determineSimpleClassName(fileName);
}

static String determineSimpleClassName(String fileName) {
int lastDot = fileName.lastIndexOf('.');
if (lastDot < 0) {
throw new JUnitException("Expected file name with file extension, but got: " + fileName);
}
return fileName.substring(0, lastDot);
}

private String determineSimpleResourceName(Path resourceFile) {
return resourceFile.getFileName().toString();
}

private String determineSubpackageName(Path baseDir, Path classFile) {
Path relativePath = baseDir.relativize(classFile.getParent());
private String determineSubpackageName(Path baseDir, Path file) {
Path relativePath = baseDir.relativize(file.getParent());
String pathSeparator = baseDir.getFileSystem().getSeparator();
String subpackageName = relativePath.toString().replace(pathSeparator, PACKAGE_SEPARATOR_STRING);
if (subpackageName.endsWith(pathSeparator)) {
// Workaround for JDK bug: https://bugs.openjdk.java.net/browse/JDK-8153248
// TODO: Remove workaround for JDK bug: https://bugs.openjdk.org/browse/JDK-8153248
subpackageName = subpackageName.substring(0, subpackageName.length() - pathSeparator.length());
}
return subpackageName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
@API(status = INTERNAL, since = "1.0")
public final class ExceptionUtils {

private static final String JUNIT_ON_RAMP_PACKAGE_PREFIX = "org.junit.onramp.";

private static final String JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX = "org.junit.platform.launcher.";

private static final Predicate<String> STACK_TRACE_ELEMENT_FILTER = ClassNamePatternFilterUtils //
Expand Down Expand Up @@ -139,6 +141,9 @@ public static void pruneStackTrace(Throwable throwable, List<String> classNames)
prunedStackTrace.addAll(stackTrace.subList(i, stackTrace.size()));
break;
}
else if (className.startsWith(JUNIT_ON_RAMP_PACKAGE_PREFIX)) {
prunedStackTrace.clear();
}
else if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) {
prunedStackTrace.clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,11 @@ List<Class<?>> scan(ModuleReference reference) {
try (ModuleReader reader = reference.open()) {
try (Stream<String> names = reader.list()) {
// @formatter:off
return names.filter(name -> name.endsWith(".class"))
.map(this::className)
return names.filter(ClasspathFilters::isClassOrSourceFileName)
.map(DefaultClasspathScanner::determineSimpleClassName)
.map(name -> name.replace('/', '.'))
.filter(name -> !"module-info".equals(name))
.filter(name -> !name.endsWith("package-info"))
.filter(classFilter::match)
.<Class<?>> map(this::loadClassUnchecked)
.filter(classFilter::match)
Expand All @@ -268,15 +270,6 @@ List<Class<?>> scan(ModuleReference reference) {
}
}

/**
* Convert resource name to binary class name.
*/
private String className(String resourceName) {
resourceName = resourceName.substring(0, resourceName.length() - 6); // 6 = ".class".length()
resourceName = resourceName.replace('/', '.');
return resourceName;
}

/**
* Load class by its binary name.
*
Expand Down
1 change: 1 addition & 0 deletions junit-platform-console/junit-platform-console.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
tasks {
compileJava {
options.compilerArgs.addAll(listOf(
"-Xlint:-module", // due to qualified exports
"--add-modules", "info.picocli",
"--add-reads", "${javaModuleName}=info.picocli"
))
Expand Down
2 changes: 2 additions & 0 deletions junit-platform-console/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@
requires org.junit.platform.launcher;
requires org.junit.platform.reporting;

exports org.junit.platform.console.output to org.junit.onramp;

provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
org.junit.onramp@${version} jar:file:.+/junit-onramp-\d.+\.jar..module-info\.class
exports org.junit.onramp
requires java.base mandated
requires org.apiguardian.api static transitive
requires org.jspecify static transitive
requires org.junit.jupiter transitive
requires org.junit.platform.console
requires org.junit.platform.launcher
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ requires org.junit.platform.engine
requires org.junit.platform.launcher
requires org.junit.platform.reporting
provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider
qualified exports org.junit.platform.console.output to org.junit.onramp
contains org.junit.platform.console
contains org.junit.platform.console.command
contains org.junit.platform.console.options
contains org.junit.platform.console.output
contains org.junit.platform.console.shadow.picocli
main-class org.junit.platform.console.ConsoleLauncher
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ void loadModuleDirectoryNames() {
"junit-jupiter-engine", //
"junit-jupiter-migrationsupport", //
"junit-jupiter-params", //
"junit-onramp", //
"junit-platform-commons", //
"junit-platform-console", //
"junit-platform-engine", //
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ include("junit-jupiter-api")
include("junit-jupiter-engine")
include("junit-jupiter-migrationsupport")
include("junit-jupiter-params")
include("junit-onramp")
include("junit-platform-commons")
include("junit-platform-console")
include("junit-platform-console-standalone")
Expand Down