Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3afdf0a
Introduce experimental junit-onramp module
marcphilipp Aug 19, 2025
2fdb122
Reduce API surface
sormuras Oct 9, 2025
e59b227
Consider `*.java` source files as classes in source mode
sormuras Oct 10, 2025
8cdf2f5
Overload `run()` API and reuse console printing listener
sormuras Oct 10, 2025
4d1f062
Use tree printer by default
sormuras Oct 10, 2025
f4ba4fc
Skip module declaration files
sormuras Oct 10, 2025
ea1e686
Add tests using the `org.junit.onramp` module
sormuras Oct 16, 2025
0eb6fa0
Fix copyright headers
sormuras Oct 16, 2025
64d6554
Merge remote-tracking branch 'origin/main' into experiments/junit-onramp
sormuras Oct 16, 2025
a3c4b61
Minor cleanup
sormuras Oct 16, 2025
f542616
Fix configuration
sormuras Oct 16, 2025
2308620
Rename to `org.junit.start`
sormuras Oct 17, 2025
4dd14d6
More renaming
sormuras Oct 17, 2025
61c678d
Assert test method names are printed
sormuras Oct 17, 2025
a170306
Merge branch 'main' into experiments/junit-onramp
sormuras Oct 17, 2025
1e060d0
Check for `NO_COLOR` environment variable
sormuras Oct 29, 2025
efb6cd8
Move more common logic into `SearchPathUtils`
marcphilipp Oct 30, 2025
bb31561
Update junit-start/junit-start.gradle.kts
sormuras Oct 30, 2025
8199ca7
Remove workaround for bug fixed in JDK 9 and later
marcphilipp Oct 30, 2025
dea7a32
Merge branch 'main' into experiments/junit-onramp
sormuras Nov 1, 2025
13cabb8
Use Markdown syntax in API documentation
sormuras Nov 4, 2025
6c5ca16
Update User Guide, API documentation overview, and Release Notes
sormuras Nov 4, 2025
fabc52c
Update documentation/src/docs/asciidoc/release-notes/release-notes-6.…
sormuras Nov 7, 2025
d15673a
Apply review changes
sormuras Nov 7, 2025
3ff5a4e
Remove source snippet from release note
sormuras Nov 7, 2025
49bf8ea
Add `JUnitStartTests.java` to documentation project
sormuras Nov 8, 2025
f912b00
Revert "Add `JUnitStartTests.java` to documentation project"
sormuras Nov 9, 2025
fc5e954
Reword "Source Launcher" introduction
sormuras Nov 9, 2025
71b8c0a
Merge branch 'main' into experiments/junit-onramp
mpkorstanje Nov 9, 2025
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.junitStart)) + platformProjects + jupiterProjects + vintageProjects)
val modularProjects by extra(mavenizedProjects - setOf(dependencyProject(projects.junitPlatformConsoleStandalone)))

dependencies {
Expand Down
2 changes: 2 additions & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ endif::[]
// Jupiter Migration Support
:EnableJUnit4MigrationSupport: {javadoc-root}/org.junit.jupiter.migrationsupport/org/junit/jupiter/migrationsupport/EnableJUnit4MigrationSupport.html[@EnableJUnit4MigrationSupport]
:EnableRuleMigrationSupport: {javadoc-root}/org.junit.jupiter.migrationsupport/org/junit/jupiter/migrationsupport/rules/EnableRuleMigrationSupport.html[@EnableRuleMigrationSupport]
// JUnit Start
:JUnit: {javadoc-root}/org.junit.start/org/junit/start/JUnit.html[JUnit]
// Vintage
:junit-vintage-engine: {javadoc-root}/org.junit.vintage.engine/org/junit/vintage/engine/package-summary.html[junit-vintage-engine]
// Examples Repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ repository on GitHub.
[[release-notes-6.1.0-M1-junit-jupiter-new-features-and-improvements]]
==== New Features and Improvements

* Introduce new module `org.junit.start` for writing and running tests. It simplifies
using JUnit in compact source files together with a single module import statement:
* Introduce new `dynamicTest(Consumer<? super Configuration>)` factory method for dynamic
tests. It allows configuring the `ExecutionMode` of the dynamic test in addition to its
display name, test source URI, and executable.
Expand Down
33 changes: 33 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/running-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,39 @@ DYNAMIC = 35
REPORTED = 37
----

[[running-tests-source-launcher]]
=== Source Launcher

Starting with Java 25 it is possible to write minimal source code test programs
using the `org.junit.start` module. For example, like in a `HelloTests.java`
file reading:

```java
import module org.junit.start;

void main() {
JUnit.run();
}

@Test
void stringLength() {
Assertions.assertEquals(11, "Hello JUnit".length());
}
```
With all required modular JAR files available in a local `lib/` directory, the
following Java 25+ command will discover and execute tests using the JUnit Platform.
It will also print the result tree to the console.

```shell
java --module-path lib --add-modules org.junit.start HelloTests.java
└─ JUnit Jupiter ✔
└─ HelloTests ✔
└─ stringLength() ✔
```

Find JUnit's class API documentation here: {JUnit}

[[running-tests-discovery-selectors]]
=== Discovery Selectors

Expand Down
7 changes: 5 additions & 2 deletions documentation/src/javadoc/junit-overview.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<body>

<p>This document consists of three sections:</p>
<p>This document consists of four sections:</p>

<dl>
<dt>Platform</dt>
Expand All @@ -12,13 +12,16 @@
</dd>
<dt>Jupiter</dt>
<dd>JUnit Jupiter is the combination of the programming model and extension model for
writing JUnit tests and extensions. The Jupiter sub-project provides a TestEngine
writing JUnit tests and extensions. The Jupiter subproject provides a TestEngine
for running Jupiter based tests on the platform.
</dd>
<dt>Vintage</dt>
<dd>JUnit Vintage provides a TestEngine for running JUnit 3 and JUnit 4 based tests on the
platform.
</dd>
<dt>Other Modules</dt>
<dd>This section lists all modules that are not part of a dedicated section.
</dd>
</dl>

<p>Already consulted the <a href="../user-guide/index.html">JUnit User Guide</a>?</p>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
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.SearchPathUtils.PACKAGE_SEPARATOR_CHAR;
import static org.junit.platform.commons.util.SearchPathUtils.PACKAGE_SEPARATOR_STRING;
import static org.junit.platform.commons.util.SearchPathUtils.determineSimpleClassName;
import static org.junit.platform.commons.util.StringUtils.isNotBlank;

import java.io.IOException;
Expand Down Expand Up @@ -57,8 +59,6 @@ class DefaultClasspathScanner implements ClasspathScanner {
private static final char CLASSPATH_RESOURCE_PATH_SEPARATOR = '/';
private static final String CLASSPATH_RESOURCE_PATH_SEPARATOR_STRING = String.valueOf(
CLASSPATH_RESOURCE_PATH_SEPARATOR);
private static final char PACKAGE_SEPARATOR_CHAR = '.';
private static final String PACKAGE_SEPARATOR_STRING = String.valueOf(PACKAGE_SEPARATOR_CHAR);

/**
* Malformed class name InternalError like reported in #401.
Expand Down Expand Up @@ -132,7 +132,7 @@ private List<Class<?>> findClassesForUris(List<URI> baseUris, String basePackage
private List<Class<?>> findClassesForUri(URI baseUri, String basePackageName, ClassFilter classFilter) {
List<Class<?>> classes = new ArrayList<>();
// @formatter:off
walkFilesForUri(baseUri, ClasspathFilters.classFiles(),
walkFilesForUri(baseUri, SearchPathUtils::isClassOrSourceFile,
(baseDir, file) ->
processClassFileSafely(baseDir, basePackageName, classFilter, file, classes::add));
// @formatter:on
Expand All @@ -156,7 +156,7 @@ private List<Resource> findResourcesForUris(List<URI> baseUris, String basePacka
private List<Resource> findResourcesForUri(URI baseUri, String basePackageName, ResourceFilter resourceFilter) {
List<Resource> resources = new ArrayList<>();
// @formatter:off
walkFilesForUri(baseUri, ClasspathFilters.resourceFiles(),
walkFilesForUri(baseUri, SearchPathUtils::isResourceFile,
(baseDir, file) ->
processResourceFileSafely(baseDir, basePackageName, resourceFilter, file, resources::add));
// @formatter:on
Expand All @@ -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,24 +253,14 @@ 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 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
subpackageName = subpackageName.substring(0, subpackageName.length() - pathSeparator.length());
}
return subpackageName;
return relativePath.toString().replace(pathSeparator, PACKAGE_SEPARATOR_STRING);
}

private void handleInternalError(Path classFile, String fullyQualifiedClassName, InternalError ex) {
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_START_PACKAGE_PREFIX = "org.junit.start.";

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_START_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 @@ -24,6 +24,7 @@
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
Expand Down Expand Up @@ -253,9 +254,10 @@ 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)
.filter(name -> !"module-info".equals(name))
return names.filter(name -> !name.endsWith("/")) // remove directories
.map(Path::of)
.filter(SearchPathUtils::isClassOrSourceFile)
.map(SearchPathUtils::determineFullyQualifiedClassName)
.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
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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.platform.commons.util;

import static java.util.stream.Collectors.joining;

import java.nio.file.Path;
import java.util.stream.IntStream;

import org.junit.platform.commons.JUnitException;

/**
* @since 1.11
*/
class SearchPathUtils {

static final char PACKAGE_SEPARATOR_CHAR = '.';
static final String PACKAGE_SEPARATOR_STRING = String.valueOf(PACKAGE_SEPARATOR_CHAR);
private static final char FILE_NAME_EXTENSION_SEPARATOR_CHAR = '.';

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

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

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

static boolean isResourceFile(Path file) {
return !isClassFile(file);
}

static boolean isClassOrSourceFile(Path file) {
var fileName = file.getFileName().toString();
return isClassOrSourceFile(fileName) && !isModuleInfoOrPackageInfo(fileName);
}

private static boolean isModuleInfoOrPackageInfo(String fileName) {
var fileNameWithoutExtension = removeExtension(fileName);
return PACKAGE_INFO_FILE_NAME.equals(fileNameWithoutExtension) //
|| MODULE_INFO_FILE_NAME.equals(fileNameWithoutExtension);
}

static String determineFullyQualifiedClassName(Path path) {
var simpleClassName = determineSimpleClassName(path);
var parent = path.getParent();
return parent == null ? simpleClassName : joinPathNamesWithPackageSeparator(parent.resolve(simpleClassName));
}

private static String joinPathNamesWithPackageSeparator(Path path) {
return IntStream.range(0, path.getNameCount()) //
.mapToObj(i -> path.getName(i).toString()) //
.collect(joining(PACKAGE_SEPARATOR_STRING));
}

static String determineSimpleClassName(Path file) {
return removeExtension(file.getFileName().toString());
}

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

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

private static boolean isClassFile(Path file) {
return file.getFileName().toString().endsWith(CLASS_FILE_SUFFIX);
}

private SearchPathUtils() {
}
}
Loading