From c2c53a3565d3c08f663d797e7f3102a0f95998ef Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 19 Aug 2025 13:42:09 +0200 Subject: [PATCH 01/26] Introduce experimental junit-aggregator module --- build.gradle.kts | 2 +- junit-aggregator/junit-aggregator.gradle.kts | 13 +++++ .../src/main/java/module-info.java | 14 ++++++ .../main/java/org/junit/aggregator/JUnit.java | 50 +++++++++++++++++++ settings.gradle.kts | 1 + 5 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 junit-aggregator/junit-aggregator.gradle.kts create mode 100644 junit-aggregator/src/main/java/module-info.java create mode 100644 junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java diff --git a/build.gradle.kts b/build.gradle.kts index ac2b1c43ec54..ab76d5d0d502 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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.junitAggregator)) + platformProjects + jupiterProjects + vintageProjects) val modularProjects by extra(mavenizedProjects - setOf(dependencyProject(projects.junitPlatformConsoleStandalone))) dependencies { diff --git a/junit-aggregator/junit-aggregator.gradle.kts b/junit-aggregator/junit-aggregator.gradle.kts new file mode 100644 index 000000000000..70e0bba81538 --- /dev/null +++ b/junit-aggregator/junit-aggregator.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("junitbuild.java-library-conventions") + id("junitbuild.java-nullability-conventions") +} + +description = "JUnit Aggregator" + +dependencies { + api(projects.junitJupiter) + compileOnlyApi(projects.junitJupiterEngine) + implementation(projects.junitPlatformLauncher) + implementation(projects.junitPlatformConsole) +} diff --git a/junit-aggregator/src/main/java/module-info.java b/junit-aggregator/src/main/java/module-info.java new file mode 100644 index 000000000000..5a744e387e92 --- /dev/null +++ b/junit-aggregator/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module org.junit.aggregator { + + 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.aggregator; + + uses java.util.spi.ToolProvider; + +} diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java new file mode 100644 index 000000000000..3173aae158ea --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -0,0 +1,50 @@ +package org.junit.aggregator; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.spi.ToolProvider; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +@API(status = EXPERIMENTAL, since = "6.0") +public final class JUnit { + + private JUnit() { + } + + public static void run(Object instance) { + if (instance instanceof Class testClass) { + run(testClass); + } else { + run(instance.getClass()); + } + } + + public static void run(Class testClass) { + var listener = new SummaryGeneratingListener(); + var request = request() // + .selectors(selectClass(testClass)) // + .forExecution() // + .listeners(listener) // + .build(); + LauncherFactory.create().execute(request); + var summary = listener.getSummary(); + if (!summary.getFailures().isEmpty()) { + summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); + throw new JUnitException("There are test failures!"); + } + } + + public static void main(String[] args) { + var junit = ToolProvider.findFirst("junit").orElseThrow(); + var exitCode = junit.run(System.out, System.err, args); + System.exit(exitCode); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 53dc8d7c4f78..17282b942d49 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -74,6 +74,7 @@ includeBuild("gradle/base") rootProject.name = "junit-framework" include("documentation") +include("junit-aggregator") include("junit-jupiter") include("junit-jupiter-api") include("junit-jupiter-engine") From 81b6142b0dc44e90ebf769bcf5f40853465906bc Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 08:13:41 +0200 Subject: [PATCH 02/26] Update aggregator module experiment --- junit-aggregator/junit-aggregator.gradle.kts | 29 +++++++++++--- .../src/main/java/module-info.java | 15 +++++++- .../main/java/org/junit/aggregator/JUnit.java | 38 +++++++++++++++---- .../org/junit/aggregator/package-info.java | 8 ++++ .../junit-aggregator.expected.txt | 10 +++++ .../platform/tooling/support/HelperTests.java | 1 + 6 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 junit-aggregator/src/main/java/org/junit/aggregator/package-info.java create mode 100644 platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt diff --git a/junit-aggregator/junit-aggregator.gradle.kts b/junit-aggregator/junit-aggregator.gradle.kts index 70e0bba81538..dc6a0a7c82ee 100644 --- a/junit-aggregator/junit-aggregator.gradle.kts +++ b/junit-aggregator/junit-aggregator.gradle.kts @@ -1,13 +1,30 @@ +import junitbuild.java.UpdateJarAction + plugins { - id("junitbuild.java-library-conventions") - id("junitbuild.java-nullability-conventions") + id("junitbuild.java-library-conventions") + id("junitbuild.java-nullability-conventions") } description = "JUnit Aggregator" dependencies { - api(projects.junitJupiter) - compileOnlyApi(projects.junitJupiterEngine) - implementation(projects.junitPlatformLauncher) - implementation(projects.junitPlatformConsole) + api(projects.junitJupiter) + compileOnlyApi(projects.junitJupiterEngine) + implementation(projects.junitPlatformLauncher) + implementation(projects.junitPlatformConsole) +} + +tasks { + jar { + manifest { + attributes("Main-Class" to "org.junit.aggregator.JUnit") + } + doLast(objects.newInstance(UpdateJarAction::class).apply { + javaLauncher = project.javaToolchains.launcherFor(java.toolchain) + args.addAll( + "--file", archiveFile.get().asFile.absolutePath, + "--main-class", "org.junit.aggregator.JUnit", + ) + }) + } } diff --git a/junit-aggregator/src/main/java/module-info.java b/junit-aggregator/src/main/java/module-info.java index 5a744e387e92..fd3a765e3c0b 100644 --- a/junit-aggregator/src/main/java/module-info.java +++ b/junit-aggregator/src/main/java/module-info.java @@ -1,10 +1,23 @@ +/* + * 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 + */ + +/** + * JUnit aggregator module for writing and running tests. + */ module org.junit.aggregator { requires static transitive org.apiguardian.api; requires static transitive org.jspecify; requires transitive org.junit.jupiter; - requires org.junit.platform.launcher; + requires transitive org.junit.platform.launcher; requires org.junit.platform.console; exports org.junit.aggregator; diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index 3173aae158ea..d66008d82009 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -1,15 +1,28 @@ +/* + * 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.aggregator; 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 java.util.function.UnaryOperator; import java.util.spi.ToolProvider; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.launcher.core.LauncherFactory; import org.junit.platform.launcher.listeners.SummaryGeneratingListener; @@ -18,24 +31,33 @@ public final class JUnit { private JUnit() { } - + public static void run(Object instance) { if (instance instanceof Class testClass) { run(testClass); - } else { - run(instance.getClass()); + return; } + if (instance instanceof Module testModule) { + run(testModule); + return; + } + run(instance.getClass()); } public static void run(Class testClass) { + run(discovery -> discovery.selectors(selectClass(testClass))); + } + + public static void run(Module testModule) { + run(discovery -> discovery.selectors(selectModule(testModule.getName()))); + } + + public static void run(UnaryOperator discovery) { var listener = new SummaryGeneratingListener(); - var request = request() // - .selectors(selectClass(testClass)) // - .forExecution() // - .listeners(listener) // - .build(); + var request = discovery.apply(request()).forExecution().listeners(listener).build(); LauncherFactory.create().execute(request); var summary = listener.getSummary(); + summary.printTo(new PrintWriter(System.out, true, Charset.defaultCharset())); if (!summary.getFailures().isEmpty()) { summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); throw new JUnitException("There are test failures!"); diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/package-info.java b/junit-aggregator/src/main/java/org/junit/aggregator/package-info.java new file mode 100644 index 000000000000..4ed064742bc3 --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/package-info.java @@ -0,0 +1,8 @@ +/** + * JUnit aggregator API for writing and running tests. + */ + +@NullMarked +package org.junit.aggregator; + +import org.jspecify.annotations.NullMarked; diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt new file mode 100644 index 000000000000..0299d722170b --- /dev/null +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt @@ -0,0 +1,10 @@ +org.junit.aggregator@${version} jar:file:.+/junit-aggregator-\d.+\.jar..module-info\.class +exports org.junit.aggregator +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 transitive +uses java.util.spi.ToolProvider +main-class org.junit.aggregator.JUnit diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java index d755f73cd5e8..4ef9bd08e15a 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java @@ -31,6 +31,7 @@ class HelperTests { @Test void loadModuleDirectoryNames() { assertLinesMatch(List.of( // + "junit-aggregator", // "junit-jupiter", // "junit-jupiter-api", // "junit-jupiter-engine", // From 46365655809be06763ae5ad40cf9a50c653d3c84 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 11:52:59 +0200 Subject: [PATCH 03/26] Use Jitpack's version --- .jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jitpack.yml b/.jitpack.yml index 4b239cdf1e36..4cf55153e2a9 100644 --- a/.jitpack.yml +++ b/.jitpack.yml @@ -3,4 +3,4 @@ before_install: - sdk install java 24-open - sdk use java 24-open install: - - ./gradlew --show-version javaToolchains publishToMavenLocal + - ./gradlew --show-version -Pversion=$VERSION javaToolchains publishToMavenLocal From e6a0ba7939f3cd998bcc01c02b224173da6d3f5c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 11:57:49 +0200 Subject: [PATCH 04/26] Disable build parameter validation --- gradle/plugins/build-parameters/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index f2db3d75afaa..c27c5d754cbe 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -9,6 +9,7 @@ tasks.compileJava { } buildParameters { + enableValidation = false pluginId("junitbuild.build-parameters") bool("ci") { description = "Whether or not this build is running in a CI environment" From 6722b2ed58091a1b5f919b10360fcb7596be5177 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 12:21:34 +0200 Subject: [PATCH 05/26] Disable signing on JitPack --- .jitpack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jitpack.yml b/.jitpack.yml index 4cf55153e2a9..fe316e04acc8 100644 --- a/.jitpack.yml +++ b/.jitpack.yml @@ -3,4 +3,4 @@ before_install: - sdk install java 24-open - sdk use java 24-open install: - - ./gradlew --show-version -Pversion=$VERSION javaToolchains publishToMavenLocal + - ./gradlew --show-version -Pversion=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal From 62e029e858a07d84508fcde5eca2047ccb028192 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 12:21:44 +0200 Subject: [PATCH 06/26] Disable module version on JitPack --- .../junitbuild.java-library-conventions.gradle.kts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 6b5be1377708..13bbcf37af53 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -220,9 +220,12 @@ tasks.withType().configureEach { } tasks.compileJava { - options.compilerArgs.addAll(listOf( - "--module-version", "${project.version}" - )) + if (version.toString().matches("\\d\\..+".toRegex())) { + // Only set module version if it adheres to the syntax rules + options.compilerArgs.addAll(listOf( + "--module-version", "${project.version}" + )) + } } configurations { From b4f285d12dced968f3d36cd3dca9a4a8ad5ac9c7 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 12:47:41 +0200 Subject: [PATCH 07/26] Adjust OSGi headers for Jitpack builds --- .../kotlin/junitbuild.java-library-conventions.gradle.kts | 2 +- .../src/main/kotlin/junitbuild.osgi-conventions.gradle.kts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 13bbcf37af53..fcef8bddec34 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -220,7 +220,7 @@ tasks.withType().configureEach { } tasks.compileJava { - if (version.toString().matches("\\d\\..+".toRegex())) { + if (providers.environmentVariable("JITPACK").orNull != "true") { // Only set module version if it adheres to the syntax rules options.compilerArgs.addAll(listOf( "--module-version", "${project.version}" diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts index 631c73b5faa9..bfd55bf31db6 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts @@ -16,6 +16,11 @@ tasks.withType().named { val importAPIGuardian by extra { "org.apiguardian.*;resolution:=\"optional\"" } val importJSpecify by extra { "org.jspecify.*;resolution:=\"optional\"" } + val exportAPIGuardian = if (providers.environmentVariable("JITPACK").orNull != "true") { + "*;version=${'$'}{versionmask;===;${'$'}{version_cleanup;${'$'}{task.archiveVersion}}}" + } else { + "*" + } extensions.create(BundleTaskExtension.NAME, this).apply { properties.set(projectDescription.map { @@ -62,7 +67,7 @@ tasks.withType().named { # Instruct the APIGuardianAnnotations how to operate. # See https://bnd.bndtools.org/instructions/export-apiguardian.html - -export-apiguardian: *;version=${'$'}{versionmask;===;${'$'}{version_cleanup;${'$'}{task.archiveVersion}}} + -export-apiguardian: $exportAPIGuardian # Avoid including java packages in Import-Package header to maximize compatibility with older OSGi runtimes. # See https://bnd.bndtools.org/instructions/noimportjava.html From cce1cae1d3e43c370fd02bd5aba5ba8db8fd00b6 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 13:12:08 +0200 Subject: [PATCH 08/26] Implement module-local select module workaround --- .../main/java/org/junit/aggregator/JUnit.java | 8 ++- .../org/junit/aggregator/ModuleSupport.java | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index d66008d82009..e1943c2f3064 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -12,7 +12,6 @@ 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; @@ -22,6 +21,7 @@ import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; import org.junit.platform.launcher.core.LauncherFactory; import org.junit.platform.launcher.listeners.SummaryGeneratingListener; @@ -49,7 +49,11 @@ public static void run(Class testClass) { } public static void run(Module testModule) { - run(discovery -> discovery.selectors(selectModule(testModule.getName()))); + // TODO run(discovery -> discovery.selectors(selectModule(testModule.getName()))); + // https://github.com/junit-team/junit-framework/issues/4852 + var selectors = ModuleSupport.listClassesInModule(testModule).stream().map( + DiscoverySelectors::selectClass).toList(); + run(discovery -> discovery.selectors(selectors)); } public static void run(UnaryOperator discovery) { diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java new file mode 100644 index 000000000000..1c9c87c906b3 --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -0,0 +1,50 @@ +/* + * 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.aggregator; + +import java.util.List; +import java.util.Optional; + +import org.junit.platform.commons.JUnitException; + +class ModuleSupport { + static List> listClassesInModule(Module module) { + var resolved = module.getLayer().configuration().findModule(module.getName()).orElseThrow(); + try (var reader = resolved.reference().open()) { + return reader.list() // + .map(ModuleSupport::loadClassByResourceName) // + .flatMap(Optional::stream) // + .distinct() // + .toList(); + } + catch (Exception exception) { + throw new JUnitException("Listing classes in module %s failed".formatted(module), exception); + } + } + + static Optional> loadClassByResourceName(String name) { + var className = name; + if (System.getProperty("jdk.launcher.sourcefile") != null) { + if (name.endsWith(".java")) { + className = name.substring(0, name.length() - 5).replace('/', '.'); + } + } + if (name.endsWith(".class")) { + className = name.substring(0, name.length() - 6).replace('/', '.'); + } + try { + return Optional.of(Class.forName(className)); + } + catch (Throwable ignored) { + return Optional.empty(); + } + } +} From 50366c3e54bf0a3904accc7473e83aa15ee6e9dc Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:20:31 +0200 Subject: [PATCH 09/26] Revert "Adjust OSGi headers for Jitpack builds" This reverts commit b4f285d12dced968f3d36cd3dca9a4a8ad5ac9c7. --- .../kotlin/junitbuild.java-library-conventions.gradle.kts | 2 +- .../src/main/kotlin/junitbuild.osgi-conventions.gradle.kts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index fcef8bddec34..13bbcf37af53 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -220,7 +220,7 @@ tasks.withType().configureEach { } tasks.compileJava { - if (providers.environmentVariable("JITPACK").orNull != "true") { + if (version.toString().matches("\\d\\..+".toRegex())) { // Only set module version if it adheres to the syntax rules options.compilerArgs.addAll(listOf( "--module-version", "${project.version}" diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts index bfd55bf31db6..631c73b5faa9 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.osgi-conventions.gradle.kts @@ -16,11 +16,6 @@ tasks.withType().named { val importAPIGuardian by extra { "org.apiguardian.*;resolution:=\"optional\"" } val importJSpecify by extra { "org.jspecify.*;resolution:=\"optional\"" } - val exportAPIGuardian = if (providers.environmentVariable("JITPACK").orNull != "true") { - "*;version=${'$'}{versionmask;===;${'$'}{version_cleanup;${'$'}{task.archiveVersion}}}" - } else { - "*" - } extensions.create(BundleTaskExtension.NAME, this).apply { properties.set(projectDescription.map { @@ -67,7 +62,7 @@ tasks.withType().named { # Instruct the APIGuardianAnnotations how to operate. # See https://bnd.bndtools.org/instructions/export-apiguardian.html - -export-apiguardian: $exportAPIGuardian + -export-apiguardian: *;version=${'$'}{versionmask;===;${'$'}{version_cleanup;${'$'}{task.archiveVersion}}} # Avoid including java packages in Import-Package header to maximize compatibility with older OSGi runtimes. # See https://bnd.bndtools.org/instructions/noimportjava.html From 38a080dd8eb4184b25ba46e74225323bb0c8190c Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:20:32 +0200 Subject: [PATCH 10/26] Revert "Disable module version on JitPack" This reverts commit 62e029e858a07d84508fcde5eca2047ccb028192. --- .../junitbuild.java-library-conventions.gradle.kts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 13bbcf37af53..6b5be1377708 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -220,12 +220,9 @@ tasks.withType().configureEach { } tasks.compileJava { - if (version.toString().matches("\\d\\..+".toRegex())) { - // Only set module version if it adheres to the syntax rules - options.compilerArgs.addAll(listOf( - "--module-version", "${project.version}" - )) - } + options.compilerArgs.addAll(listOf( + "--module-version", "${project.version}" + )) } configurations { From 0757829a82bc2c8dab31ce3bcf0f825fa0bcc8e0 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:20:33 +0200 Subject: [PATCH 11/26] Revert "Disable build parameter validation" This reverts commit e6a0ba7939f3cd998bcc01c02b224173da6d3f5c. --- gradle/plugins/build-parameters/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index c27c5d754cbe..f2db3d75afaa 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -9,7 +9,6 @@ tasks.compileJava { } buildParameters { - enableValidation = false pluginId("junitbuild.build-parameters") bool("ci") { description = "Whether or not this build is running in a CI environment" From 9ce5221d32a4a1b335e2f17da21000d015af6bc9 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:41:36 +0200 Subject: [PATCH 12/26] Introduce publishing.version property --- .jitpack.yml | 2 +- gradle/plugins/build-parameters/build.gradle.kts | 3 +++ .../kotlin/junitbuild.java-library-conventions.gradle.kts | 8 +++++--- .../kotlin/junitbuild.publishing-conventions.gradle.kts | 1 + 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.jitpack.yml b/.jitpack.yml index fe316e04acc8..a18babe165cc 100644 --- a/.jitpack.yml +++ b/.jitpack.yml @@ -3,4 +3,4 @@ before_install: - sdk install java 24-open - sdk use java 24-open install: - - ./gradlew --show-version -Pversion=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal + - ./gradlew --show-version -Ppublishing.version=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index f2db3d75afaa..4108923b17d0 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -93,6 +93,9 @@ buildParameters { bool("signArtifacts") { description = "Sign artifacts before publishing them to Maven repos" } + string("version") { + description = "The version to use for published Maven artifacts" + } } group("manifest") { string("buildTimestamp") { diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 6b5be1377708..99a945c2445e 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -113,9 +113,11 @@ if (project in mavenizedProjects) { publications { named("maven") { from(components["java"]) - versionMapping { - allVariants { - fromResolutionResult() + if (!buildParameters.publishing.version.isPresent) { + versionMapping { + allVariants { + fromResolutionResult() + } } } pom { diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts index 0783f037b2be..44b8c2d16043 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts @@ -33,6 +33,7 @@ tasks.withType().configureEach { publishing { publications { create("maven") { + version = buildParameters.publishing.version.getOrElse(project.version.toString()) pom { name.set(provider { project.description ?: "${project.group}:${project.name}" From d226ca941d7a4f33e5e7b9cd16b6f37127f66411 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:41:49 +0200 Subject: [PATCH 13/26] Make dependencies consistent with module descriptor --- junit-aggregator/junit-aggregator.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junit-aggregator/junit-aggregator.gradle.kts b/junit-aggregator/junit-aggregator.gradle.kts index dc6a0a7c82ee..56f593ea55ed 100644 --- a/junit-aggregator/junit-aggregator.gradle.kts +++ b/junit-aggregator/junit-aggregator.gradle.kts @@ -10,7 +10,7 @@ description = "JUnit Aggregator" dependencies { api(projects.junitJupiter) compileOnlyApi(projects.junitJupiterEngine) - implementation(projects.junitPlatformLauncher) + api(projects.junitPlatformLauncher) implementation(projects.junitPlatformConsole) } From dce66f8b3a6dd7a18111232c6895e8ac87ea1b31 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 13:42:11 +0200 Subject: [PATCH 14/26] Export platform dependency on BOM to support aligning dependencies --- junit-aggregator/junit-aggregator.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/junit-aggregator/junit-aggregator.gradle.kts b/junit-aggregator/junit-aggregator.gradle.kts index 56f593ea55ed..1bbec45e0ab2 100644 --- a/junit-aggregator/junit-aggregator.gradle.kts +++ b/junit-aggregator/junit-aggregator.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "JUnit Aggregator" dependencies { + api(platform(projects.junitBom)) api(projects.junitJupiter) compileOnlyApi(projects.junitJupiterEngine) api(projects.junitPlatformLauncher) From 6aabeea1f6e18c063fa1dc3c4c5725cc0fe0636a Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 14:04:15 +0200 Subject: [PATCH 15/26] Replace commit sha and number in JitPack version with `SNAPSHOT` --- .jitpack.yml | 2 +- gradle/plugins/build-parameters/build.gradle.kts | 4 +++- .../kotlin/junitbuild.java-library-conventions.gradle.kts | 2 +- .../main/kotlin/junitbuild.publishing-conventions.gradle.kts | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.jitpack.yml b/.jitpack.yml index a18babe165cc..56d421fa8cf3 100644 --- a/.jitpack.yml +++ b/.jitpack.yml @@ -3,4 +3,4 @@ before_install: - sdk install java 24-open - sdk use java 24-open install: - - ./gradlew --show-version -Ppublishing.version=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal + - ./gradlew --show-version -Pjitpack.version=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index 4108923b17d0..0f86f5d729c0 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -93,8 +93,10 @@ buildParameters { bool("signArtifacts") { description = "Sign artifacts before publishing them to Maven repos" } + } + group("jitpack") { string("version") { - description = "The version to use for published Maven artifacts" + description = "The version computed by Jitpack" } } group("manifest") { diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 99a945c2445e..3f1e846f0e5f 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -113,7 +113,7 @@ if (project in mavenizedProjects) { publications { named("maven") { from(components["java"]) - if (!buildParameters.publishing.version.isPresent) { + if (!buildParameters.jitpack.version.isPresent) { versionMapping { allVariants { fromResolutionResult() diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts index 44b8c2d16043..662bfdc3a4e8 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts @@ -33,7 +33,9 @@ tasks.withType().configureEach { publishing { publications { create("maven") { - version = buildParameters.publishing.version.getOrElse(project.version.toString()) + version = buildParameters.jitpack.version + .map { value -> "(.+)-[0-9a-f]+-\\d+".toRegex().matchEntire(value)!!.groupValues[1] + "-SNAPSHOT" } + .getOrElse(project.version.toString()) pom { name.set(provider { project.description ?: "${project.group}:${project.name}" From 5c2e952234e70c3e732c5ea62c37e51558a2bac1 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 14:27:01 +0200 Subject: [PATCH 16/26] Use JitPack's group and artifact id --- .jitpack.yml | 9 ++++++++- gradle/plugins/build-parameters/build.gradle.kts | 3 +++ .../junitbuild.publishing-conventions.gradle.kts | 13 +++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.jitpack.yml b/.jitpack.yml index 56d421fa8cf3..fd83c0c2efde 100644 --- a/.jitpack.yml +++ b/.jitpack.yml @@ -3,4 +3,11 @@ before_install: - sdk install java 24-open - sdk use java 24-open install: - - ./gradlew --show-version -Pjitpack.version=$VERSION -Ppublishing.signArtifacts=false javaToolchains publishToMavenLocal + - | + ./gradlew \ + --show-version \ + -Pjitpack.version=$VERSION \ + -Ppublishing.group=$GROUP.$ARTIFACT \ + -Ppublishing.signArtifacts=false \ + javaToolchains \ + publishToMavenLocal diff --git a/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index 0f86f5d729c0..8cdab340d4c3 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -93,6 +93,9 @@ buildParameters { bool("signArtifacts") { description = "Sign artifacts before publishing them to Maven repos" } + string("group") { + description = "Group ID for published Maven artifacts" + } } group("jitpack") { string("version") { diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts index 662bfdc3a4e8..5dc22b4e6bfb 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.publishing-conventions.gradle.kts @@ -11,12 +11,13 @@ val jupiterProjects: List by rootProject val platformProjects: List by rootProject val vintageProjects: List by rootProject -group = when (project) { - in jupiterProjects -> "org.junit.jupiter" - in platformProjects -> "org.junit.platform" - in vintageProjects -> "org.junit.vintage" - else -> "org.junit" -} +group = buildParameters.publishing.group + .getOrElse(when (project) { + in jupiterProjects -> "org.junit.jupiter" + in platformProjects -> "org.junit.platform" + in vintageProjects -> "org.junit.vintage" + else -> "org.junit" + }) val signArtifacts = buildParameters.publishing.signArtifacts.getOrElse(!(project.version.isSnapshot() || buildParameters.ci)) From 5867643f50b9cad0efb4150713db97d4cd631a9d Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Wed, 20 Aug 2025 14:39:10 +0200 Subject: [PATCH 17/26] Use JitPack version in BOM --- junit-bom/junit-bom.gradle.kts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/junit-bom/junit-bom.gradle.kts b/junit-bom/junit-bom.gradle.kts index 5e9107a5ac1f..1671c700f9bc 100644 --- a/junit-bom/junit-bom.gradle.kts +++ b/junit-bom/junit-bom.gradle.kts @@ -10,7 +10,12 @@ dependencies { val mavenizedProjects: List by rootProject.extra mavenizedProjects.sorted() .filter { it.name != "junit-platform-console-standalone" } - .forEach { api("${it.group}:${it.name}:${it.version}") } + .forEach { + val version = buildParameters.jitpack.version + .map { value -> "(.+)-[0-9a-f]+-\\d+".toRegex().matchEntire(value)!!.groupValues[1] + "-SNAPSHOT" } + .getOrElse(it.version.toString()) + api("${it.group}:${it.name}:${version}") + } } } From 9577a73fb5c274fff0d035dfbf9d1d2d57886e35 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 15:31:51 +0200 Subject: [PATCH 18/26] Use module-aware `Class::forName` variant --- .../src/main/java/org/junit/aggregator/ModuleSupport.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java index 1c9c87c906b3..c6736fc30d0c 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -20,7 +20,7 @@ static List> listClassesInModule(Module module) { var resolved = module.getLayer().configuration().findModule(module.getName()).orElseThrow(); try (var reader = resolved.reference().open()) { return reader.list() // - .map(ModuleSupport::loadClassByResourceName) // + .map(name -> loadClassByResourceName(module, name)) // .flatMap(Optional::stream) // .distinct() // .toList(); @@ -30,7 +30,7 @@ static List> listClassesInModule(Module module) { } } - static Optional> loadClassByResourceName(String name) { + static Optional> loadClassByResourceName(Module module, String name) { var className = name; if (System.getProperty("jdk.launcher.sourcefile") != null) { if (name.endsWith(".java")) { @@ -41,7 +41,7 @@ static Optional> loadClassByResourceName(String name) { className = name.substring(0, name.length() - 6).replace('/', '.'); } try { - return Optional.of(Class.forName(className)); + return Optional.of(Class.forName(module, className)); } catch (Throwable ignored) { return Optional.empty(); From 7a6349da06830bbca13f8d0c07b0215253b6187c Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 15:39:34 +0200 Subject: [PATCH 19/26] Extract constant flag for launcher source mode --- .../src/main/java/org/junit/aggregator/ModuleSupport.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java index c6736fc30d0c..f0be38032ddf 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -16,6 +16,8 @@ import org.junit.platform.commons.JUnitException; class ModuleSupport { + private static final boolean SOURCE_MODE = System.getProperty("jdk.launcher.sourcefile") != null; + static List> listClassesInModule(Module module) { var resolved = module.getLayer().configuration().findModule(module.getName()).orElseThrow(); try (var reader = resolved.reference().open()) { @@ -32,7 +34,7 @@ static List> listClassesInModule(Module module) { static Optional> loadClassByResourceName(Module module, String name) { var className = name; - if (System.getProperty("jdk.launcher.sourcefile") != null) { + if (SOURCE_MODE) { if (name.endsWith(".java")) { className = name.substring(0, name.length() - 5).replace('/', '.'); } From 20a59a7c235fb10615c9171d67c3cc0420ecb653 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 16:04:29 +0200 Subject: [PATCH 20/26] Use custom container feed printing listener --- .../ContainerFeedPrintingListener.java | 95 +++++++++++++++++++ .../main/java/org/junit/aggregator/JUnit.java | 29 +++--- 2 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java b/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java new file mode 100644 index 000000000000..6954e9fe98e2 --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java @@ -0,0 +1,95 @@ +/* + * 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.aggregator; + +import java.util.Map; +import java.util.StringJoiner; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; + +class ContainerFeedPrintingListener implements TestExecutionListener { + + private record Summary(int size, int successful, int aborted, int failed, int skipped) { + Summary with(Summary other) { + return new Summary(size + other.size, successful + other.successful, aborted + other.aborted, + failed + other.failed, skipped + other.skipped); + } + } + + ContainerFeedPrintingListener() { + } + + private final Map summaries = new ConcurrentHashMap<>(); + + @Override + public void testPlanExecutionStarted(TestPlan testPlan) { + printCaptions(); // as header + summaries.put("/", new Summary(0, 0, 0, 0, 0)); + } + + @Override + public void testPlanExecutionFinished(TestPlan testPlan) { + if (summaries.size() > 42) { + printCaptions(); // as footer + } + } + + @Override + public void executionFinished(TestIdentifier identifier, TestExecutionResult result) { + if (identifier.isTest()) { + var status = result.getStatus(); + var summary = new Summary(1, status == TestExecutionResult.Status.SUCCESSFUL ? 1 : 0, + status == TestExecutionResult.Status.ABORTED ? 1 : 0, + status == TestExecutionResult.Status.FAILED ? 1 : 0, 0); + merge(identifier, summary); + } + if (identifier.isContainer()) { + var summary = summaries.get(identifier.getUniqueId()); + if (summary == null) + return; + var indent = " ".repeat(identifier.getUniqueIdObject().getSegments().size()); + var name = identifier.getDisplayName(); + print(summary, indent + name); + } + } + + @Override + public void executionSkipped(TestIdentifier identifier, String reason) { + if (identifier.isTest()) { + var summary = new Summary(1, 0, 0, 0, 1); + merge(identifier, summary); + } + } + + private void merge(TestIdentifier identifier, Summary summary) { + var joiner = new StringJoiner("/"); + var segments = identifier.getUniqueIdObject().getSegments(); + for (var segment : segments) { + joiner.add('[' + segment.getType() + ':' + segment.getValue() + ']'); + summaries.merge(joiner.toString(), summary, Summary::with); + } + } + + private void printCaptions() { + var format = "%5s %5s %5s %5s %5s %s%n"; + System.out.printf(format, "Found", "OK", "[A]", "[F]", "[S]", "Display Name"); + } + + private void print(Summary summary, String name) { + var format = "%5s %5s %5s %5s %5s %s%n"; + System.out.printf(format, summary.size, summary.successful, summary.aborted, summary.failed, summary.skipped, + name); + } +} diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index e1943c2f3064..ab882bdcfa9b 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -49,23 +49,30 @@ public static void run(Class testClass) { } public static void run(Module testModule) { - // TODO run(discovery -> discovery.selectors(selectModule(testModule.getName()))); - // https://github.com/junit-team/junit-framework/issues/4852 - var selectors = ModuleSupport.listClassesInModule(testModule).stream().map( - DiscoverySelectors::selectClass).toList(); + // TODO run(discovery -> discovery.selectors(selectModule(testModule))); + // https://github.com/junit-team/junit-framework/issues/4852 + var selectors = ModuleSupport.listClassesInModule(testModule).stream() // + .map(DiscoverySelectors::selectClass).toList(); run(discovery -> discovery.selectors(selectors)); } public static void run(UnaryOperator discovery) { var listener = new SummaryGeneratingListener(); - var request = discovery.apply(request()).forExecution().listeners(listener).build(); - LauncherFactory.create().execute(request); + var request = discovery.apply(request()).forExecution() // + .listeners(listener, new ContainerFeedPrintingListener()) // + .build(); + var launcher = LauncherFactory.create(); + launcher.execute(request); var summary = listener.getSummary(); - summary.printTo(new PrintWriter(System.out, true, Charset.defaultCharset())); - if (!summary.getFailures().isEmpty()) { - summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); - throw new JUnitException("There are test failures!"); - } + + if (summary.getTotalFailureCount() == 0) + return; + + summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); + var message = "JUnit run finished with %d failure%s".formatted( // + summary.getTotalFailureCount(), // + summary.getTotalFailureCount() == 1 ? "" : "s"); + throw new JUnitException(message); } public static void main(String[] args) { From eda0829f5cd4790b1b7e83294caf530a3dd4e248 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Wed, 20 Aug 2025 16:28:40 +0200 Subject: [PATCH 21/26] Fix inspection warnings --- .../junit/aggregator/ContainerFeedPrintingListener.java | 9 ++++----- .../src/main/java/org/junit/aggregator/JUnit.java | 5 ++--- .../main/java/org/junit/aggregator/ModuleSupport.java | 5 ++++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java b/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java index 6954e9fe98e2..7095e571f8f4 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java @@ -83,13 +83,12 @@ private void merge(TestIdentifier identifier, Summary summary) { } private void printCaptions() { - var format = "%5s %5s %5s %5s %5s %s%n"; - System.out.printf(format, "Found", "OK", "[A]", "[F]", "[S]", "Display Name"); + System.out.printf("%5s %5s %5s %5s %5s %s%n", // + "Found", "OK", "[A]", "[F]", "[S]", "Display Name"); } private void print(Summary summary, String name) { - var format = "%5s %5s %5s %5s %5s %s%n"; - System.out.printf(format, summary.size, summary.successful, summary.aborted, summary.failed, summary.skipped, - name); + System.out.printf("%5s %5s %5s %5s %5s %s%n", // + summary.size, summary.successful, summary.aborted, summary.failed, summary.skipped, name); } } diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index ab882bdcfa9b..91266ab12e0a 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -69,10 +69,9 @@ public static void run(UnaryOperator discovery) return; summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); - var message = "JUnit run finished with %d failure%s".formatted( // + throw new JUnitException("JUnit run finished with %d failure%s".formatted( // summary.getTotalFailureCount(), // - summary.getTotalFailureCount() == 1 ? "" : "s"); - throw new JUnitException(message); + summary.getTotalFailureCount() == 1 ? "" : "s")); } public static void main(String[] args) { diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java index f0be38032ddf..a49c2dad20cf 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -16,7 +16,10 @@ import org.junit.platform.commons.JUnitException; class ModuleSupport { - private static final boolean SOURCE_MODE = System.getProperty("jdk.launcher.sourcefile") != null; + private ModuleSupport() { + } + + static final boolean SOURCE_MODE = System.getProperty("jdk.launcher.sourcefile") != null; static List> listClassesInModule(Module module) { var resolved = module.getLayer().configuration().findModule(module.getName()).orElseThrow(); From 57802c79e9444aa71bb69133f1a0208a34fb673c Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Thu, 21 Aug 2025 12:03:37 +0200 Subject: [PATCH 22/26] Add initial `Module` instance support in `ModuleSelector` (#4852) --- .../platform/commons/util/ModuleUtils.java | 20 +++++++++++++++++++ .../commons/util/ReflectionUtils.java | 7 +++++++ .../engine/discovery/DiscoverySelectors.java | 16 +++++++++++++++ .../engine/discovery/ModuleSelector.java | 16 +++++++++++++++ .../ResourceContainerSelectorResolver.java | 4 ++++ .../discovery/DiscoverySelectorsTests.java | 13 +++++++++++- 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java index 5c5a1cc9cf32..209697833821 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java @@ -133,6 +133,26 @@ public static List findAllResourcesInModule(String moduleName, Predica return scan(moduleReferences, filter, ModuleUtils.class.getClassLoader()); } + /** + * Find all {@linkplain Resource resources} for the given module. + * + * @param module the module to scan; never {@code null} or empty + * @param filter the class filter to apply; never {@code null} + * @return an immutable list of all such resources found; never {@code null} + * but potentially empty + * @since 6.0 + */ + @API(status = INTERNAL, since = "6.0") + public static List findAllResourcesInModule(Module module, Predicate filter) { + Preconditions.notNull(module, "Module name must not be null"); + Preconditions.notNull(filter, "Resource filter must not be null"); + + String name = module.getName(); + logger.debug(() -> "Looking for classes in module: " + name); + var reference = module.getLayer().configuration().findModule(name).orElseThrow().reference(); + return scan(Set.of(reference), filter, module.getClassLoader()); + } + /** * Stream resolved modules from current (or boot) module layer. */ diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index d84d8c79ad21..7b4d2b0501bc 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -1086,6 +1086,13 @@ public static List findAllResourcesInModule(String moduleName, Predica return List.copyOf(ModuleUtils.findAllResourcesInModule(moduleName, resourceFilter)); } + /** + * @since 6.0 + */ + public static List findAllResourcesInModule(Module module, Predicate resourceFilter) { + return List.copyOf(ModuleUtils.findAllResourcesInModule(module, resourceFilter)); + } + /** * @since 1.10 */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java index 3cdb8dbf71b3..1b8fb28f4a99 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java @@ -373,6 +373,22 @@ public static ModuleSelector selectModule(String moduleName) { return new ModuleSelector(moduleName.strip()); } + /** + * Create a {@code ModuleSelector} for the supplied module. + * + *

The unnamed module is not supported. + * + * @param module the module to select; never {@code null} or blank + * @since 6.0 + * @see ModuleSelector + */ + @API(status = STABLE, since = "6.0") + public static ModuleSelector selectModule(Module module) { + Preconditions.notNull(module, "Module must not be null"); + Preconditions.condition(module.isNamed(), "Module must be named"); + return new ModuleSelector(module); + } + /** * Create a list of {@code ModuleSelectors} for the supplied module names. * diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java index 1ecd5d7de8fa..942d2f9a7943 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java @@ -17,6 +17,7 @@ import java.util.Optional; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.DiscoverySelectorIdentifier; @@ -33,12 +34,27 @@ @API(status = STABLE, since = "1.1") public final class ModuleSelector implements DiscoverySelector { + @Nullable + private final Module module; private final String moduleName; + ModuleSelector(Module module) { + this.module = module; + this.moduleName = module.getName(); + } + ModuleSelector(String moduleName) { + this.module = null; this.moduleName = moduleName; } + /** + * Get the selected module wrapped in an Optional. + */ + public Optional getModule() { + return Optional.ofNullable(module); + } + /** * Get the selected module name. */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolver.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolver.java index 34beef727045..443443bef611 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolver.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolver.java @@ -48,6 +48,10 @@ public Resolution resolve(ClasspathRootSelector selector, Context context) { @Override public Resolution resolve(ModuleSelector selector, Context context) { + if (selector.getModule().isPresent()) { + Module module = selector.getModule().get(); + return resourceSelectors(findAllResourcesInModule(module, resourceFilter)); + } return resourceSelectors(findAllResourcesInModule(selector.getModuleName(), resourceFilter)); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java index 0b8dd823168a..7f97ca5710c2 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java @@ -16,7 +16,9 @@ import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; @@ -421,6 +423,15 @@ class SelectModuleTests { void selectModuleByName() { var selector = selectModule("java.base"); assertEquals("java.base", selector.getModuleName()); + assertTrue(selector.getModule().isEmpty()); + } + + @Test + void selectModuleByInstance() { + var base = Object.class.getModule(); + var selector = selectModule(base); + assertSame(base, selector.getModule().orElseThrow()); + assertEquals("java.base", selector.getModuleName()); } @Test @@ -435,7 +446,7 @@ void parseModuleByName() { @SuppressWarnings("DataFlowIssue") @Test void selectModuleByNamePreconditions() { - assertViolatesPrecondition(() -> selectModule(null)); + assertViolatesPrecondition(() -> selectModule((String) null)); assertViolatesPrecondition(() -> selectModule("")); assertViolatesPrecondition(() -> selectModule(" ")); } From 89a7ec097c23888a40158e9570abb73261127bd0 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Thu, 21 Aug 2025 13:13:48 +0200 Subject: [PATCH 23/26] Support `Module` instances class containers, too --- .../commons/support/ReflectionSupport.java | 25 ++++++++++ .../platform/commons/util/ModuleUtils.java | 47 ++++++++++++++++--- .../commons/util/ReflectionUtils.java | 17 +++++++ .../ClassContainerSelectorResolver.java | 4 ++ .../support/ReflectionSupportTests.java | 16 ++++++- 5 files changed, 101 insertions(+), 8 deletions(-) diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java index 310f70192185..97bd13657580 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java @@ -355,6 +355,31 @@ public static List> findAllClassesInModule(String moduleName, Predicate return ReflectionUtils.findAllClassesInModule(moduleName, classFilter, classNameFilter); } + /** + * Find all {@linkplain Class classes} in the supplied {@code moduleName} + * that match the specified {@code classFilter} and {@code classNameFilter} + * predicates. + * + *

The module-path scanning algorithm searches recursively in all + * packages contained in the module. + * + * @param module the name of the module to scan; never {@code null} or + * empty + * @param classFilter the class type filter; never {@code null} + * @param classNameFilter the class name filter; never {@code null} + * @return an immutable list of all such classes found; never {@code null} + * but potentially empty + * @since 1.1.1 + * @see #findAllClassesInClasspathRoot(URI, Predicate, Predicate) + * @see #findAllClassesInPackage(String, Predicate, Predicate) + */ + @API(status = MAINTAINED, since = "6.0") + public static List> findAllClassesInModule(Module module, Predicate> classFilter, + Predicate classNameFilter) { + + return ReflectionUtils.findAllClassesInModule(module, classFilter, classNameFilter); + } + /** * Find all {@linkplain Resource resources} in the supplied {@code moduleName} * that match the specified {@code resourceFilter} predicate. diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java index 209697833821..84b1fc580b67 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java @@ -27,12 +27,14 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; import org.apiguardian.api.API; +import org.jspecify.annotations.Nullable; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; @@ -109,6 +111,24 @@ public static List> findAllClassesInModule(String moduleName, ClassFilt return scan(moduleReferences, filter, ModuleUtils.class.getClassLoader()); } + /** + * Find all {@linkplain Class classes} for the given module. + * + * @param module the module to scan; never {@code null} or empty + * @param filter the class filter to apply; never {@code null} + * @return an immutable list of all such classes found; never {@code null} + * but potentially empty + */ + public static List> findAllClassesInModule(Module module, ClassFilter filter) { + Preconditions.notNull(module, "Module must not be null"); + Preconditions.notNull(filter, "Class filter must not be null"); + + String name = module.getName(); + logger.debug(() -> "Looking for classes in module: " + name); + var reference = module.getLayer().configuration().findModule(name).orElseThrow().reference(); + return scan(Set.of(reference), filter, module.getClassLoader()); + } + /** * Find all {@linkplain Resource resources} for the given module name. * @@ -217,6 +237,11 @@ private static List scan(Set references, Predicate> scan(ModuleReference reference) { try (ModuleReader reader = reference.open()) { try (Stream names = reader.list()) { // @formatter:off - return names.filter(name -> name.endsWith(".class")) - .map(this::className) + return names.map(this::convertToClassNameOrNull) + .filter(Objects::nonNull) .filter(name -> !"module-info".equals(name)) + .map(name -> name.replace('/', '.')) .filter(classFilter::match) .> map(this::loadClassUnchecked) .filter(classFilter::match) @@ -249,11 +275,20 @@ List> scan(ModuleReference reference) { /** * Convert resource name to binary class name. + *

+ * Always converts names ending with {@code .class} and in source mode, + * this method also converts names ending with {@code .java}. + * + * @return the converted name or {@code null} */ - private String className(String resourceName) { - resourceName = resourceName.substring(0, resourceName.length() - 6); // 6 = ".class".length() - resourceName = resourceName.replace('/', '.'); - return resourceName; + private @Nullable String convertToClassNameOrNull(String resourceName) { + if (resourceName.endsWith(".class")) { + return resourceName.substring(0, resourceName.length() - 6); // 6 = ".class".length() + } + if (SOURCE_MODE && resourceName.endsWith(".java")) { + return resourceName.substring(0, resourceName.length() - 5); // 5 = ".java".length() + } + return null; } /** diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index 7b4d2b0501bc..c26e96eeb66b 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -1063,6 +1063,16 @@ public static List> findAllClassesInModule(String moduleName, Predicate return findAllClassesInModule(moduleName, ClassFilter.of(classNameFilter, classFilter)); } + /** + * @since 6.0 + * @see org.junit.platform.commons.support.ReflectionSupport#findAllClassesInModule(Module, Predicate, Predicate) + */ + public static List> findAllClassesInModule(Module module, Predicate> classFilter, + Predicate classNameFilter) { + // unmodifiable since returned by public, non-internal method(s) + return findAllClassesInModule(module, ClassFilter.of(classNameFilter, classFilter)); + } + /** * @since 1.10 * @see org.junit.platform.commons.support.ReflectionSupport#streamAllClassesInModule(String, Predicate, Predicate) @@ -1079,6 +1089,13 @@ public static List> findAllClassesInModule(String moduleName, ClassFilt return List.copyOf(ModuleUtils.findAllClassesInModule(moduleName, classFilter)); } + /** + * @since 6.0 + */ + public static List> findAllClassesInModule(Module module, ClassFilter classFilter) { + return List.copyOf(ModuleUtils.findAllClassesInModule(module, classFilter)); + } + /** * @since 1.11 */ diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java index 4dfae26276f5..988ceafa7f6d 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ClassContainerSelectorResolver.java @@ -46,6 +46,10 @@ public Resolution resolve(ClasspathRootSelector selector, Context context) { @Override public Resolution resolve(ModuleSelector selector, Context context) { + if (selector.getModule().isPresent()) { + Module module = selector.getModule().get(); + return classSelectors(findAllClassesInModule(module, classFilter, classNameFilter)); + } return classSelectors(findAllClassesInModule(selector.getModuleName(), classFilter, classNameFilter)); } diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/ReflectionSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/ReflectionSupportTests.java index 5ad6b628b6eb..72703194b607 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/ReflectionSupportTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/ReflectionSupportTests.java @@ -281,9 +281,9 @@ void findAllClassesInModuleDelegates() { @SuppressWarnings("DataFlowIssue") @Test - void findAllClassesInModulePreconditions() { + void findAllClassesInModuleByNamePreconditions() { var exception = assertThrows(PreconditionViolationException.class, - () -> ReflectionSupport.findAllClassesInModule(null, allTypes, allNames)); + () -> ReflectionSupport.findAllClassesInModule((String) null, allTypes, allNames)); assertEquals("Module name must not be null or empty", exception.getMessage()); assertPreconditionViolationException("class predicate", () -> ReflectionSupport.findAllClassesInModule("org.junit.platform.commons", null, allNames)); @@ -291,6 +291,18 @@ void findAllClassesInModulePreconditions() { () -> ReflectionSupport.findAllClassesInModule("org.junit.platform.commons", allTypes, null)); } + @SuppressWarnings("DataFlowIssue") + @Test + void findAllClassesInModuleByInstancePreconditions() { + var exception = assertThrows(PreconditionViolationException.class, + () -> ReflectionSupport.findAllClassesInModule((Module) null, allTypes, allNames)); + assertEquals("Module must not be null", exception.getMessage()); + assertPreconditionViolationException("class predicate", + () -> ReflectionSupport.findAllClassesInModule("org.junit.platform.commons", null, allNames)); + assertPreconditionViolationException("name predicate", + () -> ReflectionSupport.findAllClassesInModule("org.junit.platform.commons", allTypes, null)); + } + /** * @since 1.11 */ From 6ca34545f41bbdf2df4470852b712337ae2f3307 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Thu, 21 Aug 2025 16:54:39 +0200 Subject: [PATCH 24/26] Offer both module selection modes --- .../main/java/org/junit/aggregator/JUnit.java | 22 ++++++++++++++----- .../org/junit/aggregator/ModuleSupport.java | 6 ++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index 91266ab12e0a..457cad825a4e 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -12,6 +12,7 @@ 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; @@ -48,12 +49,23 @@ public static void run(Class testClass) { run(discovery -> discovery.selectors(selectClass(testClass))); } + public enum ModuleSelectionMode { + PLATFORM_DEFAULT, AGGREGATOR_LOCAL + } + public static void run(Module testModule) { - // TODO run(discovery -> discovery.selectors(selectModule(testModule))); - // https://github.com/junit-team/junit-framework/issues/4852 - var selectors = ModuleSupport.listClassesInModule(testModule).stream() // - .map(DiscoverySelectors::selectClass).toList(); - run(discovery -> discovery.selectors(selectors)); + run(testModule, ModuleSelectionMode.PLATFORM_DEFAULT); + } + + public static void run(Module testModule, ModuleSelectionMode mode) { + switch (mode) { + case PLATFORM_DEFAULT -> run(discovery -> discovery.selectors(selectModule(testModule))); + case AGGREGATOR_LOCAL -> { + var selectors = ModuleSupport.listClassesInModule(testModule).stream() // + .map(DiscoverySelectors::selectClass).toList(); + run(discovery -> discovery.selectors(selectors)); + } + } } public static void run(UnaryOperator discovery) { diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java index a49c2dad20cf..2d179e5603ba 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -39,14 +39,14 @@ static Optional> loadClassByResourceName(Module module, String name) { var className = name; if (SOURCE_MODE) { if (name.endsWith(".java")) { - className = name.substring(0, name.length() - 5).replace('/', '.'); + className = name.substring(0, name.length() - 5); } } if (name.endsWith(".class")) { - className = name.substring(0, name.length() - 6).replace('/', '.'); + className = name.substring(0, name.length() - 6); } try { - return Optional.of(Class.forName(module, className)); + return Optional.of(Class.forName(module, className.replace('/', '.'))); } catch (Throwable ignored) { return Optional.empty(); From 95afa4fdf49951f0c97c3f82961f08bb8c27eecb Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Thu, 28 Aug 2025 11:43:33 +0200 Subject: [PATCH 25/26] Hide the launcher module from the public API avoiding compilation errors ``` reference to MethodSource is ambiguous @MethodSource ^ both class org.junit.jupiter.params.provider.MethodSource in org.junit.jupiter.params.provider and class org.junit.platform.engine.support.descriptor.MethodSource in org.junit.platform.engine.support.descriptor match ``` --- junit-aggregator/src/main/java/module-info.java | 2 +- junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/junit-aggregator/src/main/java/module-info.java b/junit-aggregator/src/main/java/module-info.java index fd3a765e3c0b..0b5e5846e541 100644 --- a/junit-aggregator/src/main/java/module-info.java +++ b/junit-aggregator/src/main/java/module-info.java @@ -17,7 +17,7 @@ requires static transitive org.jspecify; requires transitive org.junit.jupiter; - requires transitive org.junit.platform.launcher; + requires org.junit.platform.launcher; requires org.junit.platform.console; exports org.junit.aggregator; diff --git a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java index 457cad825a4e..2f7a685dd037 100644 --- a/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -68,7 +68,8 @@ public static void run(Module testModule, ModuleSelectionMode mode) { } } - public static void run(UnaryOperator discovery) { + // Don't transitively expose types from org.junit.platform.launcher module + private static void run(UnaryOperator discovery) { var listener = new SummaryGeneratingListener(); var request = discovery.apply(request()).forExecution() // .listeners(listener, new ContainerFeedPrintingListener()) // From d2369ff11e3e6e2e28dd5dbddab1a2c7f0df2ab3 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Mon, 1 Sep 2025 16:43:24 +0200 Subject: [PATCH 26/26] Update expected aggregator module description --- .../projects/jar-describe-module/junit-aggregator.expected.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt index 0299d722170b..6a21ece099fe 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-aggregator.expected.txt @@ -5,6 +5,6 @@ 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 transitive +requires org.junit.platform.launcher uses java.util.spi.ToolProvider main-class org.junit.aggregator.JUnit