Skip to content

Commit 9cbc960

Browse files
committed
Resolve resource container selectors
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.
1 parent 704e584 commit 9cbc960

File tree

9 files changed

+398
-1
lines changed

9 files changed

+398
-1
lines changed

junit-platform-commons/src/main/java/org/junit/platform/commons/support/ReflectionSupport.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,26 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
113113
return ReflectionUtils.tryToLoadClass(name, classLoader);
114114
}
115115

116+
/**
117+
* Tries to load the {@link Resource} for the supplied classpath resource name.
118+
*
119+
* <p>The name of a <em>classpath resource</em> must follow the semantics
120+
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
121+
*
122+
* <p>If the supplied classpath resource name is prefixed with a slash
123+
* ({@code /}), the slash will be removed.
124+
*
125+
* @param classpathResourceName the name of the resource to load; never {@code null} or blank
126+
* @return a successful {@code Try} containing the loaded class or a failed
127+
* {@code Try} containing the exception if no such resource could be loaded;
128+
* never {@code null}
129+
* @since 1.11
130+
*/
131+
@API(status = EXPERIMENTAL, since = "1.11")
132+
public static Try<Resource> tryToLoadResource(String classpathResourceName) {
133+
return ReflectionUtils.tryToLoadResource(classpathResourceName);
134+
}
135+
116136
/**
117137
* Find all {@linkplain Class classes} in the supplied classpath {@code root}
118138
* that match the specified {@code classFilter} and {@code classNameFilter}

junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.lang.reflect.Type;
3636
import java.lang.reflect.TypeVariable;
3737
import java.net.URI;
38+
import java.net.URL;
3839
import java.nio.file.Files;
3940
import java.nio.file.Path;
4041
import java.nio.file.Paths;
@@ -56,6 +57,7 @@
5657

5758
import org.apiguardian.api.API;
5859
import org.junit.platform.commons.JUnitException;
60+
import org.junit.platform.commons.PreconditionViolationException;
5961
import org.junit.platform.commons.function.Try;
6062
import org.junit.platform.commons.logging.Logger;
6163
import org.junit.platform.commons.logging.LoggerFactory;
@@ -848,6 +850,28 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
848850
});
849851
}
850852

853+
/**
854+
* Tries to load the {@link Resource} for the supplied classpath resource name.
855+
*
856+
* <p>See {@link org.junit.platform.commons.support.ReflectionSupport#tryToLoadResource(String)}
857+
* for details.
858+
*
859+
* @param classpathResourceName the name of the resource to load; never {@code null} or blank
860+
* @since 1.11
861+
*/
862+
@API(status = INTERNAL, since = "1.11")
863+
public static Try<Resource> tryToLoadResource(String classpathResourceName) {
864+
Preconditions.notBlank(classpathResourceName, "Resource name must not be null or blank");
865+
ClassLoader classLoader = ClassLoaderUtils.getDefaultClassLoader();
866+
867+
boolean startsWithSlash = classpathResourceName.startsWith("/");
868+
URL resource = classLoader.getResource(startsWithSlash ? "/" + classpathResourceName : classpathResourceName);
869+
if (resource == null) {
870+
return Try.failure(new PreconditionViolationException("classLoader.getResource returned null"));
871+
}
872+
return Try.call(() -> new ClasspathResource(classpathResourceName, resource.toURI()));
873+
}
874+
851875
private static Class<?> loadArrayType(ClassLoader classLoader, String componentTypeName, int dimensions)
852876
throws ClassNotFoundException {
853877

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@
1010

1111
package org.junit.platform.engine.discovery;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.STABLE;
1415

1516
import java.util.Objects;
1617
import java.util.Optional;
1718

1819
import org.apiguardian.api.API;
20+
import org.junit.platform.commons.PreconditionViolationException;
21+
import org.junit.platform.commons.function.Try;
22+
import org.junit.platform.commons.support.Resource;
23+
import org.junit.platform.commons.util.ReflectionUtils;
1924
import org.junit.platform.commons.util.ToStringBuilder;
2025
import org.junit.platform.engine.DiscoverySelector;
2126

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

4247
private final String classpathResourceName;
4348
private final FilePosition position;
49+
private Resource classpathResource;
4450

4551
ClasspathResourceSelector(String classpathResourceName, FilePosition position) {
4652
boolean startsWithSlash = classpathResourceName.startsWith("/");
4753
this.classpathResourceName = (startsWithSlash ? classpathResourceName.substring(1) : classpathResourceName);
4854
this.position = position;
4955
}
5056

57+
ClasspathResourceSelector(Resource classpathResource) {
58+
this(classpathResource.getName(), null);
59+
this.classpathResource = classpathResource;
60+
}
61+
5162
/**
5263
* Get the name of the selected classpath resource.
5364
*
@@ -62,6 +73,26 @@ public String getClasspathResourceName() {
6273
return this.classpathResourceName;
6374
}
6475

76+
/**
77+
* Get the selected {@link Resource}.
78+
*
79+
* <p>If the {@link Resource} was not provided, but only the name, this
80+
* method attempts to lazily load the {@link Resource} based on its name and
81+
* throws a {@link PreconditionViolationException} if the resource cannot
82+
* be loaded.
83+
*/
84+
@API(status = EXPERIMENTAL, since = "1.11")
85+
public Resource getClasspathResource() {
86+
if (this.classpathResource == null) {
87+
// @formatter:off
88+
Try<Resource> tryToLoadClass = ReflectionUtils.tryToLoadResource(this.classpathResourceName);
89+
this.classpathResource = tryToLoadClass.getOrThrow(cause ->
90+
new PreconditionViolationException("Could not load resource with name: " + this.classpathResourceName, cause));
91+
// @formatter:on
92+
}
93+
return this.classpathResource;
94+
}
95+
6596
/**
6697
* Get the selected {@code FilePosition} within the classpath resource.
6798
*/

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import org.apiguardian.api.API;
2828
import org.junit.platform.commons.PreconditionViolationException;
29+
import org.junit.platform.commons.support.Resource;
2930
import org.junit.platform.commons.util.Preconditions;
3031
import org.junit.platform.commons.util.ReflectionUtils;
3132
import org.junit.platform.engine.DiscoverySelector;
@@ -276,6 +277,7 @@ public static List<ClasspathRootSelector> selectClasspathRoots(Set<Path> classpa
276277
* @param classpathResourceName the name of the classpath resource; never
277278
* {@code null} or blank
278279
* @see #selectClasspathResource(String, FilePosition)
280+
* @see #selectClasspathResource(Resource)
279281
* @see ClasspathResourceSelector
280282
* @see ClassLoader#getResource(String)
281283
* @see ClassLoader#getResourceAsStream(String)
@@ -305,6 +307,7 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath
305307
* {@code null} or blank
306308
* @param position the position inside the classpath resource; may be {@code null}
307309
* @see #selectClasspathResource(String)
310+
* @see #selectClasspathResource(Resource)
308311
* @see ClasspathResourceSelector
309312
* @see ClassLoader#getResource(String)
310313
* @see ClassLoader#getResourceAsStream(String)
@@ -316,6 +319,28 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath
316319
return new ClasspathResourceSelector(classpathResourceName, position);
317320
}
318321

322+
/**
323+
* Create a {@code ClasspathResourceSelector} for the supplied classpath
324+
* resource.
325+
*
326+
* <p>Since {@linkplain org.junit.platform.engine.TestEngine engines} are not
327+
* expected to modify the classpath, the supplied resource must be on the
328+
* classpath of the
329+
* {@linkplain Thread#getContextClassLoader() context class loader} of the
330+
* {@linkplain Thread thread} that uses the resulting selector.
331+
*
332+
* @param classpathResource the classpath resource; never {@code null}
333+
* @see #selectClasspathResource(String, FilePosition)
334+
* @see #selectClasspathResource(String)
335+
* @see ClasspathResourceSelector
336+
* @see org.junit.platform.commons.support.ReflectionSupport#tryToLoadResource(String)
337+
*/
338+
@API(status = EXPERIMENTAL, since = "1.11")
339+
public static ClasspathResourceSelector selectClasspathResource(Resource classpathResource) {
340+
Preconditions.notNull(classpathResource, "classpath resource must not be null or blank");
341+
return new ClasspathResourceSelector(classpathResource);
342+
}
343+
319344
/**
320345
* Create a {@code ModuleSelector} for the supplied module name.
321346
*

junit-platform-engine/src/main/java/org/junit/platform/engine/support/discovery/EngineDiscoveryRequestResolver.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.junit.platform.engine.support.discovery;
1212

1313
import static java.util.stream.Collectors.toCollection;
14+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1415
import static org.apiguardian.api.API.Status.STABLE;
1516

1617
import java.util.ArrayList;
@@ -19,13 +20,15 @@
1920
import java.util.function.Predicate;
2021

2122
import org.apiguardian.api.API;
23+
import org.junit.platform.commons.support.Resource;
2224
import org.junit.platform.commons.util.Preconditions;
2325
import org.junit.platform.engine.DiscoveryFilter;
2426
import org.junit.platform.engine.EngineDiscoveryRequest;
2527
import org.junit.platform.engine.Filter;
2628
import org.junit.platform.engine.TestDescriptor;
2729
import org.junit.platform.engine.discovery.ClassNameFilter;
2830
import org.junit.platform.engine.discovery.ClassSelector;
31+
import org.junit.platform.engine.discovery.ClasspathResourceSelector;
2932
import org.junit.platform.engine.discovery.ClasspathRootSelector;
3033
import org.junit.platform.engine.discovery.ModuleSelector;
3134
import org.junit.platform.engine.discovery.PackageNameFilter;
@@ -160,6 +163,24 @@ public Builder<T> addClassContainerSelectorResolver(Predicate<Class<?>> classFil
160163
context -> new ClassContainerSelectorResolver(classFilter, context.getClassNameFilter()));
161164
}
162165

166+
/**
167+
* Add a predefined resolver that resolves {@link ClasspathRootSelector
168+
* ClasspathRootSelectors}, {@link ModuleSelector ModuleSelectors}, and
169+
* {@link PackageSelector PackageSelectors} into {@link ClasspathResourceSelector
170+
* ClasspathResourceSelectors} by scanning for resources that satisfy the supplied
171+
* predicate in the respective class containers to this builder.
172+
*
173+
* @param resourceFilter predicate the resolved classes must satisfy; never
174+
* {@code null}
175+
* @return this builder for method chaining
176+
*/
177+
@API(status = EXPERIMENTAL, since = "1.11")
178+
public Builder<T> addResourceContainerSelectorResolver(Predicate<Resource> resourceFilter) {
179+
Preconditions.notNull(resourceFilter, "resourceFilter must not be null");
180+
return addSelectorResolver(
181+
context -> new ResourceContainerSelectorResolver(resourceFilter, context.getPackageFilter()));
182+
}
183+
163184
/**
164185
* Add a context insensitive {@link SelectorResolver} to this builder.
165186
*
@@ -247,18 +268,31 @@ public interface InitializationContext<T extends TestDescriptor> {
247268
*/
248269
Predicate<String> getClassNameFilter();
249270

271+
/**
272+
* Get the class package filter built from the {@link PackageNameFilter
273+
* PackageNameFilters} in the {@link EngineDiscoveryRequest} that is
274+
* about to be resolved.
275+
*
276+
* @return the predicate for filtering the resolved resource names; never
277+
* {@code null}
278+
*/
279+
@API(status = EXPERIMENTAL, since = "1.11")
280+
Predicate<String> getPackageFilter();
281+
250282
}
251283

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

254286
private final EngineDiscoveryRequest request;
255287
private final T engineDescriptor;
256288
private final Predicate<String> classNameFilter;
289+
private final Predicate<String> packageFilter;
257290

258291
DefaultInitializationContext(EngineDiscoveryRequest request, T engineDescriptor) {
259292
this.request = request;
260293
this.engineDescriptor = engineDescriptor;
261294
this.classNameFilter = buildClassNamePredicate(request);
295+
this.packageFilter = buildPackagePredicate(request);
262296
}
263297

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

311+
private Predicate<String> buildPackagePredicate(EngineDiscoveryRequest request) {
312+
List<DiscoveryFilter<String>> filters = new ArrayList<>();
313+
filters.addAll(request.getFiltersByType(PackageNameFilter.class));
314+
return Filter.composeFilters(filters).toPredicate();
315+
}
316+
277317
@Override
278318
public EngineDiscoveryRequest getDiscoveryRequest() {
279319
return request;
@@ -288,6 +328,11 @@ public T getEngineDescriptor() {
288328
public Predicate<String> getClassNameFilter() {
289329
return classNameFilter;
290330
}
331+
332+
@Override
333+
public Predicate<String> getPackageFilter() {
334+
return packageFilter;
335+
}
291336
}
292337

293338
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2015-2024 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.engine.support.discovery;
12+
13+
import static java.util.stream.Collectors.toSet;
14+
import static org.junit.platform.commons.support.ReflectionSupport.findAllResourcesInClasspathRoot;
15+
import static org.junit.platform.commons.support.ReflectionSupport.findAllResourcesInPackage;
16+
import static org.junit.platform.commons.util.ReflectionUtils.findAllResourcesInModule;
17+
import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.selectors;
18+
import static org.junit.platform.engine.support.discovery.SelectorResolver.Resolution.unresolved;
19+
20+
import java.util.List;
21+
import java.util.function.Predicate;
22+
23+
import org.junit.platform.commons.support.Resource;
24+
import org.junit.platform.engine.discovery.ClasspathRootSelector;
25+
import org.junit.platform.engine.discovery.DiscoverySelectors;
26+
import org.junit.platform.engine.discovery.ModuleSelector;
27+
import org.junit.platform.engine.discovery.PackageSelector;
28+
29+
/**
30+
* @since 1.11
31+
*/
32+
class ResourceContainerSelectorResolver implements SelectorResolver {
33+
private static final char CLASSPATH_RESOURCE_PATH_SEPARATOR = '/';
34+
private static final char PACKAGE_SEPARATOR_CHAR = '.';
35+
public static final String DEFAULT_PACKAGE_NAME = "";
36+
private final Predicate<Resource> resourceFilter;
37+
private final Predicate<String> resourcePackageFilter;
38+
39+
ResourceContainerSelectorResolver(Predicate<Resource> resourceFilter, Predicate<String> resourcePackageFilter) {
40+
this.resourceFilter = resourceFilter;
41+
this.resourcePackageFilter = adaptPackageFilter(resourcePackageFilter);
42+
}
43+
44+
/**
45+
* A package filter is written to test {@code .} separated package names.
46+
* Resources however have {@code /} separated paths. By rewriting the path
47+
* of the resource into a package name, we can make the package filter work.
48+
*/
49+
private static Predicate<String> adaptPackageFilter(Predicate<String> packageFilter) {
50+
return classpathResourceName -> packageFilter.test(packageName(classpathResourceName));
51+
}
52+
53+
private static String packageName(String classpathResourceName) {
54+
int lastIndexOf = classpathResourceName.lastIndexOf(CLASSPATH_RESOURCE_PATH_SEPARATOR);
55+
if (lastIndexOf < 0) {
56+
return DEFAULT_PACKAGE_NAME;
57+
}
58+
// classpath resource names do not start with /
59+
String resourcePackagePath = classpathResourceName.substring(0, lastIndexOf);
60+
return resourcePackagePath.replace(CLASSPATH_RESOURCE_PATH_SEPARATOR, PACKAGE_SEPARATOR_CHAR);
61+
}
62+
63+
@Override
64+
public Resolution resolve(ClasspathRootSelector selector, Context context) {
65+
return resourceSelectors(
66+
findAllResourcesInClasspathRoot(selector.getClasspathRoot(), resourceFilter, resourcePackageFilter));
67+
}
68+
69+
@Override
70+
public Resolution resolve(ModuleSelector selector, Context context) {
71+
return resourceSelectors(
72+
findAllResourcesInModule(selector.getModuleName(), resourceFilter, resourcePackageFilter));
73+
}
74+
75+
@Override
76+
public Resolution resolve(PackageSelector selector, Context context) {
77+
return resourceSelectors(
78+
findAllResourcesInPackage(selector.getPackageName(), resourceFilter, resourcePackageFilter));
79+
}
80+
81+
private Resolution resourceSelectors(List<Resource> resources) {
82+
if (resources.isEmpty()) {
83+
return unresolved();
84+
}
85+
return selectors(resources.stream().map(DiscoverySelectors::selectClasspathResource).collect(toSet()));
86+
}
87+
88+
}

0 commit comments

Comments
 (0)