diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index d50940991b1c..7ccbf96c4df2 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -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 <>: + +`junit.jupiter.extensions.autodetection.include=`:: + Comma-separated list of _include_ patterns for auto-detected extensions. +`junit.jupiter.extensions.autodetection.exclude=`:: + 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 <> for details on the pattern syntax. + [[extensions-registration-inheritance]] ==== Extension Inheritance diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 734071e821cb..73c1ffc13f47 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -975,6 +975,7 @@ parameters_ used for the following features. - <> - <> - <> +- <> If the value for the given _configuration parameter_ consists solely of an asterisk (`+++*+++`), the pattern will match against all candidate classes. Otherwise, the value diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 500f55818923..204c57c350c2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java @@ -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} + * + *

Pattern Matching Syntax

+ * + *

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 (FQCN) 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. + * + *

Examples

+ * + *
    + *
  • {@code *}: includes all extensions. + *
  • {@code org.junit.*}: includes every extension under the {@code org.junit} + * base package and any of its subpackages. + *
  • {@code *.MyExtension}: includes every extension whose simple class name is + * exactly {@code MyExtension}. + *
  • {@code *System*}: includes every extension whose FQCN contains + * {@code System}. + *
  • {@code *System*, *Dev*}: includes every extension whose FQCN contains + * {@code System} or {@code Dev}. + *
  • {@code org.example.MyExtension, org.example.TheirExtension}: includes + * extensions whose FQCN is exactly {@code org.example.MyExtension} or + * {@code org.example.TheirExtension}. + *
+ * + *

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} + * + *

Pattern Matching Syntax

+ * + *

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 (FQCN) 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. + * + *

Examples

+ * + *
    + *
  • {@code *}: excludes all extensions. + *
  • {@code org.junit.*}: excludes every extension under the {@code org.junit} + * base package and any of its subpackages. + *
  • {@code *.MyExtension}: excludes every extension whose simple class name is + * exactly {@code MyExtension}. + *
  • {@code *System*}: excludes every extension whose FQCN contains + * {@code System}. + *
  • {@code *System*, *Dev*}: excludes every extension whose FQCN contains + * {@code System} or {@code Dev}. + *
  • {@code org.example.MyExtension, org.example.TheirExtension}: excludes + * extensions whose FQCN is exactly {@code org.example.MyExtension} or + * {@code org.example.TheirExtension}. + *
+ * + *

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} * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java index 09a98cd3e4ef..c0c61b2aeae7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java @@ -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; @@ -47,6 +48,11 @@ public CachingJupiterConfiguration(JupiterConfiguration delegate) { this.delegate = delegate; } + @Override + public Predicate> getFilterForAutoDetectedExtensions() { + return delegate.getFilterForAutoDetectedExtensions(); + } + @Override public Optional getRawConfigurationParameter(String key) { return delegate.getRawConfigurationParameter(key); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java index 11d4e46d0c9c..2ba7caf837d0 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java @@ -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; @@ -77,6 +78,25 @@ public DefaultJupiterConfiguration(ConfigurationParameters configurationParamete this.outputDirectoryProvider = outputDirectoryProvider; } + @Override + public Predicate> getFilterForAutoDetectedExtensions() { + String includePattern = getExtensionAutoDetectionIncludePattern(); + String excludePattern = getExtensionAutoDetectionExcludePattern(); + Predicate 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 getRawConfigurationParameter(String key) { return configurationParameters.get(key); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java index ebfe5939e6ca..239c3d40bec3 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java @@ -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; @@ -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; @@ -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> getFilterForAutoDetectedExtensions(); + Optional getRawConfigurationParameter(String key); Optional getRawConfigurationParameter(String key, Function transformer); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index 676598c7abe0..a9c62ca7fa98 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java @@ -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; @@ -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}. @@ -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()) { @@ -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> filter = configuration.getFilterForAutoDetectedExtensions(); + List> excludedExtensions = new ArrayList<>(); + + ServiceLoader 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> excludedExtensions) { + if (!excludedExtensions.isEmpty()) { + // @formatter:off + List 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)); + } } /** diff --git a/junit-platform-commons/junit-platform-commons.gradle.kts b/junit-platform-commons/junit-platform-commons.gradle.kts index ef52ce77ab56..3465b0078020 100644 --- a/junit-platform-commons/junit-platform-commons.gradle.kts +++ b/junit-platform-commons/junit-platform-commons.gradle.kts @@ -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 { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClassNamePatternFilterUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClassNamePatternFilterUtils.java index f9ec3d257baa..9574477400ee 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClassNamePatternFilterUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClassNamePatternFilterUtils.java @@ -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 @@ -101,16 +103,16 @@ private static Predicate matchingClasses(String patterns, Function Predicate createPredicateFromPatterns(String patterns, Function classNameProvider, - FilterType mode) { + FilterType type) { if (ALL_PATTERN.equals(patterns)) { - return __ -> mode == FilterType.INCLUDE; + return type == FilterType.INCLUDE ? __ -> true : __ -> false; } List 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; }; } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java new file mode 100644 index 000000000000..89441d68a3f1 --- /dev/null +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -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}. + * + *

DISCLAIMER

+ * + *

These utilities are intended solely for usage within the JUnit framework + * itself. Any usage by external parties is not supported. + * 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 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 Stream filter(ServiceLoader serviceLoader, + Predicate> providerPredicate) { + return StreamSupport.stream(serviceLoader.spliterator(), false).filter(it -> { + @SuppressWarnings("unchecked") + Class type = (Class) it.getClass(); + return providerPredicate.test(type); + }); + } + +} diff --git a/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java new file mode 100644 index 000000000000..426658a3e2ed --- /dev/null +++ b/junit-platform-commons/src/main/java9/org/junit/platform/commons/util/ServiceLoaderUtils.java @@ -0,0 +1,56 @@ +/* + * 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 org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Collection of utilities for working with {@link ServiceLoader}. + * + *

DISCLAIMER

+ * + *

These utilities are intended solely for usage within the JUnit framework + * itself. Any usage by external parties is not supported. + * Use at your own risk! + * + * @since 5.11 + */ +@API(status = Status.INTERNAL, since = "5.11") +public class ServiceLoaderUtils { + + private ServiceLoaderUtils() { + /* no-op */ + } + + /** + * Filters the supplied service loader using the supplied predicate. + * + * @param 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 Stream filter(ServiceLoader serviceLoader, + Predicate> providerPredicate) { + // @formatter:off + return serviceLoader + .stream() + .filter(provider -> providerPredicate.test(provider.type())) + .map(ServiceLoader.Provider::get); + // @formatter:on + } + +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java index a84296db623b..5feaf010ad2a 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java @@ -19,8 +19,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; +import java.util.function.Predicate; import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; @@ -198,15 +197,12 @@ private static void registerTestExecutionListeners(LauncherConfig config, Launch config.getAdditionalTestExecutionListeners().forEach(launcher::registerTestExecutionListeners); } - private static Stream loadAndFilterTestExecutionListeners( + private static Iterable loadAndFilterTestExecutionListeners( ConfigurationParameters configurationParameters) { - Iterable listeners = ServiceLoaderRegistry.load(TestExecutionListener.class); - String deactivatedListenersPattern = configurationParameters.get( - DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME).orElse(null); - // @formatter:off - return StreamSupport.stream(listeners.spliterator(), false) - .filter(ClassNamePatternFilterUtils.excludeMatchingClasses(deactivatedListenersPattern)); - // @formatter:on + Predicate classNameFilter = configurationParameters.get(DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME) // + .map(ClassNamePatternFilterUtils::excludeMatchingClassNames) // + .orElse(__ -> true); + return ServiceLoaderRegistry.load(TestExecutionListener.class, classNameFilter); } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java index 83866088dcb6..9f33adc15779 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/ServiceLoaderRegistry.java @@ -11,24 +11,55 @@ package org.junit.platform.launcher.core; import static java.util.stream.Collectors.toList; -import static java.util.stream.StreamSupport.stream; +import java.util.ArrayList; +import java.util.List; import java.util.ServiceLoader; +import java.util.function.Function; +import java.util.function.Predicate; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.commons.util.ServiceLoaderUtils; /** * @since 1.8 */ class ServiceLoaderRegistry { - static Iterable load(Class serviceProviderClass) { - Iterable listeners = ServiceLoader.load(serviceProviderClass, ClassLoaderUtils.getDefaultClassLoader()); - getLogger().config(() -> "Loaded " + serviceProviderClass.getSimpleName() + " instances: " - + stream(listeners.spliterator(), false).map(Object::toString).collect(toList())); - return listeners; + static Iterable load(Class type) { + return load(type, __ -> true, instances -> logLoadedInstances(type, instances, null)); + } + + static Iterable load(@SuppressWarnings("SameParameterValue") Class type, + Predicate classNameFilter) { + List exclusions = new ArrayList<>(); + Predicate collectingClassNameFilter = className -> { + boolean included = classNameFilter.test(className); + if (!included) { + exclusions.add(className); + } + return included; + }; + return load(type, collectingClassNameFilter, instances -> logLoadedInstances(type, instances, exclusions)); + } + + private static String logLoadedInstances(Class type, List instances, List exclusions) { + String typeName = type.getSimpleName(); + if (exclusions == null) { + return String.format("Loaded %s instances: %s", typeName, instances); + } + return String.format("Loaded %s instances: %s (excluded classes: %s)", typeName, instances, exclusions); + } + + private static List load(Class type, Predicate classNameFilter, + Function, String> logMessageSupplier) { + ServiceLoader serviceLoader = ServiceLoader.load(type, ClassLoaderUtils.getDefaultClassLoader()); + Predicate> providerPredicate = clazz -> classNameFilter.test(clazz.getName()); + List instances = ServiceLoaderUtils.filter(serviceLoader, providerPredicate).collect(toList()); + getLogger().config(() -> logMessageSupplier.apply(instances)); + return instances; } private static Logger getLogger() { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ConfigLoaderExtension.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ConfigLoaderExtension.java new file mode 100644 index 000000000000..9112e8587a7b --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ConfigLoaderExtension.java @@ -0,0 +1,28 @@ +/* + * 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.jupiter.engine.extension; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * Demo extension for auto-detection of extensions loaded via Java's + * {@link java.util.ServiceLoader} mechanism. + * + * @since 5.11 + */ +public class ConfigLoaderExtension implements BeforeAllCallback { + + @Override + public void beforeAll(ExtensionContext context) { + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java index dd1babf69f0e..06163b90377e 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.platform.commons.util.ClassNamePatternFilterUtils; /** * Tests for the {@link MutableExtensionRegistry}. @@ -60,17 +62,82 @@ void newRegistryWithoutParentHasDefaultExtensions() { void newRegistryWithoutParentHasDefaultExtensionsPlusAutodetectedExtensionsLoadedViaServiceLoader() { when(configuration.isExtensionAutoDetectionEnabled()).thenReturn(true); + when(configuration.getFilterForAutoDetectedExtensions()).thenReturn(__ -> true); registry = createRegistryWithDefaultExtensions(configuration); List extensions = registry.getExtensions(Extension.class); - assertEquals(NUM_DEFAULT_EXTENSIONS + 1, extensions.size()); + assertEquals(NUM_DEFAULT_EXTENSIONS + 2, extensions.size()); + assertDefaultGlobalExtensionsAreRegistered(4); + + assertExtensionRegistered(registry, ServiceLoaderExtension.class); + assertEquals(4, countExtensions(registry, BeforeAllCallback.class)); + } + + @Test + void registryIncludesAndExcludesSpecificAutoDetectedExtensions() { + when(configuration.isExtensionAutoDetectionEnabled()).thenReturn(true); + when(configuration.getFilterForAutoDetectedExtensions()).thenReturn( + extensionFilter(ServiceLoaderExtension.class.getName(), ConfigLoaderExtension.class.getName())); + registry = createRegistryWithDefaultExtensions(configuration); + + List extensions = registry.getExtensions(Extension.class); + + assertEquals(NUM_DEFAULT_EXTENSIONS, extensions.size()); assertDefaultGlobalExtensionsAreRegistered(3); assertExtensionRegistered(registry, ServiceLoaderExtension.class); assertEquals(3, countExtensions(registry, BeforeAllCallback.class)); } + @Test + void registryIncludesAllAutoDetectedExtensionsAndExcludesNone() { + when(configuration.isExtensionAutoDetectionEnabled()).thenReturn(true); + when(configuration.getFilterForAutoDetectedExtensions()).thenReturn(extensionFilter("*", "")); + registry = createRegistryWithDefaultExtensions(configuration); + + List extensions = registry.getExtensions(Extension.class); + + assertEquals(NUM_DEFAULT_EXTENSIONS + 2, extensions.size()); + assertDefaultGlobalExtensionsAreRegistered(4); + + assertExtensionRegistered(registry, ServiceLoaderExtension.class); + assertExtensionRegistered(registry, ConfigLoaderExtension.class); + assertEquals(4, countExtensions(registry, BeforeAllCallback.class)); + } + + @Test + void registryIncludesSpecificAutoDetectedExtensionsAndExcludesAll() { + when(configuration.isExtensionAutoDetectionEnabled()).thenReturn(true); + when(configuration.getFilterForAutoDetectedExtensions()).thenReturn( + extensionFilter(ServiceLoaderExtension.class.getName(), "*")); + registry = createRegistryWithDefaultExtensions(configuration); + + List extensions = registry.getExtensions(Extension.class); + + assertEquals(NUM_CORE_EXTENSIONS, extensions.size()); + assertDefaultGlobalExtensionsAreRegistered(2); + + assertExtensionNotRegistered(registry, ServiceLoaderExtension.class); + assertEquals(2, countExtensions(registry, BeforeAllCallback.class)); + } + + @Test + void registryIncludesAndExcludesSameAutoDetectedExtension() { + when(configuration.isExtensionAutoDetectionEnabled()).thenReturn(true); + when(configuration.getFilterForAutoDetectedExtensions()).thenReturn( + extensionFilter(ServiceLoaderExtension.class.getName(), ServiceLoaderExtension.class.getName())); + registry = createRegistryWithDefaultExtensions(configuration); + + List extensions = registry.getExtensions(Extension.class); + + assertEquals(NUM_CORE_EXTENSIONS, extensions.size()); + assertDefaultGlobalExtensionsAreRegistered(2); + + assertExtensionNotRegistered(registry, ServiceLoaderExtension.class); + assertEquals(2, countExtensions(registry, BeforeAllCallback.class)); + } + @Test void registerExtensionByImplementingClass() { registry.registerExtension(MyExtension.class); @@ -156,6 +223,11 @@ private void assertExtensionRegistered(ExtensionRegistry registry, Class extensionType.getSimpleName() + " should be present"); } + private void assertExtensionNotRegistered(ExtensionRegistry registry, Class extensionType) { + assertTrue(registry.getExtensions(extensionType).isEmpty(), + () -> extensionType.getSimpleName() + " should not be present"); + } + private void assertDefaultGlobalExtensionsAreRegistered() { assertDefaultGlobalExtensionsAreRegistered(2); } @@ -176,6 +248,12 @@ private void assertDefaultGlobalExtensionsAreRegistered(long bacCount) { assertEquals(1, countExtensions(registry, InvocationInterceptor.class)); } + private static Predicate> extensionFilter(String includes, String excludes) { + var nameFilter = ClassNamePatternFilterUtils.includeMatchingClassNames(includes) // + .and(ClassNamePatternFilterUtils.excludeMatchingClassNames(excludes)); + return clazz -> nameFilter.test(clazz.getName()); + } + // ------------------------------------------------------------------------- interface MyExtensionApi extends Extension { diff --git a/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension index 758c3e555809..27a9f87d158c 100644 --- a/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension +++ b/jupiter-tests/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -1 +1,2 @@ org.junit.jupiter.engine.extension.ServiceLoaderExtension +org.junit.jupiter.engine.extension.ConfigLoaderExtension diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java index ed5b7d755c68..ccdd66de0f9a 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java @@ -24,11 +24,14 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.LogRecord; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.logging.LogRecordListener; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestDescriptor; @@ -79,7 +82,8 @@ void testExecutionListenerIsLoadedViaServiceApi() { } @Test - void testExecutionListenersExcludedViaConfigParametersIsNotLoadedViaServiceApi() { + void testExecutionListenersExcludedViaConfigParametersIsNotLoadedViaServiceApi( + @TrackLogRecords LogRecordListener listener) { withTestServices(() -> { var value = "org.junit.*.launcher.listeners.Unused*,org.junit.*.launcher.listeners.AnotherUnused*"; withSystemProperty(DEACTIVATE_LISTENERS_PATTERN_PROPERTY_NAME, value, () -> { @@ -94,6 +98,16 @@ void testExecutionListenersExcludedViaConfigParametersIsNotLoadedViaServiceApi() launcher.execute(request().build()); + var logMessage = listener.stream(ServiceLoaderRegistry.class) // + .map(LogRecord::getMessage) // + .filter(it -> it.startsWith("Loaded TestExecutionListener instances")) // + .findAny(); + assertThat(logMessage).isPresent(); + assertThat(logMessage.get()) // + .contains("NoopTestExecutionListener@") // + .endsWith(" (excluded classes: [" + UnusedTestExecutionListener.class.getName() + ", " + + AnotherUnusedTestExecutionListener.class.getName() + "])"); + assertFalse(UnusedTestExecutionListener.called); assertFalse(AnotherUnusedTestExecutionListener.called); }); diff --git a/platform-tooling-support-tests/projects/standalone/expected-err.txt b/platform-tooling-support-tests/projects/standalone/expected-err.txt index de1899fd021a..53ca33b7f6f0 100644 --- a/platform-tooling-support-tests/projects/standalone/expected-err.txt +++ b/platform-tooling-support-tests/projects/standalone/expected-err.txt @@ -12,7 +12,7 @@ .+ org.junit.platform.launcher.core.ServiceLoaderRegistry load .+ Loaded LauncherDiscoveryListener instances: .. .+ org.junit.platform.launcher.core.ServiceLoaderRegistry load -.+ Loaded TestExecutionListener instances: .+ +.+ Loaded TestExecutionListener instances: .+ \Q(excluded classes: [])\E .+ org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines .+ Discovered TestEngines: - junit-platform-suite .+