Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Implement include and exclude filtering when calling registerAutoDetectedExtensions in MutableExtensionRegistry #4120

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cfb6845
Enable specific global extension in JUnit 5
YongGoose Nov 12, 2024
07a6ae3
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Nov 14, 2024
4a896f1
Add Java 9+ variant for optimized ServiceLoader filtering
YongGoose Nov 14, 2024
7c58f66
Merge branch 'main' into feature/enable-specific-global-extensions-in…
YongGoose Nov 16, 2024
09dca93
Remove final keyword
YongGoose Nov 18, 2024
355e113
Merge branch 'main' into feature/enable-specific-global-extensions-in…
YongGoose Nov 18, 2024
2568ffe
Avoid instantiating deactivated TestExecutionListeners
marcphilipp Nov 19, 2024
d5a3511
Pass ServiceLoader to Utils to avoid failing its caller check
marcphilipp Nov 19, 2024
a599008
Merge pull request #1 from junit-team/marc/avoid-deactivated-test-exe…
YongGoose Nov 19, 2024
8b2f41e
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Nov 19, 2024
eb32053
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Nov 19, 2024
1597992
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Nov 19, 2024
eb1b5b3
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Nov 19, 2024
f86c1d6
Add constants and javadoc for supported syntax
YongGoose Nov 19, 2024
02c91b5
move code to JupiterConfiguration
YongGoose Nov 19, 2024
3c95ac9
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/co…
YongGoose Nov 27, 2024
624760b
Rename method
YongGoose Nov 27, 2024
6c5d6c3
Add javadoc
YongGoose Nov 27, 2024
13645f0
Update user-guide
YongGoose Nov 27, 2024
9daa88a
Merge branch 'main' into feature/enable-specific-global-extensions-in…
YongGoose Nov 29, 2024
f870747
Log excluded classes once
YongGoose Nov 30, 2024
671e387
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/ex…
YongGoose Dec 5, 2024
87ac022
Merge branch 'main' into feature/enable-specific-global-extensions-in…
YongGoose Dec 5, 2024
a190de6
Apply spotless
YongGoose Dec 5, 2024
13c1b19
Update user-guide
YongGoose Dec 5, 2024
6e5b161
Document new configuration parameters
marcphilipp Dec 6, 2024
ec13b6d
Add integration test
YongGoose Dec 14, 2024
ff30825
Merge remote-tracking branch 'origin/feature/enable-specific-global-e…
YongGoose Dec 14, 2024
4b3c670
Avoid 2nd loop and loading all extensions
marcphilipp Dec 16, 2024
6279713
Polish tests
marcphilipp Dec 16, 2024
93f0e2f
Improve readability
marcphilipp Dec 16, 2024
f92b14d
Return "constant" predicates for default case
marcphilipp Dec 16, 2024
fbefdc9
Remove orphaned anchor
marcphilipp Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,23 @@ When auto-detection is enabled, extensions discovered via the `{ServiceLoader}`
will be added to the extension registry after JUnit Jupiter's global extensions (e.g.,
support for `TestInfo`, `TestReporter`, etc.).

[[extensions-registration-automatic-filtering]]
===== Filtering Auto-detected Extensions

The list of auto-detected extensions can be filtered using include and exclude patterns
via the following <<running-tests-config-params, configuration parameters>>:

`junit.jupiter.extensions.autodetection.include=<patterns>`::
Comma-separated list of _include_ patterns for auto-detected extensions.
`junit.jupiter.extensions.autodetection.exclude=<patterns>`::
Comma-separated list of _exclude_ patterns for auto-detected extensions.

Include patterns are applied _before_ exclude patterns. If both include and exclude
patterns are provided, only extensions that match at least one include pattern and do not
match any exclude pattern will be auto-detected.

See <<running-tests-config-params-deactivation-pattern>> for details on the pattern syntax.

[[extensions-registration-inheritance]]
==== Extension Inheritance

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,7 @@ parameters_ used for the following features.
- <<extensions-conditions-deactivation>>
- <<launcher-api-listeners-custom-deactivation>>
- <<stacktrace-pruning>>
- <<extensions-registration-automatic-filtering>>

If the value for the given _configuration parameter_ consists solely of an asterisk
(`+++*+++`), the pattern will match against all candidate classes. Otherwise, the value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,80 @@
@API(status = STABLE, since = "5.0")
public final class Constants {

/**
* Property name used to include patterns for auto-detecting extensions: {@value}
*
* <h4>Pattern Matching Syntax</h4>
*
* <p>If the property value consists solely of an asterisk ({@code *}), all
* extensions will be included. Otherwise, the property value will be treated
* as a comma-separated list of patterns where each individual pattern will be
* matched against the fully qualified class name (<em>FQCN</em>) of each extension.
* Any dot ({@code .}) in a pattern will match against a dot ({@code .})
* or a dollar sign ({@code $}) in a FQCN. Any asterisk ({@code *}) will match
* against one or more characters in a FQCN. All other characters in a pattern
* will be matched one-to-one against a FQCN.
*
* <h4>Examples</h4>
*
* <ul>
* <li>{@code *}: includes all extensions.
* <li>{@code org.junit.*}: includes every extension under the {@code org.junit}
* base package and any of its subpackages.
* <li>{@code *.MyExtension}: includes every extension whose simple class name is
* exactly {@code MyExtension}.
* <li>{@code *System*}: includes every extension whose FQCN contains
* {@code System}.
* <li>{@code *System*, *Dev*}: includes every extension whose FQCN contains
* {@code System} or {@code Dev}.
* <li>{@code org.example.MyExtension, org.example.TheirExtension}: includes
* extensions whose FQCN is exactly {@code org.example.MyExtension} or
* {@code org.example.TheirExtension}.
* </ul>
*
* <p>Note: A class that matches both an inclusion and exclusion pattern will be excluded.
*
* @see JupiterConfiguration#EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME
*/
public static final String EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME;

/**
* Property name used to exclude patterns for auto-detecting extensions: {@value}
*
* <h4>Pattern Matching Syntax</h4>
*
* <p>If the property value consists solely of an asterisk ({@code *}), all
* extensions will be excluded. Otherwise, the property value will be treated
* as a comma-separated list of patterns where each individual pattern will be
* matched against the fully qualified class name (<em>FQCN</em>) of each extension.
* Any dot ({@code .}) in a pattern will match against a dot ({@code .})
* or a dollar sign ({@code $}) in a FQCN. Any asterisk ({@code *}) will match
* against one or more characters in a FQCN. All other characters in a pattern
* will be matched one-to-one against a FQCN.
*
* <h4>Examples</h4>
*
* <ul>
* <li>{@code *}: excludes all extensions.
* <li>{@code org.junit.*}: excludes every extension under the {@code org.junit}
* base package and any of its subpackages.
* <li>{@code *.MyExtension}: excludes every extension whose simple class name is
* exactly {@code MyExtension}.
* <li>{@code *System*}: excludes every extension whose FQCN contains
* {@code System}.
* <li>{@code *System*, *Dev*}: excludes every extension whose FQCN contains
* {@code System} or {@code Dev}.
* <li>{@code org.example.MyExtension, org.example.TheirExtension}: excludes
* extensions whose FQCN is exactly {@code org.example.MyExtension} or
* {@code org.example.TheirExtension}.
* </ul>
*
* <p>Note: A class that matches both an inclusion and exclusion pattern will be excluded.
*
* @see JupiterConfiguration#EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME
*/
public static final String EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME;

/**
* Property name used to provide patterns for deactivating conditions: {@value}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDirFactory;
Expand All @@ -47,6 +48,11 @@ public CachingJupiterConfiguration(JupiterConfiguration delegate) {
this.delegate = delegate;
}

@Override
public Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions() {
return delegate.getFilterForAutoDetectedExtensions();
}

@Override
public Optional<String> getRawConfigurationParameter(String key) {
return delegate.getRawConfigurationParameter(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDirFactory;
Expand Down Expand Up @@ -77,6 +78,25 @@ public DefaultJupiterConfiguration(ConfigurationParameters configurationParamete
this.outputDirectoryProvider = outputDirectoryProvider;
}

@Override
public Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions() {
String includePattern = getExtensionAutoDetectionIncludePattern();
String excludePattern = getExtensionAutoDetectionExcludePattern();
Predicate<String> predicate = ClassNamePatternFilterUtils.includeMatchingClassNames(includePattern) //
.and(ClassNamePatternFilterUtils.excludeMatchingClassNames(excludePattern));
return clazz -> predicate.test(clazz.getName());
}

private String getExtensionAutoDetectionIncludePattern() {
return configurationParameters.get(EXTENSIONS_AUTODETECTION_INCLUDE_PROPERTY_NAME) //
.orElse(ClassNamePatternFilterUtils.ALL_PATTERN);
}

private String getExtensionAutoDetectionExcludePattern() {
return configurationParameters.get(EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME) //
.orElse(ClassNamePatternFilterUtils.BLANK);
}

@Override
public Optional<String> getRawConfigurationParameter(String key) {
return configurationParameters.get(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.PreInterruptCallback;
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
import org.junit.jupiter.api.io.CleanupMode;
Expand All @@ -37,6 +38,8 @@
@API(status = INTERNAL, since = "5.4")
public interface JupiterConfiguration {

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

Predicate<Class<? extends Extension>> getFilterForAutoDetectedExtensions();

Optional<String> getRawConfigurationParameter(String key);

<T> Optional<T> getRawConfigurationParameter(String key, Function<String, T> transformer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apiguardian.api.API;
Expand All @@ -38,6 +40,7 @@
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.util.ClassLoaderUtils;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ServiceLoaderUtils;

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

if (configuration.isExtensionAutoDetectionEnabled()) {
registerAutoDetectedExtensions(extensionRegistry);
registerAutoDetectedExtensions(extensionRegistry, configuration);
}

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

private static void registerAutoDetectedExtensions(MutableExtensionRegistry extensionRegistry) {
ServiceLoader.load(Extension.class, ClassLoaderUtils.getDefaultClassLoader())//
private static void registerAutoDetectedExtensions(MutableExtensionRegistry extensionRegistry,
JupiterConfiguration configuration) {

Predicate<Class<? extends Extension>> filter = configuration.getFilterForAutoDetectedExtensions();
List<Class<? extends Extension>> excludedExtensions = new ArrayList<>();

ServiceLoader<Extension> serviceLoader = ServiceLoader.load(Extension.class,
ClassLoaderUtils.getDefaultClassLoader());
ServiceLoaderUtils.filter(serviceLoader, clazz -> {
boolean included = filter.test(clazz);
if (!included) {
excludedExtensions.add(clazz);
}
return included;
}) //
.forEach(extensionRegistry::registerAutoDetectedExtension);

logExcludedExtensions(excludedExtensions);
}

private static void logExcludedExtensions(List<Class<? extends Extension>> excludedExtensions) {
if (!excludedExtensions.isEmpty()) {
// @formatter:off
List<String> excludeExtensionNames = excludedExtensions
.stream()
.map(Class::getName)
.collect(Collectors.toList());
// @formatter:on
logger.config(() -> String.format(
"Excluded auto-detected extensions due to configured includes/excludes: %s", excludeExtensionNames));
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions junit-platform-commons/junit-platform-commons.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ tasks.jar {
tasks.codeCoverageClassesJar {
exclude("org/junit/platform/commons/util/ModuleUtils.class")
exclude("org/junit/platform/commons/util/PackageNameUtils.class")
exclude("org/junit/platform/commons/util/ServiceLoaderUtils.class")
}

eclipse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ private ClassNamePatternFilterUtils() {

public static final String ALL_PATTERN = "*";

public static final String BLANK = "";

/**
* Create a {@link Predicate} that can be used to exclude (i.e., filter out)
* objects of type {@code T} whose fully qualified class names match any of
Expand Down Expand Up @@ -101,16 +103,16 @@ private static <T> Predicate<T> matchingClasses(String patterns, Function<T, Str
}

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

List<Pattern> patternList = convertToRegularExpressions(patterns);
return object -> {
boolean isMatchingAnyPattern = patternList.stream().anyMatch(
pattern -> pattern.matcher(classNameProvider.apply(object)).matches());
return (mode == FilterType.INCLUDE) == isMatchingAnyPattern;
return (type == FilterType.INCLUDE) == isMatchingAnyPattern;
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.commons.util;

import java.util.ServiceLoader;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apiguardian.api.API;

/**
* Collection of utilities for working with {@link ServiceLoader}.
*
* <h2>DISCLAIMER</h2>
*
* <p>These utilities are intended solely for usage within the JUnit framework
* itself. <strong>Any usage by external parties is not supported.</strong>
* Use at your own risk!
*
* @since 5.11
*/
@API(status = API.Status.INTERNAL, since = "5.11")
public class ServiceLoaderUtils {

private ServiceLoaderUtils() {
/* no-op */
}

/**
* Filters the supplied service loader using the supplied predicate.
*
* @param <T> the type of the service
* @param serviceLoader the service loader to be filtered
* @param providerPredicate the predicate to filter the loaded services
* @return a stream of loaded services that match the predicate
*/
public static <T> Stream<T> filter(ServiceLoader<T> serviceLoader,
Predicate<? super Class<? extends T>> providerPredicate) {
return StreamSupport.stream(serviceLoader.spliterator(), false).filter(it -> {
@SuppressWarnings("unchecked")
Class<? extends T> type = (Class<? extends T>) it.getClass();
return providerPredicate.test(type);
});
}

}
Loading
Loading