From 2b90fd011d57000fb8400eb64561a8551ad7511a Mon Sep 17 00:00:00 2001 From: "Y.H. Kuo" Date: Sun, 21 May 2023 22:36:34 +0800 Subject: [PATCH] Introduce method filter support in the ConsoleLauncher Issue: #3107 --- .../release-notes-5.10.0-M1.adoc | 4 + .../console/options/TestDiscoveryOptions.java | 18 ++ .../options/TestDiscoveryOptionsMixin.java | 12 ++ .../tasks/DiscoveryRequestCreator.java | 10 + .../launcher/AbstractMethodFilter.java | 64 +++++++ .../launcher/ExcludeMethodFilter.java | 59 ++++++ .../launcher/IncludeMethodFilter.java | 58 ++++++ .../junit/platform/launcher/MethodFilter.java | 107 +++++++++++ .../ConsoleLauncherIntegrationTests.java | 15 ++ .../CommandLineOptionsParsingTests.java | 42 +++++ .../tasks/DiscoveryRequestCreatorTests.java | 17 ++ .../platform/launcher/MethodFilterTests.java | 172 ++++++++++++++++++ 12 files changed, 578 insertions(+) create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/AbstractMethodFilter.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/ExcludeMethodFilter.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/IncludeMethodFilter.java create mode 100644 junit-platform-launcher/src/main/java/org/junit/platform/launcher/MethodFilter.java create mode 100644 platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc index cb26ac56a9dc..a7ee9f035839 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.10.0-M1.adoc @@ -83,6 +83,10 @@ repository on GitHub. `java`, and `jdk` packages by default. This feature can be disabled or configured to prune other calls via configurations parameters. Please refer to the <<../user-guide/index.adoc#stacktrace-pruning, User Guide>> for details. +* New `--exclude-methodname` and `--include-methodname` options added to the +`ConsoleLauncher` to include or exclude methods based on fully qualified method names +without parameters. For example, `--exclude-methodname=^org\.example\..+#methodname` +will exclude all methods called `methodName` under package `org.example`. [[release-notes-5.10.0-M1-junit-jupiter]] diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java index b7949de159f6..9fdb2d02c396 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java @@ -61,6 +61,8 @@ public class TestDiscoveryOptions { private List excludedClassNamePatterns = emptyList(); private List includedPackages = emptyList(); private List excludedPackages = emptyList(); + private List includedMethodNamePatterns = emptyList(); + private List excludedMethodNamePatterns = emptyList(); private List includedEngines = emptyList(); private List excludedEngines = emptyList(); private List includedTagExpressions = emptyList(); @@ -222,6 +224,22 @@ public void setExcludedPackages(List excludedPackages) { this.excludedPackages = excludedPackages; } + public List getIncludedMethodNamePatterns() { + return includedMethodNamePatterns; + } + + public void setIncludedMethodNamePatterns(List includedMethodNamePatterns) { + this.includedMethodNamePatterns = includedMethodNamePatterns; + } + + public List getExcludedMethodNamePatterns() { + return excludedMethodNamePatterns; + } + + public void setExcludedMethodNamePatterns(List excludedMethodNamePatterns) { + this.excludedMethodNamePatterns = excludedMethodNamePatterns; + } + public List getIncludedEngines() { return this.includedEngines; } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java index 2a8e7751e3a1..617db8caa8fa 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java @@ -190,6 +190,16 @@ static class FilterOptions { @Option(names = { "-exclude-package" }, arity = "1", hidden = true) private List excludePackages2 = new ArrayList<>(); + @Option(names = { + "--include-methodname" }, paramLabel = "PATTERN", arity = "1", description = "Provide a regular expression to include only methods whose fully qualified names without parameters match. " // + + "When this option is repeated, all patterns will be combined using OR semantics.") + private List includeMethodNamePatterns = new ArrayList<>(); + + @Option(names = { + "--exclude-methodname" }, paramLabel = "PATTERN", arity = "1", description = "Provide a regular expression to exclude those methods whose fully qualified names without parameters match. " // + + "When this option is repeated, all patterns will be combined using OR semantics.") + private List excludeMethodNamePatterns = new ArrayList<>(); + @Option(names = { "-t", "--include-tag" }, paramLabel = "TAG", arity = "1", description = "Provide a tag or tag expression to include only tests whose tags match. " + // @@ -227,6 +237,8 @@ private void applyTo(TestDiscoveryOptions result) { result.setExcludedClassNamePatterns(merge(this.excludeClassNamePatterns, this.excludeClassNamePatterns2)); result.setIncludedPackages(merge(this.includePackages, this.includePackages2)); result.setExcludedPackages(merge(this.excludePackages, this.excludePackages2)); + result.setIncludedMethodNamePatterns(new ArrayList<>(this.includeMethodNamePatterns)); + result.setExcludedMethodNamePatterns(new ArrayList<>(this.excludeMethodNamePatterns)); result.setIncludedTagExpressions(merge(this.includedTags, this.includedTags2)); result.setExcludedTagExpressions(merge(this.excludedTags, this.excludedTags2)); result.setIncludedEngines(merge(this.includedEngines, this.includedEngines2)); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java index 89aeec8c3817..a1b9447383ee 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/DiscoveryRequestCreator.java @@ -18,6 +18,8 @@ import static org.junit.platform.engine.discovery.PackageNameFilter.includePackageNames; import static org.junit.platform.launcher.EngineFilter.excludeEngines; import static org.junit.platform.launcher.EngineFilter.includeEngines; +import static org.junit.platform.launcher.MethodFilter.excludeMethodNamePatterns; +import static org.junit.platform.launcher.MethodFilter.includeMethodNamePatterns; import static org.junit.platform.launcher.TagFilter.excludeTags; import static org.junit.platform.launcher.TagFilter.includeTags; import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; @@ -104,6 +106,14 @@ private void addFilters(LauncherDiscoveryRequestBuilder requestBuilder, TestDisc requestBuilder.filters(excludePackageNames(options.getExcludedPackages())); } + if (!options.getIncludedMethodNamePatterns().isEmpty()) { + requestBuilder.filters(includeMethodNamePatterns(options.getIncludedMethodNamePatterns())); + } + + if (!options.getExcludedMethodNamePatterns().isEmpty()) { + requestBuilder.filters(excludeMethodNamePatterns(options.getExcludedMethodNamePatterns())); + } + if (!options.getIncludedTagExpressions().isEmpty()) { requestBuilder.filters(includeTags(options.getIncludedTagExpressions())); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/AbstractMethodFilter.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/AbstractMethodFilter.java new file mode 100644 index 000000000000..5222a1521725 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/AbstractMethodFilter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015-2023 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.launcher; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.support.descriptor.MethodSource; + +/** + * Abstract {@link MethodFilter} that servers as a superclass + * for filters including or excluding fully qualified method names + * without parameters based on pattern-matching. + * + * @since 1.10 + */ +abstract class AbstractMethodFilter implements MethodFilter { + + protected final List patterns; + protected final String patternDescription; + + AbstractMethodFilter(String... patterns) { + Preconditions.notEmpty(patterns, "patterns array must not be null or empty"); + Preconditions.containsNoNullElements(patterns, "patterns array must not contain null elements"); + this.patterns = Arrays.stream(patterns).map(Pattern::compile).collect(toList()); + this.patternDescription = Arrays.stream(patterns).collect(joining("' OR '", "'", "'")); + } + + protected Optional findMatchingPattern(String methodName) { + if (methodName == null) { + return Optional.empty(); + } + return this.patterns.stream().filter(pattern -> pattern.matcher(methodName).matches()).findAny(); + } + + protected String getFullyQualifiedMethodNameFromDescriptor(TestDescriptor descriptor) { + return descriptor.getSource() // + .filter(source -> source instanceof MethodSource) // + .map(methodSource -> getFullyQualifiedMethodNameWithoutParameters(((MethodSource) methodSource))) // + .orElse(null); + } + + private String getFullyQualifiedMethodNameWithoutParameters(MethodSource methodSource) { + String methodNameWithParentheses = ReflectionUtils.getFullyQualifiedMethodName(methodSource.getJavaClass(), + methodSource.getMethodName(), (Class[]) null); + return methodNameWithParentheses.substring(0, methodNameWithParentheses.length() - 2); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/ExcludeMethodFilter.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/ExcludeMethodFilter.java new file mode 100644 index 000000000000..a33a6dafb83b --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/ExcludeMethodFilter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2023 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.launcher; + +import static org.junit.platform.engine.FilterResult.excluded; +import static org.junit.platform.engine.FilterResult.included; + +import java.util.regex.Pattern; + +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; + +/** + * {@link MethodFilter} that matches fully qualified method names against + * patterns in the form of regular expressions. + * + *

If the fully qualified name of a method matches against at least one + * pattern, the class will be excluded. + * + * @since 1.10 + */ +class ExcludeMethodFilter extends AbstractMethodFilter { + + ExcludeMethodFilter(String... patterns) { + super(patterns); + } + + @Override + public FilterResult apply(TestDescriptor descriptor) { + String methodName = getFullyQualifiedMethodNameFromDescriptor(descriptor); + return findMatchingPattern(methodName) // + .map(pattern -> excluded(formatExclusionReason(methodName, pattern))) // + .orElseGet(() -> included(formatInclusionReason(methodName))); + } + + private String formatInclusionReason(String methodName) { + return String.format("Method name [%s] does not match any excluded pattern: %s", methodName, + patternDescription); + } + + private String formatExclusionReason(String methodName, Pattern pattern) { + return String.format("Method name [%s] matches excluded pattern: '%s'", methodName, pattern); + } + + @Override + public String toString() { + return String.format("%s that excludes method names that match one of the following regular expressions: %s", + getClass().getSimpleName(), patternDescription); + } + +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/IncludeMethodFilter.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/IncludeMethodFilter.java new file mode 100644 index 000000000000..05782b06d751 --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/IncludeMethodFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright 2015-2023 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.launcher; + +import static org.junit.platform.engine.FilterResult.excluded; +import static org.junit.platform.engine.FilterResult.included; + +import java.util.regex.Pattern; + +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; + +/** + * {@link MethodFilter} that matches fully qualified method names against + * patterns in the form of regular expressions. + * + *

If the fully qualified name of a method matches against at least one + * pattern, the method will be included. + * + * @since 1.10 + */ +class IncludeMethodFilter extends AbstractMethodFilter { + + IncludeMethodFilter(String... patterns) { + super(patterns); + } + + @Override + public FilterResult apply(TestDescriptor descriptor) { + String methodName = getFullyQualifiedMethodNameFromDescriptor(descriptor); + return findMatchingPattern(methodName) // + .map(pattern -> included(formatInclusionReason(methodName, pattern))) // + .orElseGet(() -> excluded(formatExclusionReason(methodName))); + } + + private String formatInclusionReason(String methodName, Pattern pattern) { + return String.format("Method name [%s] matches included pattern: '%s'", methodName, pattern); + } + + private String formatExclusionReason(String methodName) { + return String.format("Method name [%s] does not match any included pattern: %s", methodName, + patternDescription); + } + + @Override + public String toString() { + return String.format("%s that includes method names that match one of the following regular expressions: %s", + getClass().getSimpleName(), patternDescription); + } +} diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/MethodFilter.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/MethodFilter.java new file mode 100644 index 000000000000..b453254548df --- /dev/null +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/MethodFilter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015-2023 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.launcher; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.reflect.Method; +import java.util.List; + +import org.apiguardian.api.API; + +/** + * {@link PostDiscoveryFilter} that is applied to the fully qualified + * {@link Method} name without parameters. + * + * @since 1.10 + * @see #includeMethodNamePatterns(String...) + * @see #excludeMethodNamePatterns(String...) + */ +@API(status = EXPERIMENTAL, since = "1.10") +public interface MethodFilter extends PostDiscoveryFilter { + + /** + * Create a new include {@link MethodFilter} based on the + * supplied patterns. + * + *

The patterns are combined using OR semantics, i.e. if the fully + * qualified name of a method matches against at least one of the patterns, + * the method will be included in the result set. + * + * @param patterns regular expressions to match against fully qualified + * method names; never {@code null}, empty, or containing {@code null} + * @see Class#getName() + * @see Method#getName() + * @see #includeMethodNamePatterns(List) + * @see #excludeMethodNamePatterns(String...) + */ + static MethodFilter includeMethodNamePatterns(String... patterns) { + return new IncludeMethodFilter(patterns); + } + + /** + * Create a new include {@link MethodFilter} based on the + * supplied patterns. + * + *

The patterns are combined using OR semantics, i.e. if the fully + * qualified name of a method matches against at least one of the patterns, + * the method will be included in the result set. + * + * @param patterns regular expressions to match against fully qualified + * method names; never {@code null}, empty, or containing {@code null} + * @see Class#getName() + * @see Method#getName() + * @see #includeMethodNamePatterns(String...) + * @see #excludeMethodNamePatterns(String...) + */ + static MethodFilter includeMethodNamePatterns(List patterns) { + return includeMethodNamePatterns(patterns.toArray(new String[0])); + } + + /** + * Create a new exclude {@link MethodFilter} based on the + * supplied patterns. + * + *

The patterns are combined using OR semantics, i.e. if the fully + * qualified name of a method matches against at least one of the patterns, + * the method will be excluded from the result set. + * + * @param patterns regular expressions to match against fully qualified + * method names; never {@code null}, empty, or containing {@code null} + * @see Class#getName() + * @see Method#getName() + * @see #excludeMethodNamePatterns(List) + * @see #includeMethodNamePatterns(String...) + */ + static MethodFilter excludeMethodNamePatterns(String... patterns) { + return new ExcludeMethodFilter(patterns); + } + + /** + * Create a new exclude {@link MethodFilter} based on the + * supplied patterns. + * + *

The patterns are combined using OR semantics, i.e. if the fully + * qualified name of a method matches against at least one of the patterns, + * the method will be excluded from the result set. + * + * @param patterns regular expressions to match against fully qualified + * method names; never {@code null}, empty, or containing {@code null} + * @see Class#getName() + * @see Method#getName() + * @see #excludeMethodNamePatterns(String...) + * @see #includeMethodNamePatterns(String...) + */ + static MethodFilter excludeMethodNamePatterns(List patterns) { + return excludeMethodNamePatterns(patterns.toArray(new String[0])); + } + +} diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java index c325729acf3e..79b12fda1024 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java @@ -50,6 +50,21 @@ void executeWithExcludeClassnameOptionExcludesClasses() { ); } + /** + * @since 1.10 + */ + @Test + void executeWithExcludeMethodNameOptionExcludesMethods() { + String[] args = { "-e", "junit-jupiter", "-p", "org.junit.platform.console.subpackage", "--exclude-methodname", + "^org\\.junit\\.platform\\.console\\.subpackage\\..+#test" }; + var result = new ConsoleLauncherWrapper().execute(args); + assertAll("all subpackage test methods are excluded by the method name filter", // + () -> assertArrayEquals(args, result.args), // + () -> assertEquals(0, result.code), // + () -> assertEquals(0, result.getTestsFoundCount()) // + ); + } + @Test void executeSelectingModuleNames() { String[] args1 = { "-e", "junit-jupiter", "-o", "java.base" }; diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java index 03d2d92e77ee..b10abac0278f 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java @@ -62,6 +62,8 @@ void parseNoArguments() { () -> assertEquals(List.of(), options.discovery.getExcludedClassNamePatterns()), () -> assertEquals(List.of(), options.discovery.getIncludedPackages()), () -> assertEquals(List.of(), options.discovery.getExcludedPackages()), + () -> assertEquals(List.of(), options.discovery.getIncludedMethodNamePatterns()), + () -> assertEquals(List.of(), options.discovery.getExcludedMethodNamePatterns()), () -> assertEquals(List.of(), options.discovery.getIncludedTagExpressions()), () -> assertEquals(List.of(), options.discovery.getExcludedTagExpressions()), () -> assertEquals(List.of(), options.discovery.getAdditionalClasspathEntries()), @@ -196,6 +198,46 @@ void parseValidExcludedPackages(ArgsType type) { // @formatter:on } + @ParameterizedTest + @EnumSource + void parseValidIncludeMethodNamePatterns(ArgsType type) { + // @formatter:off + assertAll( + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--include-methodname .+#method.*").discovery.getIncludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), + type.parseArgLine("--include-methodname .+#methodA.* --include-methodname .+#methodB.*").discovery.getIncludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--include-methodname=.+#method.*").discovery.getIncludedMethodNamePatterns()) + ); + // @formatter:on + } + + @ParameterizedTest + @EnumSource + void parseValidExcludeMethodNamePatterns(ArgsType type) { + // @formatter:off + assertAll( + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--exclude-methodname .+#method.*").discovery.getExcludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#methodA.*", ".+#methodB.*"), + type.parseArgLine("--exclude-methodname .+#methodA.* --exclude-methodname .+#methodB.*").discovery.getExcludedMethodNamePatterns()), + () -> assertEquals(List.of(".+#method.*"), + type.parseArgLine("--exclude-methodname=.+#method.*").discovery.getExcludedMethodNamePatterns()) + ); + // @formatter:on + } + + @Test + void parseInvalidIncludeMethodNamePatterns() { + assertOptionWithMissingRequiredArgumentThrowsException("--include-methodname"); + } + + @Test + void parseInvalidExcludeMethodNamePatterns() { + assertOptionWithMissingRequiredArgumentThrowsException("--exclude-methodname"); + } + @ParameterizedTest @EnumSource void parseValidIncludedTags(ArgsType type) { diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java index 733d35f7f466..a2c88a2b222f 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java @@ -178,6 +178,23 @@ void convertsPackageOptions() { assertExcludes(packageNameFilters.get(1), "org.junit.excluded1"); } + /** + * @since 1.10 + */ + @Test + void convertsMethodNamePatternOptions() { + options.setScanClasspath(true); + options.setIncludedMethodNamePatterns(List.of(".+#foo.*Bar", ".+#toString", ".+#method.*")); + options.setExcludedMethodNamePatterns(List.of(".+#bar.*Foo")); + + var request = convert(); + var methodNameFilters = request.getPostDiscoveryFilters(); + + assertThat(methodNameFilters).hasSize(2); + assertThat(methodNameFilters.get(0).toString()).contains(".+#foo.*Bar", ".+#toString", ".+#method.*"); + assertThat(methodNameFilters.get(1).toString()).contains(".+#bar.*Foo"); + } + @Test void convertsTagOptions() { options.setScanClasspath(true); diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java new file mode 100644 index 000000000000..9f685923fa4c --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/MethodFilterTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2015-2023 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.launcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.launcher.MethodFilter.excludeMethodNamePatterns; +import static org.junit.platform.launcher.MethodFilter.includeMethodNamePatterns; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.DemoMethodTestDescriptor; + +/** + * Unit tests for {@link MethodFilter}. + * + * @since 1.10 + */ +class MethodFilterTests { + private static final String CLASS1_TEST1_NAME = "org.junit.platform.launcher.MethodFilterTests$Class1#test1"; + private static final String CLASS1_TEST2_NAME = "org.junit.platform.launcher.MethodFilterTests$Class1#test2"; + private static final String CLASS2_TEST1_NAME = "org.junit.platform.launcher.MethodFilterTests$Class2#test1"; + private static final String CLASS2_TEST2_NAME = "org.junit.platform.launcher.MethodFilterTests$Class2#test2"; + private static final TestDescriptor CLASS1_TEST1 = methodTestDescriptor("class1", Class1.class, "test1"); + private static final TestDescriptor CLASS1_TEST2 = methodTestDescriptor("class1", Class1.class, "test2"); + private static final TestDescriptor CLASS2_TEST1 = methodTestDescriptor("class2", Class2.class, "test1"); + private static final TestDescriptor CLASS2_TEST2 = methodTestDescriptor("class2", Class2.class, "test2"); + + @Test + void includeMethodNamePatternsChecksPreconditions() { + assertThatThrownBy(() -> includeMethodNamePatterns((String[]) null)) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not be null or empty"); + assertThatThrownBy(() -> includeMethodNamePatterns(new String[0])) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not be null or empty"); + assertThatThrownBy(() -> includeMethodNamePatterns(new String[] { null })) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not contain null elements"); + } + + @Test + void includeSingleMethodNamePattern() { + var regex = "^org\\.junit\\.platform\\.launcher\\.MethodFilterTests\\$Class1#test.*"; + var filter = includeMethodNamePatterns(regex); + + assertIncluded(filter.apply(CLASS1_TEST1), + String.format("Method name [%s] matches included pattern: '%s'", CLASS1_TEST1_NAME, regex)); + assertIncluded(filter.apply(CLASS1_TEST2), + String.format("Method name [%s] matches included pattern: '%s'", CLASS1_TEST2_NAME, regex)); + + assertExcluded(filter.apply(CLASS2_TEST1), + String.format("Method name [%s] does not match any included pattern: '%s'", CLASS2_TEST1_NAME, regex)); + assertExcluded(filter.apply(CLASS2_TEST2), + String.format("Method name [%s] does not match any included pattern: '%s'", CLASS2_TEST2_NAME, regex)); + } + + @Test + void includeMultipleMethodNamePatterns() { + var firstRegex = "^org\\.junit\\.platform\\.launcher\\.MethodFilterTests\\$Class1#test.*"; + var secondRegex = ".+Class.+#test1"; + var filter = includeMethodNamePatterns(firstRegex, secondRegex); + + assertIncluded(filter.apply(CLASS1_TEST1), + String.format("Method name [%s] matches included pattern: '%s'", CLASS1_TEST1_NAME, firstRegex)); + assertIncluded(filter.apply(CLASS1_TEST2), + String.format("Method name [%s] matches included pattern: '%s'", CLASS1_TEST2_NAME, firstRegex)); + assertIncluded(filter.apply(CLASS2_TEST1), + String.format("Method name [%s] matches included pattern: '%s'", CLASS2_TEST1_NAME, secondRegex)); + + assertExcluded(filter.apply(CLASS2_TEST2), + String.format("Method name [%s] does not match any included pattern: '%s' OR '%s'", CLASS2_TEST2_NAME, + firstRegex, secondRegex)); + } + + @Test + void excludeMethodNamePatternsChecksPreconditions() { + assertThatThrownBy(() -> excludeMethodNamePatterns((String[]) null)) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not be null or empty"); + assertThatThrownBy(() -> excludeMethodNamePatterns(new String[0])) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not be null or empty"); + assertThatThrownBy(() -> excludeMethodNamePatterns(new String[] { null })) // + .isInstanceOf(PreconditionViolationException.class) // + .hasMessage("patterns array must not contain null elements"); + } + + @Test + void excludeSingleMethodNamePattern() { + var regex = "^org\\.junit\\.platform\\.launcher\\.MethodFilterTests\\$Class1#test.*"; + var filter = excludeMethodNamePatterns(regex); + + assertExcluded(filter.apply(CLASS1_TEST1), + String.format("Method name [%s] matches excluded pattern: '%s'", CLASS1_TEST1_NAME, regex)); + assertExcluded(filter.apply(CLASS1_TEST2), + String.format("Method name [%s] matches excluded pattern: '%s'", CLASS1_TEST2_NAME, regex)); + + assertIncluded(filter.apply(CLASS2_TEST1), + String.format("Method name [%s] does not match any excluded pattern: '%s'", CLASS2_TEST1_NAME, regex)); + assertIncluded(filter.apply(CLASS2_TEST2), + String.format("Method name [%s] does not match any excluded pattern: '%s'", CLASS2_TEST2_NAME, regex)); + } + + @Test + void excludeMultipleMethodNamePatterns() { + var firstRegex = "^org\\.junit\\.platform\\.launcher\\.MethodFilterTests\\$Class1#test.*"; + var secondRegex = ".+Class.+#test1"; + var filter = excludeMethodNamePatterns(firstRegex, secondRegex); + + assertExcluded(filter.apply(CLASS1_TEST1), + String.format("Method name [%s] matches excluded pattern: '%s'", CLASS1_TEST1_NAME, firstRegex)); + assertExcluded(filter.apply(CLASS1_TEST2), + String.format("Method name [%s] matches excluded pattern: '%s'", CLASS1_TEST2_NAME, firstRegex)); + assertExcluded(filter.apply(CLASS2_TEST1), + String.format("Method name [%s] matches excluded pattern: '%s'", CLASS2_TEST1_NAME, secondRegex)); + + assertIncluded(filter.apply(CLASS2_TEST2), + String.format("Method name [%s] does not match any excluded pattern: '%s' OR '%s'", CLASS2_TEST2_NAME, + firstRegex, secondRegex)); + } + + private void assertIncluded(FilterResult filterResult, String expectedReason) { + assertTrue(filterResult.included()); + assertThat(filterResult.getReason()).isPresent().contains(expectedReason); + } + + private void assertExcluded(FilterResult filterResult, String excludedPattern) { + assertTrue(filterResult.excluded()); + assertThat(filterResult.getReason()).isPresent().contains(excludedPattern); + } + + private static TestDescriptor methodTestDescriptor(String uniqueId, Class testClass, String methodName) { + var method = ReflectionUtils.findMethod(testClass, methodName, new Class[0]).orElseThrow(); + return new DemoMethodTestDescriptor(UniqueId.root("method", uniqueId), testClass, method); + } + + // ------------------------------------------------------------------------- + + private static class Class1 { + @Test + void test1() { + } + + @Test + void test2() { + } + } + + private static class Class2 { + @Test + void test1() { + } + + @Test + void test2() { + } + } +}