| package org.apache.maven.surefire.api.testset; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedHashSet; |
| import java.util.Set; |
| |
| import static java.util.Collections.unmodifiableSet; |
| import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank; |
| import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank; |
| import static org.apache.maven.surefire.shared.utils.StringUtils.split; |
| import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX; |
| import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX; |
| import static java.util.Collections.singleton; |
| import static org.apache.maven.surefire.api.testset.ResolvedTest.Type.CLASS; |
| import static org.apache.maven.surefire.api.testset.ResolvedTest.Type.METHOD; |
| |
| // TODO In Surefire 3.0 see SUREFIRE-1309 and use normal fully qualified class name regex instead. |
| /** |
| * Resolved multi pattern filter e.g. -Dtest=MyTest#test,!AnotherTest#otherTest into an object model |
| * composed of included and excluded tests.<br> |
| * The methods {@link #shouldRun(String, String)} are filters easily used in JUnit filter or TestNG. |
| * This class is independent of JUnit and TestNG API.<br> |
| * It is accessed by Java Reflection API in {@code org.apache.maven.surefire.booter.SurefireReflector} |
| * using specific ClassLoader. |
| */ |
| public class TestListResolver |
| implements GenericTestPattern<ResolvedTest, String, String> |
| { |
| private static final String JAVA_CLASS_FILE_EXTENSION = ".class"; |
| |
| private static final TestListResolver WILDCARD = new TestListResolver( "*" + JAVA_CLASS_FILE_EXTENSION ); |
| |
| private static final TestListResolver EMPTY = new TestListResolver( "" ); |
| |
| private final Set<ResolvedTest> includedPatterns; |
| |
| private final Set<ResolvedTest> excludedPatterns; |
| |
| private final boolean hasIncludedMethodPatterns; |
| |
| private final boolean hasExcludedMethodPatterns; |
| |
| public TestListResolver( Collection<String> tests ) |
| { |
| final IncludedExcludedPatterns patterns = new IncludedExcludedPatterns(); |
| final Set<ResolvedTest> includedFilters = new LinkedHashSet<>( 0 ); |
| final Set<ResolvedTest> excludedFilters = new LinkedHashSet<>( 0 ); |
| |
| for ( final String csvTests : tests ) |
| { |
| if ( isNotBlank( csvTests ) ) |
| { |
| for ( String request : split( csvTests, "," ) ) |
| { |
| request = request.trim(); |
| if ( !request.isEmpty() && !request.equals( "!" ) ) |
| { |
| resolveTestRequest( request, patterns, includedFilters, excludedFilters ); |
| } |
| } |
| } |
| } |
| |
| this.includedPatterns = unmodifiableSet( includedFilters ); |
| this.excludedPatterns = unmodifiableSet( excludedFilters ); |
| this.hasIncludedMethodPatterns = patterns.hasIncludedMethodPatterns; |
| this.hasExcludedMethodPatterns = patterns.hasExcludedMethodPatterns; |
| } |
| |
| public TestListResolver( String csvTests ) |
| { |
| this( csvTests == null ? Collections.<String>emptySet() : singleton( csvTests ) ); |
| } |
| |
| public TestListResolver( Collection<String> included, Collection<String> excluded ) |
| { |
| this( mergeIncludedAndExcludedTests( included, excluded ) ); |
| } |
| |
| /** |
| * Used only in method filter. |
| */ |
| private TestListResolver( boolean hasIncludedMethodPatterns, boolean hasExcludedMethodPatterns, |
| Set<ResolvedTest> includedPatterns, Set<ResolvedTest> excludedPatterns ) |
| { |
| this.includedPatterns = includedPatterns; |
| this.excludedPatterns = excludedPatterns; |
| this.hasIncludedMethodPatterns = hasIncludedMethodPatterns; |
| this.hasExcludedMethodPatterns = hasExcludedMethodPatterns; |
| } |
| |
| public static TestListResolver newTestListResolver( Set<ResolvedTest> includedPatterns, |
| Set<ResolvedTest> excludedPatterns ) |
| { |
| return new TestListResolver( haveMethodPatterns( includedPatterns ), haveMethodPatterns( excludedPatterns ), |
| includedPatterns, excludedPatterns ); |
| } |
| |
| @Override |
| public boolean hasIncludedMethodPatterns() |
| { |
| return hasIncludedMethodPatterns; |
| } |
| |
| @Override |
| public boolean hasExcludedMethodPatterns() |
| { |
| return hasExcludedMethodPatterns; |
| } |
| |
| @Override |
| public boolean hasMethodPatterns() |
| { |
| return hasIncludedMethodPatterns() || hasExcludedMethodPatterns(); |
| } |
| |
| /** |
| * |
| * @param resolver filter possibly having method patterns |
| * @return {@code resolver} if {@link TestListResolver#hasMethodPatterns() resolver.hasMethodPatterns()} |
| * returns {@code true}; Otherwise wildcard filter {@code *.class} is returned. |
| */ |
| public static TestListResolver optionallyWildcardFilter( TestListResolver resolver ) |
| { |
| return resolver.hasMethodPatterns() ? resolver : WILDCARD; |
| } |
| |
| public static TestListResolver getWildcard() |
| { |
| return WILDCARD; |
| } |
| |
| public static TestListResolver getEmptyTestListResolver() |
| { |
| return EMPTY; |
| } |
| |
| public final boolean isWildcard() |
| { |
| return equals( WILDCARD ); |
| } |
| |
| public TestFilter<String, String> and( final TestListResolver another ) |
| { |
| return new TestFilter<String, String>() |
| { |
| @Override |
| public boolean shouldRun( String testClass, String methodName ) |
| { |
| return TestListResolver.this.shouldRun( testClass, methodName ) |
| && another.shouldRun( testClass, methodName ); |
| } |
| }; |
| } |
| |
| public TestFilter<String, String> or( final TestListResolver another ) |
| { |
| return new TestFilter<String, String>() |
| { |
| @Override |
| public boolean shouldRun( String testClass, String methodName ) |
| { |
| return TestListResolver.this.shouldRun( testClass, methodName ) |
| || another.shouldRun( testClass, methodName ); |
| } |
| }; |
| } |
| |
| public boolean shouldRun( Class<?> testClass, String methodName ) |
| { |
| return shouldRun( toClassFileName( testClass ), methodName ); |
| } |
| |
| /** |
| * Returns {@code true} if satisfies {@code testClassFile} and {@code methodName} filter. |
| * |
| * @param testClassFile format must be e.g. "my/package/MyTest.class" including class extension; or null |
| * @param methodName real test-method name; or null |
| */ |
| @Override |
| public boolean shouldRun( String testClassFile, String methodName ) |
| { |
| if ( isEmpty() || isBlank( testClassFile ) && isBlank( methodName ) ) |
| { |
| return true; |
| } |
| else |
| { |
| boolean shouldRun = false; |
| |
| if ( getIncludedPatterns().isEmpty() ) |
| { |
| shouldRun = true; |
| } |
| else |
| { |
| for ( ResolvedTest filter : getIncludedPatterns() ) |
| { |
| if ( filter.matchAsInclusive( testClassFile, methodName ) ) |
| { |
| shouldRun = true; |
| break; |
| } |
| } |
| } |
| |
| if ( shouldRun ) |
| { |
| for ( ResolvedTest filter : getExcludedPatterns() ) |
| { |
| if ( filter.matchAsExclusive( testClassFile, methodName ) ) |
| { |
| shouldRun = false; |
| break; |
| } |
| } |
| } |
| return shouldRun; |
| } |
| } |
| |
| @Override |
| public boolean isEmpty() |
| { |
| return equals( EMPTY ); |
| } |
| |
| @Override |
| public String getPluginParameterTest() |
| { |
| String aggregatedTest = aggregatedTest( "", getIncludedPatterns() ); |
| |
| if ( isNotBlank( aggregatedTest ) && !getExcludedPatterns().isEmpty() ) |
| { |
| aggregatedTest += ", "; |
| } |
| |
| aggregatedTest += aggregatedTest( "!", getExcludedPatterns() ); |
| return aggregatedTest.isEmpty() ? "" : aggregatedTest; |
| } |
| |
| @Override |
| public Set<ResolvedTest> getIncludedPatterns() |
| { |
| return includedPatterns; |
| } |
| |
| @Override |
| public Set<ResolvedTest> getExcludedPatterns() |
| { |
| return excludedPatterns; |
| } |
| |
| @Override |
| public boolean equals( Object o ) |
| { |
| if ( this == o ) |
| { |
| return true; |
| } |
| if ( o == null || getClass() != o.getClass() ) |
| { |
| return false; |
| } |
| |
| TestListResolver that = (TestListResolver) o; |
| |
| return getIncludedPatterns().equals( that.getIncludedPatterns() ) |
| && getExcludedPatterns().equals( that.getExcludedPatterns() ); |
| |
| } |
| |
| @Override |
| public int hashCode() |
| { |
| int result = getIncludedPatterns().hashCode(); |
| result = 31 * result + getExcludedPatterns().hashCode(); |
| return result; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return getPluginParameterTest(); |
| } |
| |
| public static String toClassFileName( Class<?> test ) |
| { |
| return test == null ? null : toClassFileName( test.getName() ); |
| } |
| |
| public static String toClassFileName( String fullyQualifiedTestClass ) |
| { |
| return fullyQualifiedTestClass == null |
| ? null |
| : fullyQualifiedTestClass.replace( '.', '/' ) + JAVA_CLASS_FILE_EXTENSION; |
| } |
| |
| static String removeExclamationMark( String s ) |
| { |
| return !s.isEmpty() && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s; |
| } |
| |
| private static void updatedFilters( boolean isExcluded, ResolvedTest test, IncludedExcludedPatterns patterns, |
| Collection<ResolvedTest> includedFilters, |
| Collection<ResolvedTest> excludedFilters ) |
| { |
| if ( isExcluded ) |
| { |
| excludedFilters.add( test ); |
| patterns.hasExcludedMethodPatterns |= test.hasTestMethodPattern(); |
| } |
| else |
| { |
| includedFilters.add( test ); |
| patterns.hasIncludedMethodPatterns |= test.hasTestMethodPattern(); |
| } |
| } |
| |
| private static String aggregatedTest( String testPrefix, Set<ResolvedTest> tests ) |
| { |
| StringBuilder aggregatedTest = new StringBuilder(); |
| for ( ResolvedTest test : tests ) |
| { |
| String readableTest = test.toString(); |
| if ( !readableTest.isEmpty() ) |
| { |
| if ( aggregatedTest.length() != 0 ) |
| { |
| aggregatedTest.append( ", " ); |
| } |
| aggregatedTest.append( testPrefix ) |
| .append( readableTest ); |
| } |
| } |
| return aggregatedTest.toString(); |
| } |
| |
| private static Collection<String> mergeIncludedAndExcludedTests( Collection<String> included, |
| Collection<String> excluded ) |
| { |
| ArrayList<String> incExc = new ArrayList<>( included ); |
| incExc.removeAll( Collections.<String>singleton( null ) ); |
| for ( String exc : excluded ) |
| { |
| if ( exc != null ) |
| { |
| exc = exc.trim(); |
| if ( !exc.isEmpty() ) |
| { |
| if ( exc.contains( "!" ) ) |
| { |
| throw new IllegalArgumentException( "Exclamation mark not expected in 'exclusion': " + exc ); |
| } |
| exc = exc.replace( ",", ",!" ); |
| if ( !exc.startsWith( "!" ) ) |
| { |
| exc = "!" + exc; |
| } |
| incExc.add( exc ); |
| } |
| } |
| } |
| return incExc; |
| } |
| |
| static boolean isRegexPrefixedPattern( String pattern ) |
| { |
| int indexOfRegex = pattern.indexOf( REGEX_HANDLER_PREFIX ); |
| int prefixLength = REGEX_HANDLER_PREFIX.length(); |
| if ( indexOfRegex != -1 ) |
| { |
| if ( indexOfRegex != 0 |
| || !pattern.endsWith( PATTERN_HANDLER_SUFFIX ) |
| || !isRegexMinLength( pattern ) |
| || pattern.indexOf( REGEX_HANDLER_PREFIX, prefixLength ) != -1 ) |
| { |
| String msg = "Illegal test|includes|excludes regex '%s'. Expected %%regex[class#method] " |
| + "or !%%regex[class#method] " + "with optional class or #method."; |
| throw new IllegalArgumentException( String.format( msg, pattern ) ); |
| } |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| |
| static boolean isRegexMinLength( String pattern ) |
| { |
| //todo bug in maven-shared-utils: '+1' should not appear in the condition |
| //todo cannot reuse code from SelectorUtils.java because method isRegexPrefixedPattern is in private package. |
| return pattern.length() > REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1; |
| } |
| |
| static String[] unwrapRegex( String regex ) |
| { |
| regex = regex.trim(); |
| int from = REGEX_HANDLER_PREFIX.length(); |
| int to = regex.length() - PATTERN_HANDLER_SUFFIX.length(); |
| return unwrap( regex.substring( from, to ) ); |
| } |
| |
| static String[] unwrap( final String request ) |
| { |
| final String[] classAndMethod = { "", "" }; |
| final int indexOfHash = request.indexOf( '#' ); |
| if ( indexOfHash == -1 ) |
| { |
| classAndMethod[0] = request.trim(); |
| } |
| else |
| { |
| classAndMethod[0] = request.substring( 0, indexOfHash ).trim(); |
| classAndMethod[1] = request.substring( 1 + indexOfHash ).trim(); |
| } |
| return classAndMethod; |
| } |
| |
| static void nonRegexClassAndMethods( String clazz, String methods, boolean isExcluded, |
| IncludedExcludedPatterns patterns, |
| Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters ) |
| { |
| for ( String method : split( methods, "+" ) ) |
| { |
| method = method.trim(); |
| ResolvedTest test = new ResolvedTest( clazz, method, false ); |
| if ( !test.isEmpty() ) |
| { |
| updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters ); |
| } |
| } |
| } |
| |
| /** |
| * Requires trimmed {@code request} been not equal to "!". |
| */ |
| static void resolveTestRequest( String request, IncludedExcludedPatterns patterns, |
| Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters ) |
| { |
| final boolean isExcluded = request.startsWith( "!" ); |
| ResolvedTest test = null; |
| request = removeExclamationMark( request ); |
| if ( isRegexPrefixedPattern( request ) ) |
| { |
| final String[] unwrapped = unwrapRegex( request ); |
| final boolean hasClass = !unwrapped[0].isEmpty(); |
| final boolean hasMethod = !unwrapped[1].isEmpty(); |
| if ( hasClass && hasMethod ) |
| { |
| test = new ResolvedTest( unwrapped[0], unwrapped[1], true ); |
| } |
| else if ( hasClass ) |
| { |
| test = new ResolvedTest( CLASS, unwrapped[0], true ); |
| } |
| else if ( hasMethod ) |
| { |
| test = new ResolvedTest( METHOD, unwrapped[1], true ); |
| } |
| } |
| else |
| { |
| final int indexOfMethodSeparator = request.indexOf( '#' ); |
| if ( indexOfMethodSeparator == -1 ) |
| { |
| test = new ResolvedTest( CLASS, request, false ); |
| } |
| else |
| { |
| String clazz = request.substring( 0, indexOfMethodSeparator ); |
| String methods = request.substring( 1 + indexOfMethodSeparator ); |
| nonRegexClassAndMethods( clazz, methods, isExcluded, patterns, includedFilters, excludedFilters ); |
| } |
| } |
| |
| if ( test != null && !test.isEmpty() ) |
| { |
| updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters ); |
| } |
| } |
| |
| private static boolean haveMethodPatterns( Set<ResolvedTest> patterns ) |
| { |
| for ( ResolvedTest pattern : patterns ) |
| { |
| if ( pattern.hasTestMethodPattern() ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |