Skip to content

Commit 16c6f72

Browse files
Add include/exclude-based filtering for auto-detected extensions (#4120)
Two new configuration parameters allow configured comma-separated lists of includes and excludes for auto-detected extension registration: * `junit.jupiter.extensions.autodetection.include` * `junit.jupiter.extensions.autodetection.exclude` Resolves #3717. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent efa1527 commit 16c6f72

File tree

18 files changed

+441
-25
lines changed

18 files changed

+441
-25
lines changed

documentation/src/docs/asciidoc/user-guide/extensions.adoc

+17
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,23 @@ When auto-detection is enabled, extensions discovered via the `{ServiceLoader}`
307307
will be added to the extension registry after JUnit Jupiter's global extensions (e.g.,
308308
support for `TestInfo`, `TestReporter`, etc.).
309309

310+
[[extensions-registration-automatic-filtering]]
311+
===== Filtering Auto-detected Extensions
312+
313+
The list of auto-detected extensions can be filtered using include and exclude patterns
314+
via the following <<running-tests-config-params, configuration parameters>>:
315+
316+
`junit.jupiter.extensions.autodetection.include=<patterns>`::
317+
Comma-separated list of _include_ patterns for auto-detected extensions.
318+
`junit.jupiter.extensions.autodetection.exclude=<patterns>`::
319+
Comma-separated list of _exclude_ patterns for auto-detected extensions.
320+
321+
Include patterns are applied _before_ exclude patterns. If both include and exclude
322+
patterns are provided, only extensions that match at least one include pattern and do not
323+
match any exclude pattern will be auto-detected.
324+
325+
See <<running-tests-config-params-deactivation-pattern>> for details on the pattern syntax.
326+
310327
[[extensions-registration-inheritance]]
311328
==== Extension Inheritance
312329

documentation/src/docs/asciidoc/user-guide/running-tests.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ parameters_ used for the following features.
975975
- <<extensions-conditions-deactivation>>
976976
- <<launcher-api-listeners-custom-deactivation>>
977977
- <<stacktrace-pruning>>
978+
- <<extensions-registration-automatic-filtering>>
978979

979980
If the value for the given _configuration parameter_ consists solely of an asterisk
980981
(`+++*+++`), the pattern will match against all candidate classes. Otherwise, the value

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java

+74
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,80 @@
4949
@API(status = STABLE, since = "5.0")
5050
public final class Constants {
5151

52+
/**
53+
* Property name used to include patterns for auto-detecting extensions: {@value}
54+
*
55+
* <h4>Pattern Matching Syntax</h4>
56+
*
57+
* <p>If the property value consists solely of an asterisk ({@code *}), all
58+
* extensions will be included. Otherwise, the property value will be treated
59+
* as a comma-separated list of patterns where each individual pattern will be
60+
* matched against the fully qualified class name (<em>FQCN</em>) of each extension.
61+
* Any dot ({@code .}) in a pattern will match against a dot ({@code .})
62+
* or a dollar sign ({@code $}) in a FQCN. Any asterisk ({@code *}) will match
63+
* against one or more characters in a FQCN. All other characters in a pattern
64+
* will be matched one-to-one against a FQCN.
65+
*
66+
* <h4>Examples</h4>
67+
*
68+
* <ul>
69+
* <li>{@code *}: includes all extensions.
70+
* <li>{@code org.junit.*}: includes every extension under the {@code org.junit}
71+
* base package and any of its subpackages.
72+
* <li>{@code *.MyExtension}: includes every extension whose simple class name is
73+
* exactly {@code MyExtension}.
74+
* <li>{@code *System*}: includes every extension whose FQCN contains
75+
* {@code System}.
76+
* <li>{@code *System*, *Dev*}: includes every extension whose FQCN contains
77+
* {@code System} or {@code Dev}.
78+
* <li>{@code org.example.MyExtension, org.example.TheirExtension}: includes
79+
* extensions whose FQCN is exactly {@code org.example.MyExtension} or
80+
* {@code org.example.TheirExtension}.
81+
* </ul>
82+
*
83+
* <p>Note: A class that matches both an inclusion and exclusion pattern will be excluded.
84+
*
85+
* @see JupiterConfiguration#EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME
86+
*/
87+
public static final String EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME;
88+
89+
/**
90+
* Property name used to exclude patterns for auto-detecting extensions: {@value}
91+
*
92+
* <h4>Pattern Matching Syntax</h4>
93+
*
94+
* <p>If the property value consists solely of an asterisk ({@code *}), all
95+
* extensions will be excluded. Otherwise, the property value will be treated
96+
* as a comma-separated list of patterns where each individual pattern will be
97+
* matched against the fully qualified class name (<em>FQCN</em>) of each extension.
98+
* Any dot ({@code .}) in a pattern will match against a dot ({@code .})
99+
* or a dollar sign ({@code $}) in a FQCN. Any asterisk ({@code *}) will match
100+
* against one or more characters in a FQCN. All other characters in a pattern
101+
* will be matched one-to-one against a FQCN.
102+
*
103+
* <h4>Examples</h4>
104+
*
105+
* <ul>
106+
* <li>{@code *}: excludes all extensions.
107+
* <li>{@code org.junit.*}: excludes every extension under the {@code org.junit}
108+
* base package and any of its subpackages.
109+
* <li>{@code *.MyExtension}: excludes every extension whose simple class name is
110+
* exactly {@code MyExtension}.
111+
* <li>{@code *System*}: excludes every extension whose FQCN contains
112+
* {@code System}.
113+
* <li>{@code *System*, *Dev*}: excludes every extension whose FQCN contains
114+
* {@code System} or {@code Dev}.
115+
* <li>{@code org.example.MyExtension, org.example.TheirExtension}: excludes
116+
* extensions whose FQCN is exactly {@code org.example.MyExtension} or
117+
* {@code org.example.TheirExtension}.
118+
* </ul>
119+
*
120+
* <p>Note: A class that matches both an inclusion and exclusion pattern will be excluded.
121+
*
122+
* @see JupiterConfiguration#EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME
123+
*/
124+
public static final String EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME;
125+
52126
/**
53127
* Property name used to provide patterns for deactivating conditions: {@value}
54128
*

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.jupiter.api.MethodOrderer;
2727
import org.junit.jupiter.api.TestInstance;
2828
import org.junit.jupiter.api.extension.ExecutionCondition;
29+
import org.junit.jupiter.api.extension.Extension;
2930
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
3031
import org.junit.jupiter.api.io.CleanupMode;
3132
import org.junit.jupiter.api.io.TempDirFactory;
@@ -47,6 +48,11 @@ public CachingJupiterConfiguration(JupiterConfiguration delegate) {
4748
this.delegate = delegate;
4849
}
4950

51+
@Override
52+
public Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions() {
53+
return delegate.getFilterForAutoDetectedExtensions();
54+
}
55+
5056
@Override
5157
public Optional<String> getRawConfigurationParameter(String key) {
5258
return delegate.getRawConfigurationParameter(key);

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java

+20
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.junit.jupiter.api.MethodOrderer;
2727
import org.junit.jupiter.api.TestInstance.Lifecycle;
2828
import org.junit.jupiter.api.extension.ExecutionCondition;
29+
import org.junit.jupiter.api.extension.Extension;
2930
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
3031
import org.junit.jupiter.api.io.CleanupMode;
3132
import org.junit.jupiter.api.io.TempDirFactory;
@@ -77,6 +78,25 @@ public DefaultJupiterConfiguration(ConfigurationParameters configurationParamete
7778
this.outputDirectoryProvider = outputDirectoryProvider;
7879
}
7980

81+
@Override
82+
public Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions() {
83+
String includePattern = getExtensionAutoDetectionIncludePattern();
84+
String excludePattern = getExtensionAutoDetectionExcludePattern();
85+
Predicate<String> predicate = ClassNamePatternFilterUtils.includeMatchingClassNames(includePattern) //
86+
.and(ClassNamePatternFilterUtils.excludeMatchingClassNames(excludePattern));
87+
return clazz -> predicate.test(clazz.getName());
88+
}
89+
90+
private String getExtensionAutoDetectionIncludePattern() {
91+
return configurationParameters.get(EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME) //
92+
.orElse(ClassNamePatternFilterUtils.ALL_PATTERN);
93+
}
94+
95+
private String getExtensionAutoDetectionExcludePattern() {
96+
return configurationParameters.get(EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME) //
97+
.orElse(ClassNamePatternFilterUtils.BLANK);
98+
}
99+
80100
@Override
81101
public Optional<String> getRawConfigurationParameter(String key) {
82102
return configurationParameters.get(key);

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.junit.jupiter.api.MethodOrderer;
2424
import org.junit.jupiter.api.TestInstance;
2525
import org.junit.jupiter.api.extension.ExecutionCondition;
26+
import org.junit.jupiter.api.extension.Extension;
2627
import org.junit.jupiter.api.extension.PreInterruptCallback;
2728
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
2829
import org.junit.jupiter.api.io.CleanupMode;
@@ -37,6 +38,8 @@
3738
@API(status = INTERNAL, since = "5.4")
3839
public interface JupiterConfiguration {
3940

41+
String EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.include";
42+
String EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.exclude";
4043
String DEACTIVATE_CONDITIONS_PATTERN_PROPERTY_NAME = "junit.jupiter.conditions.deactivate";
4144
String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.parallel.enabled";
4245
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
@@ -49,6 +52,8 @@ public interface JupiterConfiguration {
4952
String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME;;
5053
String DEFAULT_TEST_INSTANTIATION_EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME = ExtensionContextScope.DEFAULT_SCOPE_PROPERTY_NAME;
5154

55+
Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions();
56+
5257
Optional<String> getRawConfigurationParameter(String key);
5358

5459
<T> Optional<T> getRawConfigurationParameter(String key, Function<String, T> transformer);

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java

+34-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.ServiceLoader;
2929
import java.util.Set;
3030
import java.util.function.Function;
31+
import java.util.function.Predicate;
32+
import java.util.stream.Collectors;
3133
import java.util.stream.Stream;
3234

3335
import org.apiguardian.api.API;
@@ -38,6 +40,7 @@
3840
import org.junit.platform.commons.support.ReflectionSupport;
3941
import org.junit.platform.commons.util.ClassLoaderUtils;
4042
import org.junit.platform.commons.util.Preconditions;
43+
import org.junit.platform.commons.util.ServiceLoaderUtils;
4144

4245
/**
4346
* Default, mutable implementation of {@link ExtensionRegistry}.
@@ -83,7 +86,7 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit
8386
extensionRegistry.registerDefaultExtension(new TempDirectory(configuration));
8487

8588
if (configuration.isExtensionAutoDetectionEnabled()) {
86-
registerAutoDetectedExtensions(extensionRegistry);
89+
registerAutoDetectedExtensions(extensionRegistry, configuration);
8790
}
8891

8992
if (configuration.isThreadDumpOnTimeoutEnabled()) {
@@ -93,9 +96,37 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit
9396
return extensionRegistry;
9497
}
9598

96-
private static void registerAutoDetectedExtensions(MutableExtensionRegistry extensionRegistry) {
97-
ServiceLoader.load(Extension.class, ClassLoaderUtils.getDefaultClassLoader())//
99+
private static void registerAutoDetectedExtensions(MutableExtensionRegistry extensionRegistry,
100+
JupiterConfiguration configuration) {
101+
102+
Predicate<Class<? extends Extension>> filter = configuration.getFilterForAutoDetectedExtensions();
103+
List<Class<? extends Extension>> excludedExtensions = new ArrayList<>();
104+
105+
ServiceLoader<Extension> serviceLoader = ServiceLoader.load(Extension.class,
106+
ClassLoaderUtils.getDefaultClassLoader());
107+
ServiceLoaderUtils.filter(serviceLoader, clazz -> {
108+
boolean included = filter.test(clazz);
109+
if (!included) {
110+
excludedExtensions.add(clazz);
111+
}
112+
return included;
113+
}) //
98114
.forEach(extensionRegistry::registerAutoDetectedExtension);
115+
116+
logExcludedExtensions(excludedExtensions);
117+
}
118+
119+
private static void logExcludedExtensions(List<Class<? extends Extension>> excludedExtensions) {
120+
if (!excludedExtensions.isEmpty()) {
121+
// @formatter:off
122+
List<String> excludeExtensionNames = excludedExtensions
123+
.stream()
124+
.map(Class::getName)
125+
.collect(Collectors.toList());
126+
// @formatter:on
127+
logger.config(() -> String.format(
128+
"Excluded auto-detected extensions due to configured includes/excludes: %s", excludeExtensionNames));
129+
}
99130
}
100131

101132
/**

junit-platform-commons/junit-platform-commons.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ tasks.jar {
3030
tasks.codeCoverageClassesJar {
3131
exclude("org/junit/platform/commons/util/ModuleUtils.class")
3232
exclude("org/junit/platform/commons/util/PackageNameUtils.class")
33+
exclude("org/junit/platform/commons/util/ServiceLoaderUtils.class")
3334
}
3435

3536
eclipse {

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

+5-3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ private ClassNamePatternFilterUtils() {
4343

4444
public static final String ALL_PATTERN = "*";
4545

46+
public static final String BLANK = "";
47+
4648
/**
4749
* Create a {@link Predicate} that can be used to exclude (i.e., filter out)
4850
* objects of type {@code T} whose fully qualified class names match any of
@@ -101,16 +103,16 @@ private static <T> Predicate<T> matchingClasses(String patterns, Function<T, Str
101103
}
102104

103105
private static <T> Predicate<T> createPredicateFromPatterns(String patterns, Function<T, String> classNameProvider,
104-
FilterType mode) {
106+
FilterType type) {
105107
if (ALL_PATTERN.equals(patterns)) {
106-
return __ -> mode == FilterType.INCLUDE;
108+
return type == FilterType.INCLUDE ? __ -> true : __ -> false;
107109
}
108110

109111
List<Pattern> patternList = convertToRegularExpressions(patterns);
110112
return object -> {
111113
boolean isMatchingAnyPattern = patternList.stream().anyMatch(
112114
pattern -> pattern.matcher(classNameProvider.apply(object)).matches());
113-
return (mode == FilterType.INCLUDE) == isMatchingAnyPattern;
115+
return (type == FilterType.INCLUDE) == isMatchingAnyPattern;
114116
};
115117
}
116118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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.commons.util;
12+
13+
import java.util.ServiceLoader;
14+
import java.util.function.Predicate;
15+
import java.util.stream.Stream;
16+
import java.util.stream.StreamSupport;
17+
18+
import org.apiguardian.api.API;
19+
20+
/**
21+
* Collection of utilities for working with {@link ServiceLoader}.
22+
*
23+
* <h2>DISCLAIMER</h2>
24+
*
25+
* <p>These utilities are intended solely for usage within the JUnit framework
26+
* itself. <strong>Any usage by external parties is not supported.</strong>
27+
* Use at your own risk!
28+
*
29+
* @since 5.11
30+
*/
31+
@API(status = API.Status.INTERNAL, since = "5.11")
32+
public class ServiceLoaderUtils {
33+
34+
private ServiceLoaderUtils() {
35+
/* no-op */
36+
}
37+
38+
/**
39+
* Filters the supplied service loader using the supplied predicate.
40+
*
41+
* @param <T> the type of the service
42+
* @param serviceLoader the service loader to be filtered
43+
* @param providerPredicate the predicate to filter the loaded services
44+
* @return a stream of loaded services that match the predicate
45+
*/
46+
public static <T> Stream<T> filter(ServiceLoader<T> serviceLoader,
47+
Predicate<? super Class<? extends T>> providerPredicate) {
48+
return StreamSupport.stream(serviceLoader.spliterator(), false).filter(it -> {
49+
@SuppressWarnings("unchecked")
50+
Class<? extends T> type = (Class<? extends T>) it.getClass();
51+
return providerPredicate.test(type);
52+
});
53+
}
54+
55+
}

0 commit comments

Comments
 (0)