| 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 org.apache.maven.surefire.shared.utils.StringUtils; |
| import org.apache.maven.surefire.shared.utils.io.MatchPatterns; |
| |
| import java.util.regex.Pattern; |
| |
| import static java.io.File.separatorChar; |
| import static java.util.regex.Pattern.compile; |
| import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank; |
| import static org.apache.maven.surefire.shared.utils.io.MatchPatterns.from; |
| 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 org.apache.maven.surefire.shared.utils.io.SelectorUtils.matchPath; |
| |
| /** |
| * Single pattern test filter resolved from multi pattern filter -Dtest=MyTest#test,AnotherTest#otherTest. |
| * @deprecated will be renamed to ResolvedTestPattern |
| */ |
| // will be renamed to ResolvedTestPattern |
| @Deprecated |
| public final class ResolvedTest |
| { |
| /** |
| * Type of patterns in ResolvedTest constructor. |
| */ |
| public enum Type |
| { |
| CLASS, METHOD |
| } |
| |
| private static final String CLASS_FILE_EXTENSION = ".class"; |
| |
| private static final String JAVA_FILE_EXTENSION = ".java"; |
| |
| private static final String WILDCARD_PATH_PREFIX = "**/"; |
| |
| private static final String WILDCARD_FILENAME_POSTFIX = ".*"; |
| |
| private final String classPattern; |
| |
| private final String methodPattern; |
| |
| private final boolean isRegexTestClassPattern; |
| |
| private final boolean isRegexTestMethodPattern; |
| |
| private final String description; |
| |
| private final ClassMatcher classMatcher = new ClassMatcher(); |
| |
| private final MethodMatcher methodMatcher = new MethodMatcher(); |
| |
| /** |
| * '*' means zero or more characters<br> |
| * '?' means one and only one character |
| * The pattern %regex[] prefix and suffix does not appear. The regex <i>pattern</i> is always |
| * unwrapped by the caller. |
| * |
| * @param classPattern test class file pattern |
| * @param methodPattern test method |
| * @param isRegex {@code true} if pattern is regex |
| */ |
| public ResolvedTest( String classPattern, String methodPattern, boolean isRegex ) |
| { |
| classPattern = tryBlank( classPattern ); |
| methodPattern = tryBlank( methodPattern ); |
| description = description( classPattern, methodPattern, isRegex ); |
| |
| if ( isRegex && classPattern != null ) |
| { |
| classPattern = wrapRegex( classPattern ); |
| } |
| |
| if ( isRegex && methodPattern != null ) |
| { |
| methodPattern = wrapRegex( methodPattern ); |
| } |
| |
| this.classPattern = reformatClassPattern( classPattern, isRegex ); |
| this.methodPattern = methodPattern; |
| isRegexTestClassPattern = isRegex; |
| isRegexTestMethodPattern = isRegex; |
| methodMatcher.sanityCheck(); |
| } |
| |
| /** |
| * The regex {@code pattern} is always unwrapped. |
| * |
| * @param type class or method |
| * @param pattern pattern or regex |
| * @param isRegex {@code true} if pattern is regex |
| */ |
| public ResolvedTest( Type type, String pattern, boolean isRegex ) |
| { |
| pattern = tryBlank( pattern ); |
| final boolean isClass = type == Type.CLASS; |
| description = description( isClass ? pattern : null, !isClass ? pattern : null, isRegex ); |
| if ( isRegex && pattern != null ) |
| { |
| pattern = wrapRegex( pattern ); |
| } |
| classPattern = isClass ? reformatClassPattern( pattern, isRegex ) : null; |
| methodPattern = !isClass ? pattern : null; |
| isRegexTestClassPattern = isRegex && isClass; |
| isRegexTestMethodPattern = isRegex && !isClass; |
| methodMatcher.sanityCheck(); |
| } |
| |
| /** |
| * Test class file pattern, e.g. org/**/Cat*.class<br>, or null if not any |
| * and {@link #hasTestClassPattern()} returns false. |
| * Other examples: org/animals/Cat*, org/animals/Ca?.class, %regex[Cat.class|Dog.*]<br> |
| * <br> |
| * '*' means zero or more characters<br> |
| * '?' means one and only one character |
| * |
| * @return class pattern or regex |
| */ |
| public String getTestClassPattern() |
| { |
| return classPattern; |
| } |
| |
| public boolean hasTestClassPattern() |
| { |
| return classPattern != null; |
| } |
| |
| /** |
| * Test method, e.g. "realTestMethod".<br>, or null if not any and {@link #hasTestMethodPattern()} returns false. |
| * Other examples: test* or testSomethin? or %regex[testOne|testTwo] or %ant[testOne|testTwo]<br> |
| * <br> |
| * '*' means zero or more characters<br> |
| * '?' means one and only one character |
| * |
| * @return method pattern or regex |
| */ |
| public String getTestMethodPattern() |
| { |
| return methodPattern; |
| } |
| |
| public boolean hasTestMethodPattern() |
| { |
| return methodPattern != null; |
| } |
| |
| public boolean isRegexTestClassPattern() |
| { |
| return isRegexTestClassPattern; |
| } |
| |
| public boolean isRegexTestMethodPattern() |
| { |
| return isRegexTestMethodPattern; |
| } |
| |
| public boolean isEmpty() |
| { |
| return classPattern == null && methodPattern == null; |
| } |
| |
| public boolean matchAsInclusive( String testClassFile, String methodName ) |
| { |
| testClassFile = tryBlank( testClassFile ); |
| methodName = tryBlank( methodName ); |
| |
| return isEmpty() || alwaysInclusiveQuietly( testClassFile ) || match( testClassFile, methodName ); |
| } |
| |
| public boolean matchAsExclusive( String testClassFile, String methodName ) |
| { |
| testClassFile = tryBlank( testClassFile ); |
| methodName = tryBlank( methodName ); |
| |
| return !isEmpty() && canMatchExclusive( testClassFile, methodName ) && match( testClassFile, methodName ); |
| } |
| |
| @Override |
| public boolean equals( Object o ) |
| { |
| if ( this == o ) |
| { |
| return true; |
| } |
| if ( o == null || getClass() != o.getClass() ) |
| { |
| return false; |
| } |
| |
| ResolvedTest that = (ResolvedTest) o; |
| |
| return ( classPattern == null ? that.classPattern == null : classPattern.equals( that.classPattern ) ) |
| && ( methodPattern == null ? that.methodPattern == null : methodPattern.equals( that.methodPattern ) ); |
| } |
| |
| @Override |
| public int hashCode() |
| { |
| int result = classPattern != null ? classPattern.hashCode() : 0; |
| result = 31 * result + ( methodPattern != null ? methodPattern.hashCode() : 0 ); |
| return result; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return isEmpty() ? "" : description; |
| } |
| |
| private static String description( String clazz, String method, boolean isRegex ) |
| { |
| String description; |
| if ( clazz == null && method == null ) |
| { |
| description = null; |
| } |
| else if ( clazz == null ) |
| { |
| description = "#" + method; |
| } |
| else if ( method == null ) |
| { |
| description = clazz; |
| } |
| else |
| { |
| description = clazz + "#" + method; |
| } |
| return isRegex && description != null ? wrapRegex( description ) : description; |
| } |
| |
| private boolean canMatchExclusive( String testClassFile, String methodName ) |
| { |
| return canMatchExclusiveMethods( testClassFile, methodName ) |
| || canMatchExclusiveClasses( testClassFile, methodName ) |
| || canMatchExclusiveAll( testClassFile, methodName ); |
| } |
| |
| private boolean canMatchExclusiveMethods( String testClassFile, String methodName ) |
| { |
| return testClassFile == null && methodName != null && classPattern == null && methodPattern != null; |
| } |
| |
| private boolean canMatchExclusiveClasses( String testClassFile, String methodName ) |
| { |
| return testClassFile != null && methodName == null && classPattern != null && methodPattern == null; |
| } |
| |
| private boolean canMatchExclusiveAll( String testClassFile, String methodName ) |
| { |
| return testClassFile != null && methodName != null && ( classPattern != null || methodPattern != null ); |
| } |
| |
| /** |
| * Prevents {@link #match(String, String)} from throwing NPE in situations when inclusive returns true. |
| * |
| * @param testClassFile path to class file |
| * @return {@code true} if examined class in null and class pattern exists |
| */ |
| private boolean alwaysInclusiveQuietly( String testClassFile ) |
| { |
| return testClassFile == null && classPattern != null; |
| } |
| |
| private boolean match( String testClassFile, String methodName ) |
| { |
| return matchClass( testClassFile ) && matchMethod( methodName ); |
| } |
| |
| private boolean matchClass( String testClassFile ) |
| { |
| return classPattern == null || classMatcher.matchTestClassFile( testClassFile ); |
| } |
| |
| private boolean matchMethod( String methodName ) |
| { |
| return methodPattern == null || methodName == null || methodMatcher.matchMethodName( methodName ); |
| } |
| |
| private static String tryBlank( String s ) |
| { |
| if ( s == null ) |
| { |
| return null; |
| } |
| else |
| { |
| String trimmed = s.trim(); |
| return StringUtils.isEmpty( trimmed ) ? null : trimmed; |
| } |
| } |
| |
| private static String reformatClassPattern( String s, boolean isRegex ) |
| { |
| if ( s != null && !isRegex ) |
| { |
| String path = convertToPath( s ); |
| path = fromFullyQualifiedClass( path ); |
| if ( path != null && !path.startsWith( WILDCARD_PATH_PREFIX ) ) |
| { |
| path = WILDCARD_PATH_PREFIX + path; |
| } |
| return path; |
| } |
| else |
| { |
| return s; |
| } |
| } |
| |
| private static String convertToPath( String className ) |
| { |
| if ( isBlank( className ) ) |
| { |
| return null; |
| } |
| else |
| { |
| if ( className.endsWith( JAVA_FILE_EXTENSION ) ) |
| { |
| className = className.substring( 0, className.length() - JAVA_FILE_EXTENSION.length() ) |
| + CLASS_FILE_EXTENSION; |
| } |
| return className; |
| } |
| } |
| |
| static String wrapRegex( String unwrapped ) |
| { |
| return REGEX_HANDLER_PREFIX + unwrapped + PATTERN_HANDLER_SUFFIX; |
| } |
| |
| static String fromFullyQualifiedClass( String cls ) |
| { |
| if ( cls.endsWith( CLASS_FILE_EXTENSION ) ) |
| { |
| String className = cls.substring( 0, cls.length() - CLASS_FILE_EXTENSION.length() ); |
| return className.replace( '.', '/' ) + CLASS_FILE_EXTENSION; |
| } |
| else if ( !cls.contains( "/" ) ) |
| { |
| if ( cls.endsWith( WILDCARD_FILENAME_POSTFIX ) ) |
| { |
| String clsName = cls.substring( 0, cls.length() - WILDCARD_FILENAME_POSTFIX.length() ); |
| return clsName.contains( "." ) ? clsName.replace( '.', '/' ) + WILDCARD_FILENAME_POSTFIX : cls; |
| } |
| else |
| { |
| return cls.replace( '.', '/' ); |
| } |
| } |
| else |
| { |
| return cls; |
| } |
| } |
| |
| private final class ClassMatcher |
| { |
| private volatile MatchPatterns cache; |
| |
| boolean matchTestClassFile( String testClassFile ) |
| { |
| return ResolvedTest.this.isRegexTestClassPattern() |
| ? matchClassRegexPatter( testClassFile ) |
| : matchClassPatter( testClassFile ); |
| } |
| |
| private MatchPatterns of( String... sources ) |
| { |
| if ( cache == null ) |
| { |
| try |
| { |
| checkIllegalCharacters( sources ); |
| cache = from( sources ); |
| } |
| catch ( IllegalArgumentException e ) |
| { |
| throwSanityError( e ); |
| } |
| } |
| return cache; |
| } |
| |
| private boolean matchClassPatter( String testClassFile ) |
| { |
| //@todo We have to use File.separator only because the MatchPatterns is using it internally - cannot override. |
| String classPattern = ResolvedTest.this.classPattern; |
| if ( separatorChar != '/' ) |
| { |
| testClassFile = testClassFile.replace( '/', separatorChar ); |
| classPattern = classPattern.replace( '/', separatorChar ); |
| } |
| |
| if ( classPattern.endsWith( WILDCARD_FILENAME_POSTFIX ) || classPattern.endsWith( CLASS_FILE_EXTENSION ) ) |
| { |
| return of( classPattern ).matches( testClassFile, true ); |
| } |
| else |
| { |
| String[] classPatterns = { classPattern + CLASS_FILE_EXTENSION, classPattern }; |
| return of( classPatterns ).matches( testClassFile, true ); |
| } |
| } |
| |
| private boolean matchClassRegexPatter( String testClassFile ) |
| { |
| String realFile = separatorChar == '/' ? testClassFile : testClassFile.replace( '/', separatorChar ); |
| return of( classPattern ).matches( realFile, true ); |
| } |
| } |
| |
| private final class MethodMatcher |
| { |
| private volatile Pattern cache; |
| |
| boolean matchMethodName( String methodName ) |
| { |
| if ( ResolvedTest.this.isRegexTestMethodPattern() ) |
| { |
| fetchCache(); |
| return cache.matcher( methodName ) |
| .matches(); |
| } |
| else |
| { |
| return matchPath( ResolvedTest.this.methodPattern, methodName ); |
| } |
| } |
| |
| void sanityCheck() |
| { |
| if ( ResolvedTest.this.isRegexTestMethodPattern() && ResolvedTest.this.hasTestMethodPattern() ) |
| { |
| try |
| { |
| checkIllegalCharacters( ResolvedTest.this.methodPattern ); |
| fetchCache(); |
| } |
| catch ( IllegalArgumentException e ) |
| { |
| throwSanityError( e ); |
| } |
| } |
| } |
| |
| private void fetchCache() |
| { |
| if ( cache == null ) |
| { |
| int from = REGEX_HANDLER_PREFIX.length(); |
| int to = ResolvedTest.this.methodPattern.length() - PATTERN_HANDLER_SUFFIX.length(); |
| String pattern = ResolvedTest.this.methodPattern.substring( from, to ); |
| cache = compile( pattern ); |
| } |
| } |
| } |
| |
| private static void checkIllegalCharacters( String... expressions ) |
| { |
| for ( String expression : expressions ) |
| { |
| if ( expression.contains( "#" ) ) |
| { |
| throw new IllegalArgumentException( "Extra '#' in regex: " + expression ); |
| } |
| } |
| } |
| |
| private static void throwSanityError( IllegalArgumentException e ) |
| { |
| throw new IllegalArgumentException( "%regex[] usage rule violation, valid regex rules:\n" |
| + " * <classNameRegex>#<methodNameRegex> - " |
| + "where both regex can be individually evaluated as a regex\n" |
| + " * you may use at most 1 '#' to in one regex filter. " |
| + e.getLocalizedMessage(), e ); |
| } |
| } |