blob: f8ebb1bdbc7d0baa246dc700345aa4267f16e6ac [file] [log] [blame]
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;
}
}