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