Skip to content

Commit

Permalink
Add resource container selector resolver (#3718)
Browse files Browse the repository at this point in the history
As a follow up for #3630 and #3705 this adds a
`addResourceContainerSelectorResolver()`
method to `EngineDiscoveryRequestResolver.Builder` analogous to
`addClassContainerSelectorResolver()`.

Points of note:

 * As classpath resources can be selected from packages, the package
   filter should also be applied. To make this possible the base path of
   a resource is rewritten to a package name prior to being filtered.

 * The `ClasspathResourceSelector` now has a `getClasspathResources`
   method. This method will lazily try to load the resources if not already
   provided when discovering resources in a container.

 * `selectClasspathResource(Resource)` was added to short circuit the
    need to resolve resources twice. And to make it possible to use
    this method as part of the public API,
    `ReflectionSupport.tryToLoadResource` was also added.

---------

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
mpkorstanje and marcphilipp authored Oct 29, 2024
1 parent 8d3c692 commit 8f71c19
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -114,6 +115,51 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
return ReflectionUtils.tryToLoadClass(name, classLoader);
}

/**
* Tries to get the {@linkplain Resource resources} for the supplied classpath
* resource name.
*
* <p>The name of a <em>classpath resource</em> must follow the semantics
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
*
* <p>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<Set<Resource>> 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}.
*
* <p>The name of a <em>classpath resource</em> must follow the semantics
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
*
* <p>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<Set<Resource>> 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}
Expand Down Expand Up @@ -235,7 +281,8 @@ public static List<Class<?>> findAllClassesInPackage(String basePackageName, Pre
* that match the specified {@code resourceFilter} predicate.
*
* <p>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
Expand All @@ -259,7 +306,8 @@ public static List<Resource> findAllResourcesInPackage(String basePackageName, P
* predicates.
*
* <p>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
Expand All @@ -284,7 +332,8 @@ public static Stream<Class<?>> streamAllClassesInPackage(String basePackageName,
* that match the specified {@code resourceFilter} predicate.
*
* <p>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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -894,6 +896,54 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
});
}

/**
* Try to get {@linkplain Resource resources} by their name, using the
* {@link ClassLoaderUtils#getDefaultClassLoader()}.
*
* <p>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<Set<Resource>> tryToGetResources(String classpathResourceName) {
return tryToGetResources(classpathResourceName, ClassLoaderUtils.getDefaultClassLoader());
}

/**
* Try to get {@linkplain Resource resources} by their name, using the
* supplied {@link ClassLoader}.
*
* <p>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<Set<Resource>> 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<URL> 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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +42,10 @@
* {@linkplain Thread#getContextClassLoader() context class loader} of the
* {@linkplain Thread thread} that uses it.
*
* <p>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
Expand All @@ -44,13 +56,19 @@ public class ClasspathResourceSelector implements DiscoverySelector {

private final String classpathResourceName;
private final FilePosition position;
private Set<Resource> classpathResources;

ClasspathResourceSelector(String classpathResourceName, FilePosition position) {
boolean startsWithSlash = classpathResourceName.startsWith("/");
this.classpathResourceName = (startsWithSlash ? classpathResourceName.substring(1) : classpathResourceName);
this.position = position;
}

ClasspathResourceSelector(Set<Resource> classpathResources) {
this(classpathResources.iterator().next().getName(), null);
this.classpathResources = unmodifiableSet(new LinkedHashSet<>(classpathResources));
}

/**
* Get the name of the selected classpath resource.
*
Expand All @@ -65,6 +83,32 @@ public String getClasspathResourceName() {
return this.classpathResourceName;
}

/**
* Get the selected {@link Resource resources}.
*
* <p>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<Resource> getClasspathResources() {
if (this.classpathResources == null) {
Try<Set<Resource>> tryToGetResource = ReflectionUtils.tryToGetResources(this.classpathResourceName);
Set<Resource> 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.
*/
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -281,6 +284,7 @@ public static List<ClasspathRootSelector> selectClasspathRoots(Set<Path> 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)
Expand Down Expand Up @@ -310,17 +314,51 @@ 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)
* @see ClassLoader#getResources(String)
*/
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.
*
* <p>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.
*
* <p>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<Resource> classpathResources) {
Preconditions.notEmpty(classpathResources, "classpath resources must not be null or empty");
Preconditions.containsNoNullElements(classpathResources, "individual classpath resources must not be null");
List<String> 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.
*
Expand Down
Loading

0 comments on commit 8f71c19

Please sign in to comment.