Skip to content

Commit

Permalink
Resolve resource container selectors
Browse files Browse the repository at this point in the history
As a follow up for junit-team#3630 and junit-team#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 `getClasspathResource`
   method. This method will lazily try to load the resource if not was
   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.
  • Loading branch information
mpkorstanje committed Mar 8, 2024
1 parent 704e584 commit 9cbc960
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 1 deletion.
Expand Up @@ -113,6 +113,26 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
return ReflectionUtils.tryToLoadClass(name, classLoader);
}

/**
* Tries to load the {@link Resource} 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 class or a failed
* {@code Try} containing the exception if no such resource could be loaded;
* never {@code null}
* @since 1.11
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static Try<Resource> tryToLoadResource(String classpathResourceName) {
return ReflectionUtils.tryToLoadResource(classpathResourceName);
}

/**
* Find all {@linkplain Class classes} in the supplied classpath {@code root}
* that match the specified {@code classFilter} and {@code classNameFilter}
Expand Down
Expand Up @@ -35,6 +35,7 @@
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -56,6 +57,7 @@

import org.apiguardian.api.API;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.function.Try;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
Expand Down Expand Up @@ -848,6 +850,28 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
});
}

/**
* Tries to load the {@link Resource} for the supplied classpath resource name.
*
* <p>See {@link org.junit.platform.commons.support.ReflectionSupport#tryToLoadResource(String)}
* for details.
*
* @param classpathResourceName the name of the resource to load; never {@code null} or blank
* @since 1.11
*/
@API(status = INTERNAL, since = "1.11")
public static Try<Resource> tryToLoadResource(String classpathResourceName) {
Preconditions.notBlank(classpathResourceName, "Resource name must not be null or blank");
ClassLoader classLoader = ClassLoaderUtils.getDefaultClassLoader();

boolean startsWithSlash = classpathResourceName.startsWith("/");
URL resource = classLoader.getResource(startsWithSlash ? "/" + classpathResourceName : classpathResourceName);
if (resource == null) {
return Try.failure(new PreconditionViolationException("classLoader.getResource returned null"));
}
return Try.call(() -> new ClasspathResource(classpathResourceName, resource.toURI()));
}

private static Class<?> loadArrayType(ClassLoader classLoader, String componentTypeName, int dimensions)
throws ClassNotFoundException {

Expand Down
Expand Up @@ -10,12 +10,17 @@

package org.junit.platform.engine.discovery;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

import java.util.Objects;
import java.util.Optional;

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.ToStringBuilder;
import org.junit.platform.engine.DiscoverySelector;

Expand All @@ -41,13 +46,19 @@ public class ClasspathResourceSelector implements DiscoverySelector {

private final String classpathResourceName;
private final FilePosition position;
private Resource classpathResource;

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

ClasspathResourceSelector(Resource classpathResource) {
this(classpathResource.getName(), null);
this.classpathResource = classpathResource;
}

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

/**
* Get the selected {@link Resource}.
*
* <p>If the {@link Resource} was not provided, but only the name, this
* method attempts to lazily load the {@link Resource} based on its name and
* throws a {@link PreconditionViolationException} if the resource cannot
* be loaded.
*/
@API(status = EXPERIMENTAL, since = "1.11")
public Resource getClasspathResource() {
if (this.classpathResource == null) {
// @formatter:off
Try<Resource> tryToLoadClass = ReflectionUtils.tryToLoadResource(this.classpathResourceName);
this.classpathResource = tryToLoadClass.getOrThrow(cause ->
new PreconditionViolationException("Could not load resource with name: " + this.classpathResourceName, cause));
// @formatter:on
}
return this.classpathResource;
}

/**
* Get the selected {@code FilePosition} within the classpath resource.
*/
Expand Down
Expand Up @@ -26,6 +26,7 @@

import org.apiguardian.api.API;
import org.junit.platform.commons.PreconditionViolationException;
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 @@ -276,6 +277,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(Resource)
* @see ClasspathResourceSelector
* @see ClassLoader#getResource(String)
* @see ClassLoader#getResourceAsStream(String)
Expand Down Expand Up @@ -305,6 +307,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(Resource)
* @see ClasspathResourceSelector
* @see ClassLoader#getResource(String)
* @see ClassLoader#getResourceAsStream(String)
Expand All @@ -316,6 +319,28 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath
return new ClasspathResourceSelector(classpathResourceName, position);
}

/**
* Create a {@code ClasspathResourceSelector} for the supplied classpath
* resource.
*
* <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.
*
* @param classpathResource the classpath resource; never {@code null}
* @see #selectClasspathResource(String, FilePosition)
* @see #selectClasspathResource(String)
* @see ClasspathResourceSelector
* @see org.junit.platform.commons.support.ReflectionSupport#tryToLoadResource(String)
*/
@API(status = EXPERIMENTAL, since = "1.11")
public static ClasspathResourceSelector selectClasspathResource(Resource classpathResource) {
Preconditions.notNull(classpathResource, "classpath resource must not be null or blank");
return new ClasspathResourceSelector(classpathResource);
}

/**
* Create a {@code ModuleSelector} for the supplied module name.
*
Expand Down
Expand Up @@ -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;
Expand All @@ -19,13 +20,15 @@
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;
import org.junit.platform.engine.Filter;
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;
Expand Down Expand Up @@ -160,6 +163,24 @@ public Builder<T> addClassContainerSelectorResolver(Predicate<Class<?>> 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
*/
@API(status = EXPERIMENTAL, since = "1.11")
public Builder<T> addResourceContainerSelectorResolver(Predicate<Resource> 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.
*
Expand Down Expand Up @@ -247,18 +268,31 @@ public interface InitializationContext<T extends TestDescriptor> {
*/
Predicate<String> getClassNameFilter();

/**
* Get the class package 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}
*/
@API(status = EXPERIMENTAL, since = "1.11")
Predicate<String> getPackageFilter();

}

private static class DefaultInitializationContext<T extends TestDescriptor> implements InitializationContext<T> {

private final EngineDiscoveryRequest request;
private final T engineDescriptor;
private final Predicate<String> classNameFilter;
private final Predicate<String> packageFilter;

DefaultInitializationContext(EngineDiscoveryRequest request, T engineDescriptor) {
this.request = request;
this.engineDescriptor = engineDescriptor;
this.classNameFilter = buildClassNamePredicate(request);
this.packageFilter = buildPackagePredicate(request);
}

/**
Expand All @@ -274,6 +308,12 @@ private Predicate<String> buildClassNamePredicate(EngineDiscoveryRequest request
return Filter.composeFilters(filters).toPredicate();
}

private Predicate<String> buildPackagePredicate(EngineDiscoveryRequest request) {
List<DiscoveryFilter<String>> filters = new ArrayList<>();
filters.addAll(request.getFiltersByType(PackageNameFilter.class));
return Filter.composeFilters(filters).toPredicate();
}

@Override
public EngineDiscoveryRequest getDiscoveryRequest() {
return request;
Expand All @@ -288,6 +328,11 @@ public T getEngineDescriptor() {
public Predicate<String> getClassNameFilter() {
return classNameFilter;
}

@Override
public Predicate<String> getPackageFilter() {
return packageFilter;
}
}

}
@@ -0,0 +1,88 @@
/*
* 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.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.SelectorResolver.Resolution.selectors;
import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved;

import java.util.List;
import java.util.function.Predicate;

import org.junit.platform.commons.support.Resource;
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.11
*/
class ResourceContainerSelectorResolver implements SelectorResolver {
private static final char CLASSPATH_RESOURCE_PATH_SEPARATOR = '/';
private static final char PACKAGE_SEPARATOR_CHAR = '.';
public static final String DEFAULT_PACKAGE_NAME = "";
private final Predicate<Resource> resourceFilter;
private final Predicate<String> resourcePackageFilter;

ResourceContainerSelectorResolver(Predicate<Resource> resourceFilter, Predicate<String> resourcePackageFilter) {
this.resourceFilter = resourceFilter;
this.resourcePackageFilter = adaptPackageFilter(resourcePackageFilter);
}

/**
* A package filter is written to test {@code .} separated package names.
* Resources however have {@code /} separated paths. By rewriting the path
* of the resource into a package name, we can make the package filter work.
*/
private static Predicate<String> adaptPackageFilter(Predicate<String> packageFilter) {
return classpathResourceName -> packageFilter.test(packageName(classpathResourceName));
}

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);
}

@Override
public Resolution resolve(ClasspathRootSelector selector, Context context) {
return resourceSelectors(
findAllResourcesInClasspathRoot(selector.getClasspathRoot(), resourceFilter, resourcePackageFilter));
}

@Override
public Resolution resolve(ModuleSelector selector, Context context) {
return resourceSelectors(
findAllResourcesInModule(selector.getModuleName(), resourceFilter, resourcePackageFilter));
}

@Override
public Resolution resolve(PackageSelector selector, Context context) {
return resourceSelectors(
findAllResourcesInPackage(selector.getPackageName(), resourceFilter, resourcePackageFilter));
}

private Resolution resourceSelectors(List<Resource> resources) {
if (resources.isEmpty()) {
return unresolved();
}
return selectors(resources.stream().map(DiscoverySelectors::selectClasspathResource).collect(toSet()));
}

}

0 comments on commit 9cbc960

Please sign in to comment.