package org.apache.maven.surefire.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.shared.utils.StringUtils; import org.apache.maven.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.shared.utils.StringUtils.isBlank; import static org.apache.maven.shared.utils.io.MatchPatterns.from; import static org.apache.maven.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX; import static org.apache.maven.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX; import static org.apache.maven.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 ); } }