diff --git a/.jitpack.yml b/.jitpack.yml index 4b239cdf1e36..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 javaToolchains publishToMavenLocal + - | + ./gradlew \ + --show-version \ + -Pjitpack.version=$VERSION \ + -Ppublishing.group=$GROUP.$ARTIFACT \ + -Ppublishing.signArtifacts=false \ + javaToolchains \ + publishToMavenLocal 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/gradle/plugins/build-parameters/build.gradle.kts b/gradle/plugins/build-parameters/build.gradle.kts index f2db3d75afaa..8cdab340d4c3 100644 --- a/gradle/plugins/build-parameters/build.gradle.kts +++ b/gradle/plugins/build-parameters/build.gradle.kts @@ -93,6 +93,14 @@ 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") { + description = "The version computed by Jitpack" + } } 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..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,9 +113,11 @@ if (project in mavenizedProjects) { publications { named("maven") { from(components["java"]) - versionMapping { - allVariants { - fromResolutionResult() + if (!buildParameters.jitpack.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..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)) @@ -33,6 +34,9 @@ tasks.withType().configureEach { publishing { publications { create("maven") { + 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}" diff --git a/junit-aggregator/junit-aggregator.gradle.kts b/junit-aggregator/junit-aggregator.gradle.kts new file mode 100644 index 000000000000..1bbec45e0ab2 --- /dev/null +++ b/junit-aggregator/junit-aggregator.gradle.kts @@ -0,0 +1,31 @@ +import junitbuild.java.UpdateJarAction + +plugins { + id("junitbuild.java-library-conventions") + id("junitbuild.java-nullability-conventions") +} + +description = "JUnit Aggregator" + +dependencies { + api(platform(projects.junitBom)) + api(projects.junitJupiter) + compileOnlyApi(projects.junitJupiterEngine) + api(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 new file mode 100644 index 000000000000..0b5e5846e541 --- /dev/null +++ b/junit-aggregator/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * 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 org.junit.platform.console; + + exports org.junit.aggregator; + + uses java.util.spi.ToolProvider; + +} 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..7095e571f8f4 --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ContainerFeedPrintingListener.java @@ -0,0 +1,94 @@ +/* + * 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() { + System.out.printf("%5s %5s %5s %5s %5s %s%n", // + "Found", "OK", "[A]", "[F]", "[S]", "Display Name"); + } + + private void print(Summary summary, String 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 new file mode 100644 index 000000000000..fe4a936e2bc4 --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/JUnit.java @@ -0,0 +1,96 @@ +/* + * 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.jupiter.api.Assertions; +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; + +@API(status = EXPERIMENTAL, since = "6.0") +public final class JUnit extends Assertions { + + private JUnit() { + } + + public static void run(Object instance) { + if (instance instanceof Class testClass) { + run(testClass); + 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 enum ModuleSelectionMode { + PLATFORM_DEFAULT, AGGREGATOR_LOCAL + } + + public static void run(Module testModule) { + 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)); + } + } + } + + // 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()) // + .build(); + var launcher = LauncherFactory.create(); + launcher.execute(request); + var summary = listener.getSummary(); + + if (summary.getTotalFailureCount() == 0) + return; + + summary.printFailuresTo(new PrintWriter(System.err, true, Charset.defaultCharset())); + throw new JUnitException("JUnit run finished with %d failure%s".formatted( // + summary.getTotalFailureCount(), // + summary.getTotalFailureCount() == 1 ? "" : "s")); + } + + 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/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..2d179e5603ba --- /dev/null +++ b/junit-aggregator/src/main/java/org/junit/aggregator/ModuleSupport.java @@ -0,0 +1,55 @@ +/* + * 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 { + 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(); + try (var reader = resolved.reference().open()) { + return reader.list() // + .map(name -> loadClassByResourceName(module, name)) // + .flatMap(Optional::stream) // + .distinct() // + .toList(); + } + catch (Exception exception) { + throw new JUnitException("Listing classes in module %s failed".formatted(module), exception); + } + } + + static Optional> loadClassByResourceName(Module module, String name) { + var className = name; + if (SOURCE_MODE) { + if (name.endsWith(".java")) { + className = name.substring(0, name.length() - 5); + } + } + if (name.endsWith(".class")) { + className = name.substring(0, name.length() - 6); + } + try { + return Optional.of(Class.forName(module, className.replace('/', '.'))); + } + catch (Throwable ignored) { + return Optional.empty(); + } + } +} 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/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}") + } } } 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 5c5a1cc9cf32..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. * @@ -133,6 +153,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. */ @@ -197,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) @@ -229,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 d84d8c79ad21..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 */ @@ -1086,6 +1103,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/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/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/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 */ 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(" ")); } 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..6a21ece099fe --- /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 +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", // 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")