View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.api.testset;
20  
21  import java.util.regex.Pattern;
22  
23  import org.apache.maven.surefire.shared.utils.StringUtils;
24  import org.apache.maven.surefire.shared.utils.io.MatchPatterns;
25  
26  import static java.io.File.separatorChar;
27  import static java.util.regex.Pattern.compile;
28  import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
29  import static org.apache.maven.surefire.shared.utils.io.MatchPatterns.from;
30  import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX;
31  import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX;
32  import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.matchPath;
33  
34  /**
35   * Single pattern test filter resolved from multi pattern filter -Dtest=MyTest#test,AnotherTest#otherTest.
36   * @deprecated will be renamed to ResolvedTestPattern
37   */
38  // will be renamed to ResolvedTestPattern
39  @Deprecated
40  public final class ResolvedTest {
41      /**
42       * Type of patterns in ResolvedTest constructor.
43       */
44      public enum Type {
45          CLASS,
46          METHOD
47      }
48  
49      private static final String CLASS_FILE_EXTENSION = ".class";
50  
51      private static final String JAVA_FILE_EXTENSION = ".java";
52  
53      private static final String WILDCARD_PATH_PREFIX = "**/";
54  
55      private static final String WILDCARD_FILENAME_POSTFIX = ".*";
56  
57      private final String classPattern;
58  
59      private final String methodPattern;
60  
61      private final boolean isRegexTestClassPattern;
62  
63      private final boolean isRegexTestMethodPattern;
64  
65      private final String description;
66  
67      private final ClassMatcher classMatcher = new ClassMatcher();
68  
69      private final MethodMatcher methodMatcher = new MethodMatcher();
70  
71      /**
72       * '*' means zero or more characters<br>
73       * '?' means one and only one character
74       * The pattern %regex[] prefix and suffix does not appear. The regex <i>pattern</i> is always
75       * unwrapped by the caller.
76       *
77       * @param classPattern     test class file pattern
78       * @param methodPattern    test method
79       * @param isRegex          {@code true} if pattern is regex
80       */
81      public ResolvedTest(String classPattern, String methodPattern, boolean isRegex) {
82          classPattern = tryBlank(classPattern);
83          methodPattern = tryBlank(methodPattern);
84          description = description(classPattern, methodPattern, isRegex);
85  
86          if (isRegex && classPattern != null) {
87              classPattern = wrapRegex(classPattern);
88          }
89  
90          if (isRegex && methodPattern != null) {
91              methodPattern = wrapRegex(methodPattern);
92          }
93  
94          this.classPattern = reformatClassPattern(classPattern, isRegex);
95          this.methodPattern = methodPattern;
96          isRegexTestClassPattern = isRegex;
97          isRegexTestMethodPattern = isRegex;
98          methodMatcher.sanityCheck();
99      }
100 
101     /**
102      * The regex {@code pattern} is always unwrapped.
103      *
104      * @param type class or method
105      * @param pattern pattern or regex
106      * @param isRegex {@code true} if pattern is regex
107      */
108     public ResolvedTest(Type type, String pattern, boolean isRegex) {
109         pattern = tryBlank(pattern);
110         final boolean isClass = type == Type.CLASS;
111         description = description(isClass ? pattern : null, !isClass ? pattern : null, isRegex);
112         if (isRegex && pattern != null) {
113             pattern = wrapRegex(pattern);
114         }
115         classPattern = isClass ? reformatClassPattern(pattern, isRegex) : null;
116         methodPattern = !isClass ? pattern : null;
117         isRegexTestClassPattern = isRegex && isClass;
118         isRegexTestMethodPattern = isRegex && !isClass;
119         methodMatcher.sanityCheck();
120     }
121 
122     /**
123      * Test class file pattern, e.g. org&#47;**&#47;Cat*.class<br>, or null if not any
124      * and {@link #hasTestClassPattern()} returns false.
125      * Other examples: org&#47;animals&#47;Cat*, org&#47;animals&#47;Ca?.class, %regex[Cat.class|Dog.*]<br>
126      * <br>
127      * '*' means zero or more characters<br>
128      * '?' means one and only one character
129      *
130      * @return class pattern or regex
131      */
132     public String getTestClassPattern() {
133         return classPattern;
134     }
135 
136     public boolean hasTestClassPattern() {
137         return classPattern != null;
138     }
139 
140     /**
141      * Test method, e.g. "realTestMethod".<br>, or null if not any and {@link #hasTestMethodPattern()} returns false.
142      * Other examples: test* or testSomethin? or %regex[testOne|testTwo] or %ant[testOne|testTwo]<br>
143      * <br>
144      * '*' means zero or more characters<br>
145      * '?' means one and only one character
146      *
147      * @return method pattern or regex
148      */
149     public String getTestMethodPattern() {
150         return methodPattern;
151     }
152 
153     public boolean hasTestMethodPattern() {
154         return methodPattern != null;
155     }
156 
157     public boolean isRegexTestClassPattern() {
158         return isRegexTestClassPattern;
159     }
160 
161     public boolean isRegexTestMethodPattern() {
162         return isRegexTestMethodPattern;
163     }
164 
165     public boolean isEmpty() {
166         return classPattern == null && methodPattern == null;
167     }
168 
169     public boolean matchAsInclusive(String testClassFile, String methodName) {
170         testClassFile = tryBlank(testClassFile);
171         methodName = tryBlank(methodName);
172 
173         return isEmpty() || alwaysInclusiveQuietly(testClassFile) || match(testClassFile, methodName);
174     }
175 
176     public boolean matchAsExclusive(String testClassFile, String methodName) {
177         testClassFile = tryBlank(testClassFile);
178         methodName = tryBlank(methodName);
179 
180         return !isEmpty() && canMatchExclusive(testClassFile, methodName) && match(testClassFile, methodName);
181     }
182 
183     @Override
184     public boolean equals(Object o) {
185         if (this == o) {
186             return true;
187         }
188         if (o == null || getClass() != o.getClass()) {
189             return false;
190         }
191 
192         ResolvedTest that = (ResolvedTest) o;
193 
194         return (classPattern == null ? that.classPattern == null : classPattern.equals(that.classPattern))
195                 && (methodPattern == null ? that.methodPattern == null : methodPattern.equals(that.methodPattern));
196     }
197 
198     @Override
199     public int hashCode() {
200         int result = classPattern != null ? classPattern.hashCode() : 0;
201         result = 31 * result + (methodPattern != null ? methodPattern.hashCode() : 0);
202         return result;
203     }
204 
205     @Override
206     public String toString() {
207         return isEmpty() ? "" : description;
208     }
209 
210     private static String description(String clazz, String method, boolean isRegex) {
211         String description;
212         if (clazz == null && method == null) {
213             description = null;
214         } else if (clazz == null) {
215             description = "#" + method;
216         } else if (method == null) {
217             description = clazz;
218         } else {
219             description = clazz + "#" + method;
220         }
221         return isRegex && description != null ? wrapRegex(description) : description;
222     }
223 
224     private boolean canMatchExclusive(String testClassFile, String methodName) {
225         return canMatchExclusiveMethods(testClassFile, methodName)
226                 || canMatchExclusiveClasses(testClassFile, methodName)
227                 || canMatchExclusiveAll(testClassFile, methodName);
228     }
229 
230     private boolean canMatchExclusiveMethods(String testClassFile, String methodName) {
231         return testClassFile == null && methodName != null && classPattern == null && methodPattern != null;
232     }
233 
234     private boolean canMatchExclusiveClasses(String testClassFile, String methodName) {
235         return testClassFile != null && methodName == null && classPattern != null && methodPattern == null;
236     }
237 
238     private boolean canMatchExclusiveAll(String testClassFile, String methodName) {
239         return testClassFile != null && methodName != null && (classPattern != null || methodPattern != null);
240     }
241 
242     /**
243      * Prevents {@link #match(String, String)} from throwing NPE in situations when inclusive returns true.
244      *
245      * @param testClassFile    path to class file
246      * @return {@code true} if examined class in null and class pattern exists
247      */
248     private boolean alwaysInclusiveQuietly(String testClassFile) {
249         return testClassFile == null && classPattern != null;
250     }
251 
252     private boolean match(String testClassFile, String methodName) {
253         return matchClass(testClassFile) && matchMethod(methodName);
254     }
255 
256     private boolean matchClass(String testClassFile) {
257         return classPattern == null || classMatcher.matchTestClassFile(testClassFile);
258     }
259 
260     private boolean matchMethod(String methodName) {
261         return methodPattern == null || methodName == null || methodMatcher.matchMethodName(methodName);
262     }
263 
264     private static String tryBlank(String s) {
265         if (s == null) {
266             return null;
267         } else {
268             String trimmed = s.trim();
269             return StringUtils.isEmpty(trimmed) ? null : trimmed;
270         }
271     }
272 
273     private static String reformatClassPattern(String s, boolean isRegex) {
274         if (s != null && !isRegex) {
275             String path = convertToPath(s);
276             path = fromFullyQualifiedClass(path);
277             if (path != null && !path.startsWith(WILDCARD_PATH_PREFIX)) {
278                 path = WILDCARD_PATH_PREFIX + path;
279             }
280             return path;
281         } else {
282             return s;
283         }
284     }
285 
286     private static String convertToPath(String className) {
287         if (isBlank(className)) {
288             return null;
289         } else {
290             if (className.endsWith(JAVA_FILE_EXTENSION)) {
291                 className = className.substring(0, className.length() - JAVA_FILE_EXTENSION.length())
292                         + CLASS_FILE_EXTENSION;
293             }
294             return className;
295         }
296     }
297 
298     static String wrapRegex(String unwrapped) {
299         return REGEX_HANDLER_PREFIX + unwrapped + PATTERN_HANDLER_SUFFIX;
300     }
301 
302     static String fromFullyQualifiedClass(String cls) {
303         if (cls.endsWith(CLASS_FILE_EXTENSION)) {
304             String className = cls.substring(0, cls.length() - CLASS_FILE_EXTENSION.length());
305             return className.replace('.', '/') + CLASS_FILE_EXTENSION;
306         } else if (!cls.contains("/")) {
307             if (cls.endsWith(WILDCARD_FILENAME_POSTFIX)) {
308                 String clsName = cls.substring(0, cls.length() - WILDCARD_FILENAME_POSTFIX.length());
309                 return clsName.contains(".") ? clsName.replace('.', '/') + WILDCARD_FILENAME_POSTFIX : cls;
310             } else {
311                 return cls.replace('.', '/');
312             }
313         } else {
314             return cls;
315         }
316     }
317 
318     private final class ClassMatcher {
319         private volatile MatchPatterns cache;
320 
321         boolean matchTestClassFile(String testClassFile) {
322             return ResolvedTest.this.isRegexTestClassPattern()
323                     ? matchClassRegexPatter(testClassFile)
324                     : matchClassPatter(testClassFile);
325         }
326 
327         private MatchPatterns of(String... sources) {
328             if (cache == null) {
329                 try {
330                     checkIllegalCharacters(sources);
331                     cache = from(sources);
332                 } catch (IllegalArgumentException e) {
333                     throwSanityError(e);
334                 }
335             }
336             return cache;
337         }
338 
339         private boolean matchClassPatter(String testClassFile) {
340             // @todo We have to use File.separator only because the MatchPatterns is using it internally - cannot
341             // override.
342             String classPattern = ResolvedTest.this.classPattern;
343             if (separatorChar != '/') {
344                 testClassFile = testClassFile.replace('/', separatorChar);
345                 classPattern = classPattern.replace('/', separatorChar);
346             }
347 
348             if (classPattern.endsWith(WILDCARD_FILENAME_POSTFIX) || classPattern.endsWith(CLASS_FILE_EXTENSION)) {
349                 return of(classPattern).matches(testClassFile, true);
350             } else {
351                 String[] classPatterns = {classPattern + CLASS_FILE_EXTENSION, classPattern};
352                 return of(classPatterns).matches(testClassFile, true);
353             }
354         }
355 
356         private boolean matchClassRegexPatter(String testClassFile) {
357             String realFile = separatorChar == '/' ? testClassFile : testClassFile.replace('/', separatorChar);
358             return of(classPattern).matches(realFile, true);
359         }
360     }
361 
362     private final class MethodMatcher {
363         private volatile Pattern cache;
364 
365         boolean matchMethodName(String methodName) {
366             if (ResolvedTest.this.isRegexTestMethodPattern()) {
367                 fetchCache();
368                 return cache.matcher(methodName).matches();
369             } else {
370                 return matchPath(ResolvedTest.this.methodPattern, methodName);
371             }
372         }
373 
374         void sanityCheck() {
375             if (ResolvedTest.this.isRegexTestMethodPattern() && ResolvedTest.this.hasTestMethodPattern()) {
376                 try {
377                     checkIllegalCharacters(ResolvedTest.this.methodPattern);
378                     fetchCache();
379                 } catch (IllegalArgumentException e) {
380                     throwSanityError(e);
381                 }
382             }
383         }
384 
385         private void fetchCache() {
386             if (cache == null) {
387                 int from = REGEX_HANDLER_PREFIX.length();
388                 int to = ResolvedTest.this.methodPattern.length() - PATTERN_HANDLER_SUFFIX.length();
389                 String pattern = ResolvedTest.this.methodPattern.substring(from, to);
390                 cache = compile(pattern);
391             }
392         }
393     }
394 
395     private static void checkIllegalCharacters(String... expressions) {
396         for (String expression : expressions) {
397             if (expression.contains("#")) {
398                 throw new IllegalArgumentException("Extra '#' in regex: " + expression);
399             }
400         }
401     }
402 
403     private static void throwSanityError(IllegalArgumentException e) {
404         throw new IllegalArgumentException(
405                 "%regex[] usage rule violation, valid regex rules:\n"
406                         + " * <classNameRegex>#<methodNameRegex> - "
407                         + "where both regex can be individually evaluated as a regex\n"
408                         + " * you may use at most 1 '#' to in one regex filter. "
409                         + e.getLocalizedMessage(),
410                 e);
411     }
412 }