diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 520da97eef40..0a247d5a0256 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -27,6 +27,9 @@ JUnit repository on GitHub. [[release-notes-5.12.0-M1-junit-platform-new-features-and-improvements]] ==== New Features and Improvements +* New `addResourceContainerSelectorResolver()` in `EngineDiscoveryRequestResolver.Builder` to + support the discovery of class path resource based tests, analogous to the + `addClassContainerSelectorResolver()`. * Introduce `ReflectionSupport.makeAccessible(Field)` for third-party use rather than calling the internal `ReflectionUtils.makeAccessible(Field)` method directly. * Support both the primitive type `void` and the wrapper type `Void` in the internal 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 2081eb20eb4e..13ae4a640a37 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 @@ -19,6 +19,7 @@ import java.net.URI; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; @@ -114,6 +115,51 @@ public static Try> tryToLoadClass(String name, ClassLoader classLoader) return ReflectionUtils.tryToLoadClass(name, classLoader); } + /** + * Tries to get the {@linkplain Resource resources} for the supplied classpath + * resource name. + * + *

The name of a classpath resource must follow the semantics + * for resource paths as defined in {@link ClassLoader#getResource(String)}. + * + *

If the supplied classpath resource name is prefixed with a slash + * ({@code /}), the slash will be removed. + * + * @param classpathResourceName the name of the resource to load; never + * {@code null} or blank + * @return a successful {@code Try} containing the loaded resources or a failed + * {@code Try} containing the exception if no such resources could be loaded; + * never {@code null} + * @since 1.11 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static Try> tryToGetResources(String classpathResourceName) { + return ReflectionUtils.tryToGetResources(classpathResourceName); + } + + /** + * Tries to load the {@linkplain Resource resources} for the supplied classpath + * resource name, using the supplied {@link ClassLoader}. + * + *

The name of a classpath resource must follow the semantics + * for resource paths as defined in {@link ClassLoader#getResource(String)}. + * + *

If the supplied classpath resource name is prefixed with a slash + * ({@code /}), the slash will be removed. + * + * @param classpathResourceName the name of the resource to load; never + * {@code null} or blank + * @param classLoader the {@code ClassLoader} to use; never {@code null} + * @return a successful {@code Try} containing the loaded resources or a failed + * {@code Try} containing the exception if no such resources could be loaded; + * never {@code null} + * @since 1.11 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static Try> tryToGetResources(String classpathResourceName, ClassLoader classLoader) { + return ReflectionUtils.tryToGetResources(classpathResourceName, classLoader); + } + /** * Find all {@linkplain Class classes} in the supplied classpath {@code root} * that match the specified {@code classFilter} and {@code classNameFilter} @@ -235,7 +281,8 @@ public static List> findAllClassesInPackage(String basePackageName, Pre * that match the specified {@code resourceFilter} predicate. * *

The classpath scanning algorithm searches recursively in subpackages - * beginning within the supplied base package. + * beginning within the supplied base package. The resulting list may include + * identically named resources from different classpath roots. * * @param basePackageName the name of the base package in which to start * scanning; must not be {@code null} and must be valid in terms of Java @@ -259,7 +306,8 @@ public static List findAllResourcesInPackage(String basePackageName, P * predicates. * *

The classpath scanning algorithm searches recursively in subpackages - * beginning within the supplied base package. + * beginning within the supplied base package. The resulting stream may + * include identically named resources from different classpath roots. * * @param basePackageName the name of the base package in which to start * scanning; must not be {@code null} and must be valid in terms of Java @@ -284,7 +332,8 @@ public static Stream> streamAllClassesInPackage(String basePackageName, * that match the specified {@code resourceFilter} predicate. * *

The classpath scanning algorithm searches recursively in subpackages - * beginning within the supplied base package. + * beginning within the supplied base package. The resulting stream may + * include identically named resources from different classpath roots. * * @param basePackageName the name of the base package in which to start * scanning; must not be {@code null} and must be valid in terms of Java 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 77e270376854..2dafd46d5344 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 @@ -36,6 +36,8 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -894,6 +896,54 @@ public static Try> tryToLoadClass(String name, ClassLoader classLoader) }); } + /** + * Try to get {@linkplain Resource resources} by their name, using the + * {@link ClassLoaderUtils#getDefaultClassLoader()}. + * + *

See {@link org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String)} + * for details. + * + * @param classpathResourceName the name of the resources to load; never {@code null} or blank + * @since 1.12 + * @see org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String, ClassLoader) + */ + @API(status = INTERNAL, since = "1.12") + public static Try> tryToGetResources(String classpathResourceName) { + return tryToGetResources(classpathResourceName, ClassLoaderUtils.getDefaultClassLoader()); + } + + /** + * Try to get {@linkplain Resource resources} by their name, using the + * supplied {@link ClassLoader}. + * + *

See {@link org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String, ClassLoader)} + * for details. + * + * @param classpathResourceName the name of the resources to load; never {@code null} or blank + * @param classLoader the {@code ClassLoader} to use; never {@code null} + * @since 1.12 + */ + @API(status = INTERNAL, since = "1.12") + public static Try> tryToGetResources(String classpathResourceName, ClassLoader classLoader) { + Preconditions.notBlank(classpathResourceName, "Resource name must not be null or blank"); + Preconditions.notNull(classLoader, "Class loader must not be null"); + boolean startsWithSlash = classpathResourceName.startsWith("/"); + String canonicalClasspathResourceName = (startsWithSlash ? classpathResourceName.substring(1) + : classpathResourceName); + + return Try.call(() -> { + List resources = Collections.list(classLoader.getResources(canonicalClasspathResourceName)); + return resources.stream().map(url -> { + try { + return new ClasspathResource(canonicalClasspathResourceName, url.toURI()); + } + catch (URISyntaxException e) { + throw ExceptionUtils.throwAsUncheckedException(e); + } + }).collect(toCollection(LinkedHashSet::new)); + }); + } + private static Class loadArrayType(ClassLoader classLoader, String componentTypeName, int dimensions) throws ClassNotFoundException { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java index eb0aff3fda14..4b591a38684e 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java @@ -10,13 +10,21 @@ package org.junit.platform.engine.discovery; +import static java.util.Collections.unmodifiableSet; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.Optional; +import java.util.Set; import org.apiguardian.api.API; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.function.Try; +import org.junit.platform.commons.support.Resource; +import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; @@ -34,6 +42,10 @@ * {@linkplain Thread#getContextClassLoader() context class loader} of the * {@linkplain Thread thread} that uses it. * + *

Note: Since Java 9, all resources are on the module path. Either in + * named or unnamed modules. These resources are also considered to be + * classpath resources. + * * @since 1.0 * @see DiscoverySelectors#selectClasspathResource(String) * @see ClasspathRootSelector @@ -44,6 +56,7 @@ public class ClasspathResourceSelector implements DiscoverySelector { private final String classpathResourceName; private final FilePosition position; + private Set classpathResources; ClasspathResourceSelector(String classpathResourceName, FilePosition position) { boolean startsWithSlash = classpathResourceName.startsWith("/"); @@ -51,6 +64,11 @@ public class ClasspathResourceSelector implements DiscoverySelector { this.position = position; } + ClasspathResourceSelector(Set classpathResources) { + this(classpathResources.iterator().next().getName(), null); + this.classpathResources = unmodifiableSet(new LinkedHashSet<>(classpathResources)); + } + /** * Get the name of the selected classpath resource. * @@ -65,6 +83,32 @@ public String getClasspathResourceName() { return this.classpathResourceName; } + /** + * Get the selected {@link Resource resources}. + * + *

If the {@link Resource resources} were not provided, but only their name, + * this method attempts to lazily load the {@link Resource resources} based on + * their name and throws a {@link PreconditionViolationException} if the + * resource cannot be loaded. + * + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public Set getClasspathResources() { + if (this.classpathResources == null) { + Try> tryToGetResource = ReflectionUtils.tryToGetResources(this.classpathResourceName); + Set classpathResources = tryToGetResource.getOrThrow( // + cause -> new PreconditionViolationException( // + "Could not load resource(s) with name: " + this.classpathResourceName, cause)); + if (classpathResources.isEmpty()) { + throw new PreconditionViolationException( + "Could not find any resource(s) with name: " + this.classpathResourceName); + } + this.classpathResources = unmodifiableSet(classpathResources); + } + return this.classpathResources; + } + /** * Get the selected {@code FilePosition} within the classpath resource. */ @@ -100,8 +144,12 @@ public int hashCode() { @Override public String toString() { - return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).append("position", - this.position).toString(); + // @formatter:off + return new ToStringBuilder(this) + .append("classpathResourceName", this.classpathResourceName) + .append("position", this.position) + .toString(); + // @formatter:on } @Override 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 1c6964579e13..28dda12b3a76 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 @@ -10,6 +10,7 @@ package org.junit.platform.engine.discovery; +import static java.util.stream.Collectors.toList; import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; @@ -29,6 +30,8 @@ import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.support.Resource; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.engine.DiscoverySelector; @@ -281,6 +284,7 @@ public static List selectClasspathRoots(Set classpa * @param classpathResourceName the name of the classpath resource; never * {@code null} or blank * @see #selectClasspathResource(String, FilePosition) + * @see #selectClasspathResource(Set) * @see ClasspathResourceSelector * @see ClassLoader#getResource(String) * @see ClassLoader#getResourceAsStream(String) @@ -310,6 +314,7 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath * {@code null} or blank * @param position the position inside the classpath resource; may be {@code null} * @see #selectClasspathResource(String) + * @see #selectClasspathResource(Set) * @see ClasspathResourceSelector * @see ClassLoader#getResource(String) * @see ClassLoader#getResourceAsStream(String) @@ -317,10 +322,43 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath */ public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName, FilePosition position) { - Preconditions.notBlank(classpathResourceName, "Classpath resource name must not be null or blank"); + Preconditions.notBlank(classpathResourceName, "classpath resource name must not be null or blank"); return new ClasspathResourceSelector(classpathResourceName, position); } + /** + * Create a {@code ClasspathResourceSelector} for the supplied classpath + * resources. + * + *

Since {@linkplain org.junit.platform.engine.TestEngine engines} are not + * expected to modify the classpath, the supplied resource must be on the + * classpath of the + * {@linkplain Thread#getContextClassLoader() context class loader} of the + * {@linkplain Thread thread} that uses the resulting selector. + * + *

Note: Since Java 9, all resources are on the module path. Either in + * named or unnamed modules. These resources are also considered to be + * classpath resources. + * + * @param classpathResources a set of classpath resources; never + * {@code null} or empty. All resources must have the same name, may not + * be {@code null} or blank. + * @since 1.12 + * @see #selectClasspathResource(String, FilePosition) + * @see #selectClasspathResource(String) + * @see ClasspathResourceSelector + * @see ReflectionSupport#tryToGetResources(String) + */ + @API(status = EXPERIMENTAL, since = "1.12") + public static ClasspathResourceSelector selectClasspathResource(Set classpathResources) { + Preconditions.notEmpty(classpathResources, "classpath resources must not be null or empty"); + Preconditions.containsNoNullElements(classpathResources, "individual classpath resources must not be null"); + List resourceNames = classpathResources.stream().map(Resource::getName).distinct().collect(toList()); + Preconditions.condition(resourceNames.size() == 1, "all classpath resources must have the same name"); + Preconditions.notBlank(resourceNames.get(0), "classpath resource names must not be null or blank"); + return new ClasspathResourceSelector(classpathResources); + } + /** * Create a {@code ModuleSelector} for the supplied module name. * diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java index df5611990a78..277bd119cb24 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java @@ -11,6 +11,7 @@ package org.junit.platform.engine.support.discovery; import static java.util.stream.Collectors.toCollection; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.ArrayList; @@ -19,6 +20,7 @@ import java.util.function.Predicate; import org.apiguardian.api.API; +import org.junit.platform.commons.support.Resource; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.DiscoveryFilter; import org.junit.platform.engine.EngineDiscoveryRequest; @@ -26,6 +28,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.discovery.ClassNameFilter; import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.ClasspathResourceSelector; import org.junit.platform.engine.discovery.ClasspathRootSelector; import org.junit.platform.engine.discovery.ModuleSelector; import org.junit.platform.engine.discovery.PackageNameFilter; @@ -160,6 +163,25 @@ public Builder addClassContainerSelectorResolver(Predicate> classFil context -> new ClassContainerSelectorResolver(classFilter, context.getClassNameFilter())); } + /** + * Add a predefined resolver that resolves {@link ClasspathRootSelector + * ClasspathRootSelectors}, {@link ModuleSelector ModuleSelectors}, and + * {@link PackageSelector PackageSelectors} into {@link ClasspathResourceSelector + * ClasspathResourceSelectors} by scanning for resources that satisfy the supplied + * predicate in the respective class containers to this builder. + * + * @param resourceFilter predicate the resolved classes must satisfy; never + * {@code null} + * @return this builder for method chaining + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + public Builder addResourceContainerSelectorResolver(Predicate resourceFilter) { + Preconditions.notNull(resourceFilter, "resourceFilter must not be null"); + return addSelectorResolver( + context -> new ResourceContainerSelectorResolver(resourceFilter, context.getPackageFilter())); + } + /** * Add a context insensitive {@link SelectorResolver} to this builder. * @@ -247,6 +269,18 @@ public interface InitializationContext { */ Predicate getClassNameFilter(); + /** + * Get the package name filter built from the {@link PackageNameFilter + * PackageNameFilters} in the {@link EngineDiscoveryRequest} that is + * about to be resolved. + * + * @return the predicate for filtering the resolved resource names; never + * {@code null} + * @since 1.12 + */ + @API(status = EXPERIMENTAL, since = "1.12") + Predicate getPackageFilter(); + } private static class DefaultInitializationContext implements InitializationContext { @@ -254,11 +288,13 @@ private static class DefaultInitializationContext impl private final EngineDiscoveryRequest request; private final T engineDescriptor; private final Predicate classNameFilter; + private final Predicate packageFilter; DefaultInitializationContext(EngineDiscoveryRequest request, T engineDescriptor) { this.request = request; this.engineDescriptor = engineDescriptor; this.classNameFilter = buildClassNamePredicate(request); + this.packageFilter = buildPackagePredicate(request); } /** @@ -274,6 +310,12 @@ private Predicate buildClassNamePredicate(EngineDiscoveryRequest request return Filter.composeFilters(filters).toPredicate(); } + private Predicate buildPackagePredicate(EngineDiscoveryRequest request) { + List> filters = new ArrayList<>(); + filters.addAll(request.getFiltersByType(PackageNameFilter.class)); + return Filter.composeFilters(filters).toPredicate(); + } + @Override public EngineDiscoveryRequest getDiscoveryRequest() { return request; @@ -288,6 +330,11 @@ public T getEngineDescriptor() { public Predicate getClassNameFilter() { return classNameFilter; } + + @Override + public Predicate getPackageFilter() { + return packageFilter; + } } } 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 new file mode 100644 index 000000000000..e5c41492b999 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolver.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2024 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.platform.engine.support.discovery; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toSet; +import static org.junit.platform.commons.support.ReflectionSupport.findAllResourcesInClasspathRoot; +import static org.junit.platform.commons.support.ReflectionSupport.findAllResourcesInPackage; +import static org.junit.platform.commons.util.ReflectionUtils.findAllResourcesInModule; +import static org.junit.platform.engine.support.discovery.ResourceUtils.packageName; +import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.selectors; +import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import org.junit.platform.commons.support.Resource; +import org.junit.platform.engine.discovery.ClasspathResourceSelector; +import org.junit.platform.engine.discovery.ClasspathRootSelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.discovery.ModuleSelector; +import org.junit.platform.engine.discovery.PackageSelector; + +/** + * @since 1.12 + */ +class ResourceContainerSelectorResolver implements SelectorResolver { + private final Predicate resourceFilter; + + ResourceContainerSelectorResolver(Predicate resourceFilter, Predicate packageFilter) { + this.resourceFilter = packageName(packageFilter).and(resourceFilter); + } + + @Override + public Resolution resolve(ClasspathRootSelector selector, Context context) { + return resourceSelectors(findAllResourcesInClasspathRoot(selector.getClasspathRoot(), resourceFilter)); + } + + @Override + public Resolution resolve(ModuleSelector selector, Context context) { + return resourceSelectors(findAllResourcesInModule(selector.getModuleName(), resourceFilter)); + } + + @Override + public Resolution resolve(PackageSelector selector, Context context) { + return resourceSelectors(findAllResourcesInPackage(selector.getPackageName(), resourceFilter)); + } + + private Resolution resourceSelectors(List resources) { + Set selectors = resources.stream() // + .collect(groupingBy(Resource::getName)) // + .values() // + .stream() // + .map(LinkedHashSet::new) // + .map(DiscoverySelectors::selectClasspathResource) // + .collect(toSet()); + + if (selectors.isEmpty()) { + return unresolved(); + } + return selectors(selectors); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceUtils.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceUtils.java new file mode 100644 index 000000000000..02bcd3656c2a --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/ResourceUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2024 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.platform.engine.support.discovery; + +import java.util.function.Predicate; + +import org.junit.platform.commons.support.ReflectionSupport; +import org.junit.platform.commons.support.Resource; + +/** + * Resource-related utilities to be used in conjunction with {@link ReflectionSupport}. + * + * @since 1.12 + */ +class ResourceUtils { + public static final String DEFAULT_PACKAGE_NAME = ""; + private static final char CLASSPATH_RESOURCE_PATH_SEPARATOR = '/'; + private static final char PACKAGE_SEPARATOR_CHAR = '.'; + + /** + * Match resources against a package filter. + * + *

The {@code /} separated path of a resource is rewritten to a + * {@code .} separated package names. The package filter is applied to that + * package name. + */ + static Predicate packageName(Predicate packageFilter) { + return resource -> packageFilter.test(packageName(resource.getName())); + } + + private static String packageName(String classpathResourceName) { + int lastIndexOf = classpathResourceName.lastIndexOf(CLASSPATH_RESOURCE_PATH_SEPARATOR); + if (lastIndexOf < 0) { + return DEFAULT_PACKAGE_NAME; + } + // classpath resource names do not start with / + String resourcePackagePath = classpathResourceName.substring(0, lastIndexOf); + return resourcePackagePath.replace(CLASSPATH_RESOURCE_PATH_SEPARATOR, PACKAGE_SEPARATOR_CHAR); + } +} 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 549a2bee7f8f..d86843227abc 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 @@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.platform.commons.support.PreconditionAssertions.assertPreconditionViolationException; import static org.junit.platform.commons.support.PreconditionAssertions.assertPreconditionViolationExceptionForString; +import static org.junit.platform.commons.util.ClassLoaderUtils.getDefaultClassLoader; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -119,6 +120,31 @@ List findAllClassesInClasspathRootDelegates() throws Throwable { return tests; } + /** + * @since 1.12 + */ + @Test + void tryToGetResourcesPreconditions() { + assertPreconditionViolationExceptionForString("Resource name", () -> ReflectionSupport.tryToGetResources(null)); + assertPreconditionViolationExceptionForString("Resource name", () -> ReflectionSupport.tryToGetResources("")); + assertPreconditionViolationException("Class loader", + () -> ReflectionSupport.tryToGetResources("default-package.resource", null)); + assertPreconditionViolationException("Class loader", + () -> ReflectionSupport.tryToGetResources("default-package.resource", null)); + } + + /** + * @since 1.12 + */ + @Test + void tryToGetResources() { + assertEquals(ReflectionUtils.tryToGetResources("default-package.resource").toOptional(), + ReflectionSupport.tryToGetResources("default-package.resource").toOptional()); + assertEquals( + ReflectionUtils.tryToGetResources("default-package.resource", getDefaultClassLoader()).toOptional(), // + ReflectionSupport.tryToGetResources("default-package.resource", getDefaultClassLoader()).toOptional()); + } + @Test void findAllClassesInClasspathRootPreconditions() { var path = Path.of(".").toUri(); diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java index 5f4cd8f1d431..c8d6e694ec0b 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/ReflectionUtilsTests.java @@ -15,6 +15,8 @@ import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -66,6 +68,7 @@ import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.commons.support.Resource; import org.junit.platform.commons.test.TestClassLoader; import org.junit.platform.commons.util.ReflectionUtilsTests.NestedClassTests.ClassWithNestedClasses.Nested1; import org.junit.platform.commons.util.ReflectionUtilsTests.NestedClassTests.ClassWithNestedClasses.Nested2; @@ -709,6 +712,47 @@ private void privateMethod() { } + @Nested + class ResourceLoadingTests { + + @Test + void tryToGetResourcePreconditions() { + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.tryToGetResources("")); + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.tryToGetResources(" ")); + + assertThrows(PreconditionViolationException.class, () -> ReflectionUtils.tryToGetResources(null)); + assertThrows(PreconditionViolationException.class, + () -> ReflectionUtils.tryToGetResources("org/junit/platform/commons/example.resource", null)); + } + + @Test + void tryToGetResource() { + var tryToGetResource = ReflectionUtils.tryToGetResources("org/junit/platform/commons/example.resource"); + var resource = assertDoesNotThrow(tryToGetResource::get); + assertAll( // + () -> assertThat(resource).hasSize(1), // + () -> assertThat(resource).extracting(Resource::getName) // + .containsExactly("org/junit/platform/commons/example.resource")); + } + + @Test + void tryToGetResourceWithPrefixedSlash() { + var tryToGetResource = ReflectionUtils.tryToGetResources("/org/junit/platform/commons/example.resource"); + var resource = assertDoesNotThrow(tryToGetResource::get); + assertAll( // + () -> assertThat(resource).hasSize(1), // + () -> assertThat(resource).extracting(Resource::getName) // + .containsExactly("org/junit/platform/commons/example.resource")); + } + + @Test + void tryToGetResourceWhenResourceNotFound() { + var tryToGetResource = ReflectionUtils.tryToGetResources("org/junit/platform/commons/no-such.resource"); + var resource = assertDoesNotThrow(tryToGetResource::get); + assertThat(resource).isEmpty(); + } + } + @Nested class ClassLoadingTests { 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 5a1a96506927..c37ce9e8286b 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 @@ -14,6 +14,7 @@ import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; 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.assertThrows; import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; @@ -38,6 +39,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; @@ -53,6 +55,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.support.Resource; import org.junit.platform.commons.test.TestClassLoader; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.engine.DiscoverySelector; @@ -286,12 +289,22 @@ void parseDirectorySelectorWithAbsolutePath() { } @Test - void selectClasspathResources() { - assertViolatesPrecondition(() -> selectClasspathResource(null)); + void selectClasspathResourcesPreconditions() { + assertViolatesPrecondition(() -> selectClasspathResource((String) null)); assertViolatesPrecondition(() -> selectClasspathResource("")); assertViolatesPrecondition(() -> selectClasspathResource(" ")); assertViolatesPrecondition(() -> selectClasspathResource("\t")); + assertViolatesPrecondition(() -> selectClasspathResource((Set) null)); + assertViolatesPrecondition(() -> selectClasspathResource(Collections.emptySet())); + assertViolatesPrecondition(() -> selectClasspathResource(Collections.singleton(null))); + assertViolatesPrecondition(() -> selectClasspathResource(Set.of(new StubResource(null)))); + assertViolatesPrecondition(() -> selectClasspathResource(Set.of(new StubResource("")))); + assertViolatesPrecondition( + () -> selectClasspathResource(Set.of(new StubResource("a"), new StubResource("b")))); + } + @Test + void selectClasspathResources() { // with unnecessary "/" prefix var selector = selectClasspathResource("/foo/bar/spec.xml"); assertEquals("foo/bar/spec.xml", selector.getClasspathResourceName()); @@ -301,6 +314,23 @@ void selectClasspathResources() { assertEquals("A/B/C/spec.json", selector.getClasspathResourceName()); } + @Test + void getSelectedClasspathResources() { + var selector = selectClasspathResource("org/junit/platform/commons/example.resource"); + var classpathResources = selector.getClasspathResources(); + assertAll(() -> assertThat(classpathResources).hasSize(1), // + () -> assertThat(classpathResources) // + .extracting(Resource::getName) // + .containsExactly("org/junit/platform/commons/example.resource") // + ); + } + + @Test + void getMissingClasspathResources() { + var selector = selectClasspathResource("org/junit/platform/commons/no-such-example.resource"); + assertViolatesPrecondition(selector::getClasspathResources); + } + @Test void selectClasspathResourcesWithFilePosition() { var filePosition = FilePosition.from(12, 34); @@ -359,6 +389,23 @@ void parseClasspathResourcesWithFilePosition() { .containsExactly("A/B/C/spec.json", Optional.of(filePosition)); } + private static class StubResource implements Resource { + private final String name; + + private StubResource(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public URI getUri() { + return null; + } + } } @Nested diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolverTest.java b/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolverTest.java new file mode 100644 index 000000000000..9a6b8881699e --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/discovery/ResourceContainerSelectorResolverTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015-2024 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.platform.engine.support.discovery; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathRoots; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; +import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames; +import static org.junit.platform.engine.support.discovery.SelectorResolver.Match.exact; +import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.match; + +import java.net.URI; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.support.Resource; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.ClasspathResourceSelector; +import org.junit.platform.engine.support.descriptor.EngineDescriptor; +import org.junit.platform.fakes.TestDescriptorStub; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; + +class ResourceContainerSelectorResolverTest { + + final TestDescriptor engineDescriptor = new EngineDescriptor(UniqueId.forEngine("resource-engine"), + "Resource Engine"); + final Predicate isResource = resource -> resource.getName().endsWith(".resource"); + + final EngineDiscoveryRequestResolver resolver = EngineDiscoveryRequestResolver.builder() // + .addResourceContainerSelectorResolver(isResource) // + .addSelectorResolver(new ResourceSelectorResolver()) // + .build(); + + @Test + void shouldDiscoverAllResourcesInPackage() { + var request = LauncherDiscoveryRequestBuilder.request() // + .selectors(selectPackage("org.junit.platform.commons")) // + .build(); + + resolver.resolve(request, engineDescriptor); + + // @formatter:off + assertThat(engineDescriptor.getChildren()) + .extracting(TestDescriptor::getDisplayName) + .containsExactlyInAnyOrder( + "org/junit/platform/commons/example.resource", + "org/junit/platform/commons/other-example.resource"); + // @formatter:on + + } + + @Test + void shouldDiscoverAllResourcesInRootPackage() { + var request = LauncherDiscoveryRequestBuilder.request() // + .selectors(selectPackage("")) // + .build(); + + resolver.resolve(request, engineDescriptor); + + // @formatter:off + assertThat(engineDescriptor.getChildren()) + .extracting(TestDescriptor::getDisplayName) + .containsExactlyInAnyOrder( + "default-package.resource", + "org/junit/platform/commons/example.resource", + "org/junit/platform/commons/other-example.resource"); + // @formatter:on + + } + + @Test + void shouldFilterPackages() { + var request = LauncherDiscoveryRequestBuilder.request() // + .selectors(selectPackage("")) // + .filters(includePackageNames("org.junit.platform")) // + .build(); + + resolver.resolve(request, engineDescriptor); + + // @formatter:off + assertThat(engineDescriptor.getChildren()) + .extracting(TestDescriptor::getDisplayName) + .containsExactlyInAnyOrder( + "org/junit/platform/commons/example.resource", + "org/junit/platform/commons/other-example.resource"); + // @formatter:on + + } + + @Test + void shouldDiscoverAllResourcesInClasspathRoot() { + var request = LauncherDiscoveryRequestBuilder.request() // + .selectors(selectClasspathRoots(getTestClasspathResourceRoot())) // + .build(); + + resolver.resolve(request, engineDescriptor); + + // @formatter:off + assertThat(engineDescriptor.getChildren()) + .extracting(TestDescriptor::getDisplayName) + .containsExactlyInAnyOrder( + "default-package.resource", + "org/junit/platform/commons/example.resource", + "org/junit/platform/commons/other-example.resource"); + // @formatter:on + } + + private Set getTestClasspathResourceRoot() { + // Gradle puts classes and resources in different roots. + var defaultPackageResource = "/default-package.resource"; + var resourceUri = getClass().getResource(defaultPackageResource).toString(); + var uri = URI.create(resourceUri.substring(0, resourceUri.length() - defaultPackageResource.length())); + return Collections.singleton(Path.of(uri)); + } + + private static class ResourceSelectorResolver implements SelectorResolver { + @Override + public Resolution resolve(ClasspathResourceSelector selector, Context context) { + return context.addToParent(parent -> createTestDescriptor(parent, selector.getClasspathResourceName())) // + .map(testDescriptor -> match(exact(testDescriptor))) // + .orElseGet(Resolution::unresolved); + } + + private static Optional createTestDescriptor(TestDescriptor parent, + String classpathResourceName) { + var uniqueId = parent.getUniqueId().append("resource", classpathResourceName); + var descriptor = new TestDescriptorStub(uniqueId, classpathResourceName); + return Optional.of(descriptor); + } + } +}