Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
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
4 changes: 4 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 All @@ -247,6 +249,8 @@ endif::[]
:Checkstyle: https://checkstyle.sourceforge.io[Checkstyle]
:DiscussionsQA: https://github.com/junit-team/junit-framework/discussions/categories/q-a[Q&A category on GitHub Discussions]
:Hamcrest: https://hamcrest.org/JavaHamcrest/[Hamcrest]
:JEP511: https://openjdk.org/jeps/511[JEP 511]
:JEP512: https://openjdk.org/jeps/512[JEP 512]
:Jimfs: https://google.github.io/jimfs/[Jimfs]
:Log4j: https://logging.apache.org/log4j/2.x/[Log4j]
:Log4j_JDK_Logging_Adapter: https://logging.apache.org/log4j/2.x/log4j-jul/index.html[Log4j JDK Logging Adapter]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ 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
usages of JUnit in compact source files together with a single module import statement:
[source,java,indent=0]
----
import module org.junit.start;

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

@Test
void test() { Assertions.assertEquals(2, 1 + 1); }
----
* 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
34 changes: 34 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,40 @@ DYNAMIC = 35
REPORTED = 37
----

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

Java 25 introduced Module Import Declarations with {JEP511} and Compact Source Files
and Instance Methods with {JEP512}. The `org.junit.start` module is JUnit's "On-Ramp"
enabler, allowing to write minimal source code programs. 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
Loading