Skip to content

Commit

Permalink
Add include/exclude-based filtering for auto-detected extensions (jun…
Browse files Browse the repository at this point in the history
…it-team#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 junit-team#3717.

---------

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
YongGoose and marcphilipp authored Dec 16, 2024
1 parent efa1527 commit 16c6f72
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 25 deletions.
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

0 comments on commit 16c6f72

Please sign in to comment.