diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc index d255eb41044b..04e694f1d545 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc @@ -26,7 +26,8 @@ repository on GitHub. [[release-notes-6.1.0-M1-junit-platform-new-features-and-improvements]] ==== New Features and Improvements -* ❓ +* Support for creating a `ModuleSelector` from a `java.lang.Module` and using + its classloader for test discovery. [[release-notes-6.1.0-M1-junit-jupiter]] 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 28f6d32b2a26..23bbc51d8665 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 @@ -11,6 +11,7 @@ package org.junit.platform.commons.support; import static org.apiguardian.api.API.Status.DEPRECATED; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import java.lang.reflect.Field; @@ -391,6 +392,31 @@ public static List> findAllClassesInModule(String moduleName, Predicate return ReflectionUtils.findAllClassesInModule(moduleName, classFilter, classNameFilter); } + /** + * Find all {@linkplain Class classes} in the supplied {@code module} + * 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 module to scan; never {@code null} or unnamed + * @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 6.1 + * @see #findAllClassesInClasspathRoot(URI, Predicate, Predicate) + * @see #findAllClassesInPackage(String, Predicate, Predicate) + * @see ResourceSupport#findAllResourcesInModule(String, ResourceFilter) + */ + @API(status = EXPERIMENTAL, since = "6.1") + 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/support/ResourceSupport.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ResourceSupport.java index 248f6d0b1ab2..5a4b32130030 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ResourceSupport.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/support/ResourceSupport.java @@ -10,6 +10,7 @@ package org.junit.platform.commons.support; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import java.net.URI; @@ -193,6 +194,27 @@ public static List findAllResourcesInModule(String moduleName, Resourc return ReflectionUtils.findAllResourcesInModule(moduleName, resourceFilter); } + /** + * Find all {@linkplain Resource resources} in the supplied {@code module} + * that match the specified {@code resourceFilter}. + * + *

The module-path scanning algorithm searches recursively in all + * packages contained in the module. + * + * @param module the module to scan; never {@code null} or unnamed + * @param resourceFilter the resource type filter; never {@code null} + * @return an immutable list of all such resources found; never {@code null} + * but potentially empty + * @since 6.1 + * @see #findAllResourcesInClasspathRoot(URI, ResourceFilter) + * @see #findAllResourcesInPackage(String, ResourceFilter) + * @see ReflectionSupport#findAllClassesInModule(String, Predicate, Predicate) + */ + @API(status = EXPERIMENTAL, since = "6.1") + public static List findAllResourcesInModule(Module module, ResourceFilter resourceFilter) { + return ReflectionUtils.findAllResourcesInModule(module, resourceFilter); + } + /** * Find all {@linkplain Resource resources} in the supplied {@code moduleName} * that match the specified {@code resourceFilter}. 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 a279ac135459..9c0ae00120c4 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 @@ -109,6 +109,27 @@ 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 unnamed + * @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 + * @since 6.1 + */ + @API(status = INTERNAL, since = "6.1") + public static List> findAllClassesInModule(Module module, ClassFilter filter) { + Preconditions.notNull(module, "Module must not be null"); + Preconditions.condition(module.isNamed(), "Module must not be unnamed"); + 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. * @@ -124,7 +145,7 @@ public static List findAllResourcesInModule(String moduleName, Resourc Preconditions.notBlank(moduleName, "Module name must not be null or empty"); Preconditions.notNull(filter, "Resource filter must not be null"); - logger.debug(() -> "Looking for classes in module: " + moduleName); + logger.debug(() -> "Looking for resources in module: " + moduleName); // @formatter:off Set moduleReferences = streamResolvedModules(isEqual(moduleName)) .map(ResolvedModule::reference) @@ -133,6 +154,27 @@ public static List findAllResourcesInModule(String moduleName, Resourc 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.1 + */ + @API(status = INTERNAL, since = "6.1") + public static List findAllResourcesInModule(Module module, ResourceFilter filter) { + Preconditions.notNull(module, "Module must not be null"); + Preconditions.condition(module.isNamed(), "Module must not be unnamed"); + Preconditions.notNull(filter, "Resource filter must not be null"); + + String name = module.getName(); + logger.debug(() -> "Looking for resources 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. */ @@ -175,18 +217,18 @@ private static List> scan(Set references, ClassFilter } /** - * Scan for classes using the supplied set of module references, class + * Scan for resources using the supplied set of module references, class * filter, and loader. */ private static List scan(Set references, ResourceFilter filter, ClassLoader loader) { logger.debug(() -> "Scanning " + references.size() + " module references: " + references); ModuleReferenceResourceScanner scanner = new ModuleReferenceResourceScanner(filter, loader); - List classes = new ArrayList<>(); + List resources = new ArrayList<>(); for (ModuleReference reference : references) { - classes.addAll(scanner.scan(reference)); + resources.addAll(scanner.scan(reference)); } - logger.debug(() -> "Found " + classes.size() + " classes: " + classes); - return List.copyOf(classes); + logger.debug(() -> "Found " + resources.size() + " resources: " + resources); + return List.copyOf(resources); } /** 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 1ae566fde234..4e47d4c1be0e 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 @@ -1061,6 +1061,16 @@ public static List> findAllClassesInModule(String moduleName, Predicate return findAllClassesInModule(moduleName, ClassFilter.of(classNameFilter, classFilter)); } + /** + * @since 6.1 + * @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) @@ -1077,6 +1087,13 @@ public static List> findAllClassesInModule(String moduleName, ClassFilt return List.copyOf(ModuleUtils.findAllClassesInModule(moduleName, classFilter)); } + /** + * @since 6.1 + */ + public static List> findAllClassesInModule(Module module, ClassFilter classFilter) { + return List.copyOf(ModuleUtils.findAllClassesInModule(module, classFilter)); + } + /** * @since 1.11 */ @@ -1084,6 +1101,13 @@ public static List findAllResourcesInModule(String moduleName, Resourc return List.copyOf(ModuleUtils.findAllResourcesInModule(moduleName, resourceFilter)); } + /** + * @since 6.1 + */ + public static List findAllResourcesInModule(Module module, ResourceFilter 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 29c79114022a..137a6507c8eb 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 @@ -428,6 +428,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 unnamed + * @since 6.1 + * @see ModuleSelector + */ + @API(status = EXPERIMENTAL, since = "6.1") + 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..2d8958b23e91 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 @@ -10,6 +10,7 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; @@ -17,6 +18,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 +35,31 @@ @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; } + /** + * {@return the selected {@link Module}, if available} + * + * @since 6.1 + * @see DiscoverySelectors#selectModule(Module) + */ + @API(status = EXPERIMENTAL, since = "6.1") + 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 a108bee73414..507b4fb32c50 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 @@ -49,6 +49,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 1b9149ce956a..d094d5e0a881 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 @@ -299,7 +299,9 @@ void findAllClassesInModuleDelegates() { @Test void findAllClassesInModulePreconditions() { assertPreconditionViolationNotNullOrEmptyFor("Module name", - () -> ReflectionSupport.findAllClassesInModule(null, allTypes, allNames)); + () -> ReflectionSupport.findAllClassesInModule((String) null, allTypes, allNames)); + assertPreconditionViolationNotNullFor("Module", + () -> ReflectionSupport.findAllClassesInModule((Module) null, allTypes, allNames)); assertPreconditionViolationNotNullFor("class predicate", () -> ReflectionSupport.findAllClassesInModule("org.junit.platform.commons", null, allNames)); assertPreconditionViolationNotNullFor("name predicate", diff --git a/platform-tests/src/test/java/org/junit/platform/commons/support/ResourceSupportTests.java b/platform-tests/src/test/java/org/junit/platform/commons/support/ResourceSupportTests.java index 0cc207eab838..35d5648c66b8 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/support/ResourceSupportTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/support/ResourceSupportTests.java @@ -185,7 +185,9 @@ void findAllResourcesInModuleDelegates() { @Test void findAllResourcesInModulePreconditions() { assertPreconditionViolationNotNullOrEmptyFor("Module name", - () -> ResourceSupport.findAllResourcesInModule(null, allResources)); + () -> ResourceSupport.findAllResourcesInModule((String) null, allResources)); + assertPreconditionViolationNotNullFor("Module", + () -> ResourceSupport.findAllResourcesInModule((Module) null, allResources)); assertPreconditionViolationNotNullFor("Resource filter", () -> ResourceSupport.findAllResourcesInModule("org.junit.platform.commons", null)); } 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 83126ede3cfa..ca7065ad17ad 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,6 +16,7 @@ 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.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.platform.commons.test.PreconditionAssertions.assertPreconditionViolationFor; @@ -422,7 +423,7 @@ class SelectModuleTests { @SuppressWarnings("DataFlowIssue") @Test void selectModuleByNamePreconditions() { - assertPreconditionViolationFor(() -> selectModule(null)); + assertPreconditionViolationFor(() -> selectModule((String) null)); assertPreconditionViolationFor(() -> selectModule("")); assertPreconditionViolationFor(() -> selectModule(" ")); } @@ -433,6 +434,21 @@ void selectModuleByName() { assertEquals("java.base", selector.getModuleName()); } + @SuppressWarnings("DataFlowIssue") + @Test + void selectModuleByInstancePreconditions() { + assertPreconditionViolationFor(() -> selectModule((Module) null)); + assertPreconditionViolationFor(() -> selectModule(getClass().getClassLoader().getUnnamedModule())); + } + + @Test + void selectModuleByInstance() { + var module = Object.class.getModule(); + var selector = selectModule(module); + assertEquals("java.base", selector.getModuleName()); + assertSame(module, selector.getModule().orElseThrow()); + } + @SuppressWarnings("DataFlowIssue") @Test void selectModulesByNamesPreconditions() { diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/ModuleSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/ModuleSelectorTests.java index 34cf92a2c4db..532e83f31e20 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/ModuleSelectorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/ModuleSelectorTests.java @@ -12,6 +12,8 @@ import static org.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode; +import java.util.logging.Logger; + import org.junit.jupiter.api.Test; /** @@ -31,4 +33,13 @@ void equalsAndHashCode() { assertEqualsAndHashCode(selector1, selector2, selector3); } + @Test + void equalsAndHashCodeForModuleInstances() { + var selector1 = new ModuleSelector(Object.class.getModule()); // java.base + var selector2 = new ModuleSelector(Object.class.getModule()); // java.base + var selector3 = new ModuleSelector(Logger.class.getModule()); // java.logging + + assertEqualsAndHashCode(selector1, selector2, selector3); + } + }