Skip to content

Commit 8f71c19

Browse files
Add resource container selector resolver (#3718)
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]>
1 parent 8d3c692 commit 8f71c19

File tree

12 files changed

+626
-8
lines changed

12 files changed

+626
-8
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ JUnit repository on GitHub.
2727
[[release-notes-5.12.0-M1-junit-platform-new-features-and-improvements]]
2828
==== New Features and Improvements
2929

30+
* New `addResourceContainerSelectorResolver()` in `EngineDiscoveryRequestResolver.Builder` to
31+
support the discovery of class path resource based tests, analogous to the
32+
`addClassContainerSelectorResolver()`.
3033
* Introduce `ReflectionSupport.makeAccessible(Field)` for third-party use rather than
3134
calling the internal `ReflectionUtils.makeAccessible(Field)` method directly.
3235
* Support both the primitive type `void` and the wrapper type `Void` in the internal

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

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.URI;
2020
import java.util.List;
2121
import java.util.Optional;
22+
import java.util.Set;
2223
import java.util.function.Predicate;
2324
import java.util.stream.Stream;
2425

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

118+
/**
119+
* Tries to get the {@linkplain Resource resources} for the supplied classpath
120+
* resource name.
121+
*
122+
* <p>The name of a <em>classpath resource</em> must follow the semantics
123+
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
124+
*
125+
* <p>If the supplied classpath resource name is prefixed with a slash
126+
* ({@code /}), the slash will be removed.
127+
*
128+
* @param classpathResourceName the name of the resource to load; never
129+
* {@code null} or blank
130+
* @return a successful {@code Try} containing the loaded resources or a failed
131+
* {@code Try} containing the exception if no such resources could be loaded;
132+
* never {@code null}
133+
* @since 1.11
134+
*/
135+
@API(status = EXPERIMENTAL, since = "1.12")
136+
public static Try<Set<Resource>> tryToGetResources(String classpathResourceName) {
137+
return ReflectionUtils.tryToGetResources(classpathResourceName);
138+
}
139+
140+
/**
141+
* Tries to load the {@linkplain Resource resources} for the supplied classpath
142+
* resource name, using the supplied {@link ClassLoader}.
143+
*
144+
* <p>The name of a <em>classpath resource</em> must follow the semantics
145+
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
146+
*
147+
* <p>If the supplied classpath resource name is prefixed with a slash
148+
* ({@code /}), the slash will be removed.
149+
*
150+
* @param classpathResourceName the name of the resource to load; never
151+
* {@code null} or blank
152+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
153+
* @return a successful {@code Try} containing the loaded resources or a failed
154+
* {@code Try} containing the exception if no such resources could be loaded;
155+
* never {@code null}
156+
* @since 1.11
157+
*/
158+
@API(status = EXPERIMENTAL, since = "1.12")
159+
public static Try<Set<Resource>> tryToGetResources(String classpathResourceName, ClassLoader classLoader) {
160+
return ReflectionUtils.tryToGetResources(classpathResourceName, classLoader);
161+
}
162+
117163
/**
118164
* Find all {@linkplain Class classes} in the supplied classpath {@code root}
119165
* that match the specified {@code classFilter} and {@code classNameFilter}
@@ -235,7 +281,8 @@ public static List<Class<?>> findAllClassesInPackage(String basePackageName, Pre
235281
* that match the specified {@code resourceFilter} predicate.
236282
*
237283
* <p>The classpath scanning algorithm searches recursively in subpackages
238-
* beginning within the supplied base package.
284+
* beginning within the supplied base package. The resulting list may include
285+
* identically named resources from different classpath roots.
239286
*
240287
* @param basePackageName the name of the base package in which to start
241288
* scanning; must not be {@code null} and must be valid in terms of Java
@@ -259,7 +306,8 @@ public static List<Resource> findAllResourcesInPackage(String basePackageName, P
259306
* predicates.
260307
*
261308
* <p>The classpath scanning algorithm searches recursively in subpackages
262-
* beginning within the supplied base package.
309+
* beginning within the supplied base package. The resulting stream may
310+
* include identically named resources from different classpath roots.
263311
*
264312
* @param basePackageName the name of the base package in which to start
265313
* scanning; must not be {@code null} and must be valid in terms of Java
@@ -284,7 +332,8 @@ public static Stream<Class<?>> streamAllClassesInPackage(String basePackageName,
284332
* that match the specified {@code resourceFilter} predicate.
285333
*
286334
* <p>The classpath scanning algorithm searches recursively in subpackages
287-
* beginning within the supplied base package.
335+
* beginning within the supplied base package. The resulting stream may
336+
* include identically named resources from different classpath roots.
288337
*
289338
* @param basePackageName the name of the base package in which to start
290339
* scanning; must not be {@code null} and must be valid in terms of Java

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import java.lang.reflect.Type;
3737
import java.lang.reflect.TypeVariable;
3838
import java.net.URI;
39+
import java.net.URISyntaxException;
40+
import java.net.URL;
3941
import java.nio.file.Files;
4042
import java.nio.file.Path;
4143
import java.nio.file.Paths;
@@ -894,6 +896,54 @@ public static Try<Class<?>> tryToLoadClass(String name, ClassLoader classLoader)
894896
});
895897
}
896898

899+
/**
900+
* Try to get {@linkplain Resource resources} by their name, using the
901+
* {@link ClassLoaderUtils#getDefaultClassLoader()}.
902+
*
903+
* <p>See {@link org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String)}
904+
* for details.
905+
*
906+
* @param classpathResourceName the name of the resources to load; never {@code null} or blank
907+
* @since 1.12
908+
* @see org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String, ClassLoader)
909+
*/
910+
@API(status = INTERNAL, since = "1.12")
911+
public static Try<Set<Resource>> tryToGetResources(String classpathResourceName) {
912+
return tryToGetResources(classpathResourceName, ClassLoaderUtils.getDefaultClassLoader());
913+
}
914+
915+
/**
916+
* Try to get {@linkplain Resource resources} by their name, using the
917+
* supplied {@link ClassLoader}.
918+
*
919+
* <p>See {@link org.junit.platform.commons.support.ReflectionSupport#tryToGetResources(String, ClassLoader)}
920+
* for details.
921+
*
922+
* @param classpathResourceName the name of the resources to load; never {@code null} or blank
923+
* @param classLoader the {@code ClassLoader} to use; never {@code null}
924+
* @since 1.12
925+
*/
926+
@API(status = INTERNAL, since = "1.12")
927+
public static Try<Set<Resource>> tryToGetResources(String classpathResourceName, ClassLoader classLoader) {
928+
Preconditions.notBlank(classpathResourceName, "Resource name must not be null or blank");
929+
Preconditions.notNull(classLoader, "Class loader must not be null");
930+
boolean startsWithSlash = classpathResourceName.startsWith("/");
931+
String canonicalClasspathResourceName = (startsWithSlash ? classpathResourceName.substring(1)
932+
: classpathResourceName);
933+
934+
return Try.call(() -> {
935+
List<URL> resources = Collections.list(classLoader.getResources(canonicalClasspathResourceName));
936+
return resources.stream().map(url -> {
937+
try {
938+
return new ClasspathResource(canonicalClasspathResourceName, url.toURI());
939+
}
940+
catch (URISyntaxException e) {
941+
throw ExceptionUtils.throwAsUncheckedException(e);
942+
}
943+
}).collect(toCollection(LinkedHashSet::new));
944+
});
945+
}
946+
897947
private static Class<?> loadArrayType(ClassLoader classLoader, String componentTypeName, int dimensions)
898948
throws ClassNotFoundException {
899949

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

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,21 @@
1010

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

13+
import static java.util.Collections.unmodifiableSet;
14+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1315
import static org.apiguardian.api.API.Status.INTERNAL;
1416
import static org.apiguardian.api.API.Status.STABLE;
1517

18+
import java.util.LinkedHashSet;
1619
import java.util.Objects;
1720
import java.util.Optional;
21+
import java.util.Set;
1822

1923
import org.apiguardian.api.API;
24+
import org.junit.platform.commons.PreconditionViolationException;
25+
import org.junit.platform.commons.function.Try;
26+
import org.junit.platform.commons.support.Resource;
27+
import org.junit.platform.commons.util.ReflectionUtils;
2028
import org.junit.platform.commons.util.StringUtils;
2129
import org.junit.platform.commons.util.ToStringBuilder;
2230
import org.junit.platform.engine.DiscoverySelector;
@@ -34,6 +42,10 @@
3442
* {@linkplain Thread#getContextClassLoader() context class loader} of the
3543
* {@linkplain Thread thread} that uses it.
3644
*
45+
* <p>Note: Since Java 9, all resources are on the module path. Either in
46+
* named or unnamed modules. These resources are also considered to be
47+
* classpath resources.
48+
*
3749
* @since 1.0
3850
* @see DiscoverySelectors#selectClasspathResource(String)
3951
* @see ClasspathRootSelector
@@ -44,13 +56,19 @@ public class ClasspathResourceSelector implements DiscoverySelector {
4456

4557
private final String classpathResourceName;
4658
private final FilePosition position;
59+
private Set<Resource> classpathResources;
4760

4861
ClasspathResourceSelector(String classpathResourceName, FilePosition position) {
4962
boolean startsWithSlash = classpathResourceName.startsWith("/");
5063
this.classpathResourceName = (startsWithSlash ? classpathResourceName.substring(1) : classpathResourceName);
5164
this.position = position;
5265
}
5366

67+
ClasspathResourceSelector(Set<Resource> classpathResources) {
68+
this(classpathResources.iterator().next().getName(), null);
69+
this.classpathResources = unmodifiableSet(new LinkedHashSet<>(classpathResources));
70+
}
71+
5472
/**
5573
* Get the name of the selected classpath resource.
5674
*
@@ -65,6 +83,32 @@ public String getClasspathResourceName() {
6583
return this.classpathResourceName;
6684
}
6785

86+
/**
87+
* Get the selected {@link Resource resources}.
88+
*
89+
* <p>If the {@link Resource resources} were not provided, but only their name,
90+
* this method attempts to lazily load the {@link Resource resources} based on
91+
* their name and throws a {@link PreconditionViolationException} if the
92+
* resource cannot be loaded.
93+
*
94+
* @since 1.12
95+
*/
96+
@API(status = EXPERIMENTAL, since = "1.12")
97+
public Set<Resource> getClasspathResources() {
98+
if (this.classpathResources == null) {
99+
Try<Set<Resource>> tryToGetResource = ReflectionUtils.tryToGetResources(this.classpathResourceName);
100+
Set<Resource> classpathResources = tryToGetResource.getOrThrow( //
101+
cause -> new PreconditionViolationException( //
102+
"Could not load resource(s) with name: " + this.classpathResourceName, cause));
103+
if (classpathResources.isEmpty()) {
104+
throw new PreconditionViolationException(
105+
"Could not find any resource(s) with name: " + this.classpathResourceName);
106+
}
107+
this.classpathResources = unmodifiableSet(classpathResources);
108+
}
109+
return this.classpathResources;
110+
}
111+
68112
/**
69113
* Get the selected {@code FilePosition} within the classpath resource.
70114
*/
@@ -100,8 +144,12 @@ public int hashCode() {
100144

101145
@Override
102146
public String toString() {
103-
return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).append("position",
104-
this.position).toString();
147+
// @formatter:off
148+
return new ToStringBuilder(this)
149+
.append("classpathResourceName", this.classpathResourceName)
150+
.append("position", this.position)
151+
.toString();
152+
// @formatter:on
105153
}
106154

107155
@Override

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

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

13+
import static java.util.stream.Collectors.toList;
1314
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1415
import static org.apiguardian.api.API.Status.STABLE;
1516
import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList;
@@ -29,6 +30,8 @@
2930

3031
import org.apiguardian.api.API;
3132
import org.junit.platform.commons.PreconditionViolationException;
33+
import org.junit.platform.commons.support.ReflectionSupport;
34+
import org.junit.platform.commons.support.Resource;
3235
import org.junit.platform.commons.util.Preconditions;
3336
import org.junit.platform.commons.util.ReflectionUtils;
3437
import org.junit.platform.engine.DiscoverySelector;
@@ -281,6 +284,7 @@ public static List<ClasspathRootSelector> selectClasspathRoots(Set<Path> classpa
281284
* @param classpathResourceName the name of the classpath resource; never
282285
* {@code null} or blank
283286
* @see #selectClasspathResource(String, FilePosition)
287+
* @see #selectClasspathResource(Set)
284288
* @see ClasspathResourceSelector
285289
* @see ClassLoader#getResource(String)
286290
* @see ClassLoader#getResourceAsStream(String)
@@ -310,17 +314,51 @@ public static ClasspathResourceSelector selectClasspathResource(String classpath
310314
* {@code null} or blank
311315
* @param position the position inside the classpath resource; may be {@code null}
312316
* @see #selectClasspathResource(String)
317+
* @see #selectClasspathResource(Set)
313318
* @see ClasspathResourceSelector
314319
* @see ClassLoader#getResource(String)
315320
* @see ClassLoader#getResourceAsStream(String)
316321
* @see ClassLoader#getResources(String)
317322
*/
318323
public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName,
319324
FilePosition position) {
320-
Preconditions.notBlank(classpathResourceName, "Classpath resource name must not be null or blank");
325+
Preconditions.notBlank(classpathResourceName, "classpath resource name must not be null or blank");
321326
return new ClasspathResourceSelector(classpathResourceName, position);
322327
}
323328

329+
/**
330+
* Create a {@code ClasspathResourceSelector} for the supplied classpath
331+
* resources.
332+
*
333+
* <p>Since {@linkplain org.junit.platform.engine.TestEngine engines} are not
334+
* expected to modify the classpath, the supplied resource must be on the
335+
* classpath of the
336+
* {@linkplain Thread#getContextClassLoader() context class loader} of the
337+
* {@linkplain Thread thread} that uses the resulting selector.
338+
*
339+
* <p>Note: Since Java 9, all resources are on the module path. Either in
340+
* named or unnamed modules. These resources are also considered to be
341+
* classpath resources.
342+
*
343+
* @param classpathResources a set of classpath resources; never
344+
* {@code null} or empty. All resources must have the same name, may not
345+
* be {@code null} or blank.
346+
* @since 1.12
347+
* @see #selectClasspathResource(String, FilePosition)
348+
* @see #selectClasspathResource(String)
349+
* @see ClasspathResourceSelector
350+
* @see ReflectionSupport#tryToGetResources(String)
351+
*/
352+
@API(status = EXPERIMENTAL, since = "1.12")
353+
public static ClasspathResourceSelector selectClasspathResource(Set<Resource> classpathResources) {
354+
Preconditions.notEmpty(classpathResources, "classpath resources must not be null or empty");
355+
Preconditions.containsNoNullElements(classpathResources, "individual classpath resources must not be null");
356+
List<String> resourceNames = classpathResources.stream().map(Resource::getName).distinct().collect(toList());
357+
Preconditions.condition(resourceNames.size() == 1, "all classpath resources must have the same name");
358+
Preconditions.notBlank(resourceNames.get(0), "classpath resource names must not be null or blank");
359+
return new ClasspathResourceSelector(classpathResources);
360+
}
361+
324362
/**
325363
* Create a {@code ModuleSelector} for the supplied module name.
326364
*

0 commit comments

Comments
 (0)