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 <code>pattern</code> 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 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</code> is always unwrapped.
108      */
109     public ResolvedTest( Type type, String pattern, boolean isRegex )
110     {
111         pattern = tryBlank( pattern );
112         final boolean isClass = type == Type.CLASS;
113         description = description( isClass ? pattern : null, !isClass ? pattern : null, isRegex );
114         if ( isRegex && pattern != null )
115         {
116             pattern = wrapRegex( pattern );
117         }
118         classPattern = isClass ? reformatClassPattern( pattern, isRegex ) : null;
119         methodPattern = !isClass ? pattern : null;
120         isRegexTestClassPattern = isRegex && isClass;
121         isRegexTestMethodPattern = isRegex && !isClass;
122         methodMatcher.sanityCheck();
123     }
124 
125     /**
126      * Test class file pattern, e.g. org&#47;**&#47;Cat*.class<br/>, or null if not any
127      * and {@link #hasTestClassPattern()} returns false.
128      * Other examples: org&#47;animals&#47;Cat*, org&#47;animals&#47;Ca?.class, %regex[Cat.class|Dog.*]<br/>
129      * <br/>
130      * '*' means zero or more characters<br>
131      * '?' means one and only one character
132      */
133     public String getTestClassPattern()
134     {
135         return classPattern;
136     }
137 
138     public boolean hasTestClassPattern()
139     {
140         return classPattern != null;
141     }
142 
143     /**
144      * Test method, e.g. "realTestMethod".<br/>, or null if not any and {@link #hasTestMethodPattern()} returns false.
145      * Other examples: test* or testSomethin? or %regex[testOne|testTwo] or %ant[testOne|testTwo]<br/>
146      * <br/>
147      * '*' means zero or more characters<br>
148      * '?' means one and only one character
149      */
150     public String getTestMethodPattern()
151     {
152         return methodPattern;
153     }
154 
155     public boolean hasTestMethodPattern()
156     {
157         return methodPattern != null;
158     }
159 
160     public boolean isRegexTestClassPattern()
161     {
162         return isRegexTestClassPattern;
163     }
164 
165     public boolean isRegexTestMethodPattern()
166     {
167         return isRegexTestMethodPattern;
168     }
169 
170     public boolean isEmpty()
171     {
172         return classPattern == null && methodPattern == null;
173     }
174 
175     public boolean matchAsInclusive( String testClassFile, String methodName )
176     {
177         testClassFile = tryBlank( testClassFile );
178         methodName = tryBlank( methodName );
179 
180         return isEmpty() || alwaysInclusiveQuietly( testClassFile ) || match( testClassFile, methodName );
181     }
182 
183     public boolean matchAsExclusive( String testClassFile, String methodName )
184     {
185         testClassFile = tryBlank( testClassFile );
186         methodName = tryBlank( methodName );
187 
188         return !isEmpty() && canMatchExclusive( testClassFile, methodName ) && match( testClassFile, methodName );
189     }
190 
191     @Override
192     public boolean equals( Object o )
193     {
194         if ( this == o )
195         {
196             return true;
197         }
198         if ( o == null || getClass() != o.getClass() )
199         {
200             return false;
201         }
202 
203         ResolvedTest that = (ResolvedTest) o;
204 
205         return ( classPattern == null ? that.classPattern == null : classPattern.equals( that.classPattern ) )
206             && ( methodPattern == null ? that.methodPattern == null : methodPattern.equals( that.methodPattern ) );
207     }
208 
209     @Override
210     public int hashCode()
211     {
212         int result = classPattern != null ? classPattern.hashCode() : 0;
213         result = 31 * result + ( methodPattern != null ? methodPattern.hashCode() : 0 );
214         return result;
215     }
216 
217     @Override
218     public String toString()
219     {
220         return isEmpty() ? "" : description;
221     }
222 
223     private static String description( String clazz, String method, boolean isRegex )
224     {
225         String description;
226         if ( clazz == null && method == null )
227         {
228             description = null;
229         }
230         else if ( clazz == null )
231         {
232             description = "#" + method;
233         }
234         else if ( method == null )
235         {
236             description = clazz;
237         }
238         else
239         {
240             description = clazz + "#" + method;
241         }
242         return isRegex && description != null ? wrapRegex( description ) : description;
243     }
244 
245     private boolean canMatchExclusive( String testClassFile, String methodName )
246     {
247         return canMatchExclusiveMethods( testClassFile, methodName )
248             || canMatchExclusiveClasses( testClassFile, methodName )
249             || canMatchExclusiveAll( testClassFile, methodName );
250     }
251 
252     private boolean canMatchExclusiveMethods( String testClassFile, String methodName )
253     {
254         return testClassFile == null && methodName != null && classPattern == null && methodPattern != null;
255     }
256 
257     private boolean canMatchExclusiveClasses( String testClassFile, String methodName )
258     {
259         return testClassFile != null && methodName == null && classPattern != null && methodPattern == null;
260     }
261 
262     private boolean canMatchExclusiveAll( String testClassFile, String methodName )
263     {
264         return testClassFile != null && methodName != null && ( classPattern != null || methodPattern != null );
265     }
266 
267     /**
268      * Prevents {@link #match(String, String)} from throwing NPE in situations when inclusive returns true.
269      */
270     private boolean alwaysInclusiveQuietly( String testClassFile )
271     {
272         return testClassFile == null && classPattern != null;
273     }
274 
275     private boolean match( String testClassFile, String methodName )
276     {
277         return matchClass( testClassFile ) && matchMethod( methodName );
278     }
279 
280     private boolean matchClass( String testClassFile )
281     {
282         return classPattern == null || classMatcher.matchTestClassFile( testClassFile );
283     }
284 
285     private boolean matchMethod( String methodName )
286     {
287         return methodPattern == null || methodName == null || methodMatcher.matchMethodName( methodName );
288     }
289 
290     private static String tryBlank( String s )
291     {
292         if ( s == null )
293         {
294             return null;
295         }
296         else
297         {
298             String trimmed = s.trim();
299             return StringUtils.isEmpty( trimmed ) ? null : trimmed;
300         }
301     }
302 
303     private static String reformatClassPattern( String s, boolean isRegex )
304     {
305         if ( s != null && !isRegex )
306         {
307             String path = convertToPath( s );
308             path = fromFullyQualifiedClass( path );
309             if ( path != null && !path.startsWith( WILDCARD_PATH_PREFIX ) )
310             {
311                 path = WILDCARD_PATH_PREFIX + path;
312             }
313             return path;
314         }
315         else
316         {
317             return s;
318         }
319     }
320 
321     private static String convertToPath( String className )
322     {
323         if ( isBlank( className ) )
324         {
325             return null;
326         }
327         else
328         {
329             if ( className.endsWith( JAVA_FILE_EXTENSION ) )
330             {
331                 className = className.substring( 0, className.length() - JAVA_FILE_EXTENSION.length() )
332                                     + CLASS_FILE_EXTENSION;
333             }
334             return className;
335         }
336     }
337 
338     static String wrapRegex( String unwrapped )
339     {
340         return REGEX_HANDLER_PREFIX + unwrapped + PATTERN_HANDLER_SUFFIX;
341     }
342 
343     static String fromFullyQualifiedClass( String cls )
344     {
345         if ( cls.endsWith( CLASS_FILE_EXTENSION ) )
346         {
347             String className = cls.substring( 0, cls.length() - CLASS_FILE_EXTENSION.length() );
348             return className.replace( '.', '/' ) + CLASS_FILE_EXTENSION;
349         }
350         else if ( !cls.contains( "/" ) )
351         {
352             if ( cls.endsWith( WILDCARD_FILENAME_POSTFIX ) )
353             {
354                 String clsName = cls.substring( 0, cls.length() - WILDCARD_FILENAME_POSTFIX.length() );
355                 return clsName.contains( "." ) ? clsName.replace( '.', '/' ) + WILDCARD_FILENAME_POSTFIX : cls;
356             }
357             else
358             {
359                 return cls.replace( '.', '/' );
360             }
361         }
362         else
363         {
364             return cls;
365         }
366     }
367 
368     private final class ClassMatcher
369     {
370         private volatile MatchPatterns cache;
371 
372         boolean matchTestClassFile( String testClassFile )
373         {
374             return ResolvedTest.this.isRegexTestClassPattern()
375                            ? matchClassRegexPatter( testClassFile )
376                            : matchClassPatter( testClassFile );
377         }
378 
379         private MatchPatterns of( String... sources )
380         {
381             if ( cache == null )
382             {
383                 try
384                 {
385                     checkIllegalCharacters( sources );
386                     cache = from( sources );
387                 }
388                 catch ( IllegalArgumentException e )
389                 {
390                     throwSanityError( e );
391                 }
392             }
393             return cache;
394         }
395 
396         private boolean matchClassPatter( String testClassFile )
397         {
398             //@todo We have to use File.separator only because the MatchPatterns is using it internally - cannot override.
399             String classPattern = ResolvedTest.this.classPattern;
400             if ( separatorChar != '/' )
401             {
402                 testClassFile = testClassFile.replace( '/', separatorChar );
403                 classPattern = classPattern.replace( '/', separatorChar );
404             }
405 
406             if ( classPattern.endsWith( WILDCARD_FILENAME_POSTFIX ) || classPattern.endsWith( CLASS_FILE_EXTENSION ) )
407             {
408                 return of( classPattern ).matches( testClassFile, true );
409             }
410             else
411             {
412                 String[] classPatterns = { classPattern + CLASS_FILE_EXTENSION, classPattern };
413                 return of( classPatterns ).matches( testClassFile, true );
414             }
415         }
416 
417         private boolean matchClassRegexPatter( String testClassFile )
418         {
419             String realFile = separatorChar == '/' ? testClassFile : testClassFile.replace( '/', separatorChar );
420             return of( classPattern ).matches( realFile, true );
421         }
422     }
423 
424     private final class MethodMatcher
425     {
426         private volatile Pattern cache;
427 
428         boolean matchMethodName( String methodName )
429         {
430             if ( ResolvedTest.this.isRegexTestMethodPattern() )
431             {
432                 fetchCache();
433                 return cache.matcher( methodName )
434                                .matches();
435             }
436             else
437             {
438                 return matchPath( ResolvedTest.this.methodPattern, methodName );
439             }
440         }
441 
442         void sanityCheck()
443         {
444             if ( ResolvedTest.this.isRegexTestMethodPattern() && ResolvedTest.this.hasTestMethodPattern() )
445             {
446                 try
447                 {
448                     checkIllegalCharacters( ResolvedTest.this.methodPattern );
449                     fetchCache();
450                 }
451                 catch ( IllegalArgumentException e )
452                 {
453                     throwSanityError( e );
454                 }
455             }
456         }
457 
458         private void fetchCache()
459         {
460             if ( cache == null )
461             {
462                 int from = REGEX_HANDLER_PREFIX.length();
463                 int to = ResolvedTest.this.methodPattern.length() - PATTERN_HANDLER_SUFFIX.length();
464                 String pattern = ResolvedTest.this.methodPattern.substring( from, to );
465                 cache = compile( pattern );
466             }
467         }
468     }
469 
470     private static void checkIllegalCharacters( String... expressions )
471     {
472         for ( String expression : expressions )
473         {
474             if ( expression.contains( "#" ) )
475             {
476                 throw new IllegalArgumentException( "Extra '#' in regex: " + expression );
477             }
478         }
479     }
480 
481     private static void throwSanityError( IllegalArgumentException e )
482     {
483         throw new IllegalArgumentException( "%regex[] usage rule violation, valid regex rules:\n"
484                                                     + " * <classNameRegex>#<methodNameRegex> - "
485                                                     + "where both regex can be individually evaluated as a regex\n"
486                                                     + " * you may use at most 1 '#' to in one regex filter. "
487                                                     + e.getLocalizedMessage(), e );
488     }
489 }