Skip to content
Merged
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
3 changes: 3 additions & 0 deletions junit-source-launcher/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
out/

*.jar
38 changes: 38 additions & 0 deletions junit-source-launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# junit-source-launcher

Starting with Java 25 it is possible to write minimal source code test programs using the `org.junit.start` module.
For example, take a look at the [HelloTests.java](src/HelloTests.java) file reading:

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

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

@Test
void stringLength() {
Assertions.assertEquals(11, "Hello JUnit".length());
}
```

Download `org.junit.start` module and its transitively required modules into a local `lib/` directory by running in a shell:

```shell
java lib/DownloadRequiredModules.java
```

With all required modular JAR files available in a local `lib/` directory, the following Java command will discover and execute tests using the JUnit Platform.

```shell
java --module-path lib --add-modules org.junit.start src/HelloTests.java
```

It will also print the result tree to the console.

```text
└─ JUnit Jupiter ✔
└─ HelloTests ✔
└─ stringLength() ✔
```
118 changes: 118 additions & 0 deletions junit-source-launcher/lib/DownloadRequiredModules.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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
*/

final Path lib = Path.of("lib"); // local directory to be used in module path
final Set<String> roots = Set.of("org.junit.start"); // single root module to lookup
final String version = "6.1.0-M1"; // of JUnit Framework
final String repository = "https://repo.maven.apache.org/maven2"; // of JUnit Framework
final String lookup =
//language=Properties
"""
org.apiguardian.api=https://repo.maven.apache.org/maven2/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar
org.jspecify=https://repo.maven.apache.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar
org.junit.jupiter.api={{repository}}/org/junit/jupiter/junit-jupiter-api/{{version}}/junit-jupiter-api-{{version}}.jar
org.junit.jupiter.engine={{repository}}/org/junit/jupiter/junit-jupiter-engine/{{version}}/junit-jupiter-engine-{{version}}.jar
org.junit.jupiter.params={{repository}}/org/junit/jupiter/junit-jupiter-params/{{version}}/junit-jupiter-params-{{version}}.jar
org.junit.jupiter={{repository}}/org/junit/jupiter/junit-jupiter/{{version}}/junit-jupiter-{{version}}.jar
org.junit.platform.commons={{repository}}/org/junit/platform/junit-platform-commons/{{version}}/junit-platform-commons-{{version}}.jar
org.junit.platform.console={{repository}}/org/junit/platform/junit-platform-console/{{version}}/junit-platform-console-{{version}}.jar
org.junit.platform.engine={{repository}}/org/junit/platform/junit-platform-engine/{{version}}/junit-platform-engine-{{version}}.jar
org.junit.platform.launcher={{repository}}/org/junit/platform/junit-platform-launcher/{{version}}/junit-platform-launcher-{{version}}.jar
org.junit.platform.reporting={{repository}}/org/junit/platform/junit-platform-reporting/{{version}}/junit-platform-reporting-{{version}}.jar
org.junit.platform.suite.api={{repository}}/org/junit/platform/junit-platform-suite-api/{{version}}/junit-platform-suite-api-{{version}}.jar
org.junit.platform.suite.engine={{repository}}/org/junit/platform/junit-platform-suite-engine/{{version}}/junit-platform-suite-engine-{{version}}.jar
org.junit.platform.suite={{repository}}/org/junit/platform/junit-platform-suite/{{version}}/junit-platform-suite-{{version}}.jar
org.junit.start={{repository}}/org/junit/junit-start/{{version}}/junit-start-{{version}}.jar
org.opentest4j.reporting.tooling.spi=https://repo.maven.apache.org/maven2/org/opentest4j/reporting/open-test-reporting-tooling-spi/0.2.5/open-test-reporting-tooling-spi-0.2.5.jar
org.opentest4j=https://repo.maven.apache.org/maven2/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar
"""
.replace("{{repository}}", repository)
.replace("{{version}}", version);

void main() throws Exception {
// Ensure being launched inside expected working directory
var program = Path.of("src", "HelloTests.java");
if (!Files.exists(program)) {
throw new AssertionError("Expected %s in current working directory".formatted(program));
}

// Read mapping file to locate remote modules
var properties = new Properties();
properties.load(new StringReader(lookup));

// Create and initialize lib directory with root module(s)
Files.createDirectories(lib);
downloadModules(roots, properties);

// Compute missing modules and download them transitively
var missing = computeMissingModuleNames();
while (!missing.isEmpty()) {
downloadModules(missing, properties);
missing = computeMissingModuleNames();
}

IO.println("%nList modules of %s directory".formatted(lib));
listModules();
}

void downloadModules(Set<String> names, Properties properties) {
IO.println("Downloading %d module%s".formatted(names.size(), names.size() == 1 ? "" : "s"));
names.stream().parallel().forEach(name -> {
var target = lib.resolve(name + ".jar");
if (Files.exists(target)) return; // Don't overwrite existing JAR file
var source = URI.create(properties.getProperty(name));
try (var stream = source.toURL().openStream()) {
IO.println(name + " <- " + source + "...");
Files.copy(stream, target);
} catch (IOException cause) {
throw new UncheckedIOException(cause);
}
});
// Ensure that every name can be found to avoid eternal loops
var finder = ModuleFinder.of(lib);
var remainder = new TreeSet<>(names);
remainder.removeIf(name -> finder.find(name).isPresent());
if (remainder.isEmpty()) return;
throw new AssertionError("Modules not downloaded: " + remainder);
}

Set<String> computeMissingModuleNames() {
var system = ModuleFinder.ofSystem();
var finder = ModuleFinder.of(lib);
var names =
finder.findAll().stream()
.parallel()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::requires)
.flatMap(Collection::stream)
.filter(this::mustBePresentAtCompileTime)
.map(ModuleDescriptor.Requires::name)
.filter(name -> finder.find(name).isEmpty())
.filter(name -> system.find(name).isEmpty())
.toList();
return new TreeSet<>(names);
}

boolean mustBePresentAtCompileTime(ModuleDescriptor.Requires requires) {
var isStatic = requires.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC);
var isTransitive = requires.modifiers().contains(ModuleDescriptor.Requires.Modifier.TRANSITIVE);
return !isStatic || isTransitive;
}

void listModules() {
var finder = ModuleFinder.of(lib);
var modules = finder.findAll();
modules.stream()
.map(ModuleReference::descriptor)
.map(ModuleDescriptor::toNameAndVersion)
.sorted()
.forEach(IO::println);
IO.println(" %d modules".formatted(modules.size()));
}
20 changes: 20 additions & 0 deletions junit-source-launcher/src/HelloTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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
*/

import module org.junit.start;

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

@Test
void stringLength() {
Assertions.assertEquals(11, "Hello JUnit".length());
}
6 changes: 6 additions & 0 deletions junit-source-launcher/src/junit-platform.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
##
## Enable JUnit Platform Reporting
## -> https://docs.junit.org/current/user-guide/#junit-platform-reporting
##
# junit.platform.reporting.output.dir=out/junit-{uniqueNumber}
# junit.platform.reporting.open.xml.enabled=true
9 changes: 8 additions & 1 deletion src/Builder.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ int build(Target target, Set<String> excludedProjects) {
// modular
runProject(excludedProjects, "junit-modular-world", "java", modularAction);

// source launcher
runProject(excludedProjects, "junit-source-launcher", "java", "lib/DownloadRequiredModules.java");
runProject(excludedProjects, "junit-source-launcher",
"java",
"--module-path", "lib",
"--add-modules", "org.junit.start",
"src/HelloTests.java");
System.out.printf("%n%n%n|%n| Done. Build exits with status = %d.%n|%n", status);
return status;
}
Expand Down Expand Up @@ -141,7 +148,7 @@ void run(String directory, String executable, String... args) {
}

boolean isWindows() {
return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win");
return System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win");
}

void checkLicense(String blueprint, String... extensions) {
Expand Down
3 changes: 3 additions & 0 deletions src/StagingRepoInjector.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ private void inject() throws Exception {

appendAfter("junit-multiple-engines/build.gradle.kts", "mavenCentral()",
gradleKotlinDslSnippet);

replace("junit-source-launcher/lib/DownloadRequiredModules.java", "String repository = \"https://repo.maven.apache.org/maven\"",
"String repository = \"%s\"".formatted(stagingRepoUrl));
}

void appendAfter(String path, String token, String addedContent) throws IOException {
Expand Down
5 changes: 4 additions & 1 deletion src/Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import java.util.regex.Pattern;

/**
* Updates the versions of JUnit Platform artifacts in all example projects.
* Updates the versions of JUnit Framework artifacts in all example projects.
*/
@SuppressWarnings({"WeakerAccess", "SameParameterValue"})
class Updater {
Expand Down Expand Up @@ -72,6 +72,9 @@ void update() throws IOException {
update(Path.of("junit-multiple-engines/build.gradle.kts"), List.of(
Pattern.compile("junitBomVersion = \"" + VERSION_REGEX + '"')
));
update(Path.of("junit-source-launcher/lib/DownloadRequiredModules.java"), List.of(
Pattern.compile("final String version = \"" + VERSION_REGEX + '\"')
));
}

void update(Path path, List<Pattern> patterns) throws IOException {
Expand Down