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