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 java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.LinkedHashSet;
26  import java.util.Set;
27  
28  import static java.util.Collections.unmodifiableSet;
29  import static org.apache.maven.shared.utils.StringUtils.isBlank;
30  import static org.apache.maven.shared.utils.StringUtils.isNotBlank;
31  import static org.apache.maven.shared.utils.StringUtils.split;
32  import static org.apache.maven.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX;
33  import static org.apache.maven.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX;
34  import static java.util.Collections.singleton;
35  import static org.apache.maven.surefire.testset.ResolvedTest.Type.CLASS;
36  import static org.apache.maven.surefire.testset.ResolvedTest.Type.METHOD;
37  
38  // TODO In Surefire 3.0 see SUREFIRE-1309 and use normal fully qualified class name regex instead.
39  /**
40   * Resolved multi pattern filter e.g. -Dtest=MyTest#test,!AnotherTest#otherTest into an object model
41   * composed of included and excluded tests.<br>
42   * The methods {@link #shouldRun(String, String)} are filters easily used in JUnit filter or TestNG.
43   * This class is independent of JUnit and TestNG API.<br>
44   * It is accessed by Java Reflection API in {@link org.apache.maven.surefire.booter.SurefireReflector}
45   * using specific ClassLoader.
46   */
47  public class TestListResolver
48      implements GenericTestPattern<ResolvedTest, String, String>
49  {
50      private static final String JAVA_CLASS_FILE_EXTENSION = ".class";
51  
52      private static final TestListResolver WILDCARD = new TestListResolver( "*" + JAVA_CLASS_FILE_EXTENSION );
53  
54      private static final TestListResolver EMPTY = new TestListResolver( "" );
55  
56      private final Set<ResolvedTest> includedPatterns;
57  
58      private final Set<ResolvedTest> excludedPatterns;
59  
60      private final boolean hasIncludedMethodPatterns;
61  
62      private final boolean hasExcludedMethodPatterns;
63  
64      public TestListResolver( Collection<String> tests )
65      {
66          final IncludedExcludedPatterns patterns = new IncludedExcludedPatterns();
67          final Set<ResolvedTest> includedFilters = new LinkedHashSet<ResolvedTest>( 0 );
68          final Set<ResolvedTest> excludedFilters = new LinkedHashSet<ResolvedTest>( 0 );
69  
70          for ( final String csvTests : tests )
71          {
72              if ( isNotBlank( csvTests ) )
73              {
74                  for ( String request : split( csvTests, "," ) )
75                  {
76                      request = request.trim();
77                      if ( !request.isEmpty() && !request.equals( "!" ) )
78                      {
79                          resolveTestRequest( request, patterns, includedFilters, excludedFilters );
80                      }
81                  }
82              }
83          }
84  
85          this.includedPatterns = unmodifiableSet( includedFilters );
86          this.excludedPatterns = unmodifiableSet( excludedFilters );
87          this.hasIncludedMethodPatterns = patterns.hasIncludedMethodPatterns;
88          this.hasExcludedMethodPatterns = patterns.hasExcludedMethodPatterns;
89      }
90  
91      public TestListResolver( String csvTests )
92      {
93          this( csvTests == null ? Collections.<String>emptySet() : singleton( csvTests ) );
94      }
95  
96      public TestListResolver( Collection<String> included, Collection<String> excluded )
97      {
98          this( mergeIncludedAndExcludedTests( included, excluded ) );
99      }
100 
101     /**
102      * Used only in method filter.
103      */
104     private TestListResolver( boolean hasIncludedMethodPatterns, boolean hasExcludedMethodPatterns,
105                               Set<ResolvedTest> includedPatterns, Set<ResolvedTest> excludedPatterns )
106     {
107         this.includedPatterns = includedPatterns;
108         this.excludedPatterns = excludedPatterns;
109         this.hasIncludedMethodPatterns = hasIncludedMethodPatterns;
110         this.hasExcludedMethodPatterns = hasExcludedMethodPatterns;
111     }
112 
113     public static TestListResolver newTestListResolver( Set<ResolvedTest> includedPatterns,
114                                                         Set<ResolvedTest> excludedPatterns )
115     {
116         return new TestListResolver( haveMethodPatterns( includedPatterns ), haveMethodPatterns( excludedPatterns ),
117                                      includedPatterns, excludedPatterns );
118     }
119 
120     @Override
121     public boolean hasIncludedMethodPatterns()
122     {
123         return hasIncludedMethodPatterns;
124     }
125 
126     @Override
127     public boolean hasExcludedMethodPatterns()
128     {
129         return hasExcludedMethodPatterns;
130     }
131 
132     @Override
133     public boolean hasMethodPatterns()
134     {
135         return hasIncludedMethodPatterns() || hasExcludedMethodPatterns();
136     }
137 
138     /**
139      *
140      * @param resolver    filter possibly having method patterns
141      * @return {@code resolver} if {@link TestListResolver#hasMethodPatterns() resolver.hasMethodPatterns()}
142      * returns {@code true}; Otherwise wildcard filter {@code *.class} is returned.
143      */
144     public static TestListResolver optionallyWildcardFilter( TestListResolver resolver )
145     {
146         return resolver.hasMethodPatterns() ? resolver : WILDCARD;
147     }
148 
149     public static TestListResolver getWildcard()
150     {
151         return WILDCARD;
152     }
153 
154     public static TestListResolver getEmptyTestListResolver()
155     {
156         return EMPTY;
157     }
158 
159     public final boolean isWildcard()
160     {
161         return equals( WILDCARD );
162     }
163 
164     public TestFilter<String, String> and( final TestListResolver another )
165     {
166         return new TestFilter<String, String>()
167         {
168             @Override
169             public boolean shouldRun( String testClass, String methodName )
170             {
171                 return TestListResolver.this.shouldRun( testClass, methodName )
172                     && another.shouldRun( testClass, methodName );
173             }
174         };
175     }
176 
177     public TestFilter<String, String> or( final TestListResolver another )
178     {
179         return new TestFilter<String, String>()
180         {
181             @Override
182             public boolean shouldRun( String testClass, String methodName )
183             {
184                 return TestListResolver.this.shouldRun( testClass, methodName )
185                     || another.shouldRun( testClass, methodName );
186             }
187         };
188     }
189 
190     public boolean shouldRun( Class<?> testClass, String methodName )
191     {
192         return shouldRun( toClassFileName( testClass ), methodName );
193     }
194 
195     /**
196      * Returns {@code true} if satisfies {@code testClassFile} and {@code methodName} filter.
197      *
198      * @param testClassFile format must be e.g. "my/package/MyTest.class" including class extension; or null
199      * @param methodName real test-method name; or null
200      */
201     @Override
202     public boolean shouldRun( String testClassFile, String methodName )
203     {
204         if ( isEmpty() || isBlank( testClassFile ) && isBlank( methodName ) )
205         {
206             return true;
207         }
208         else
209         {
210             boolean shouldRun = false;
211 
212             if ( getIncludedPatterns().isEmpty() )
213             {
214                 shouldRun = true;
215             }
216             else
217             {
218                 for ( ResolvedTest filter : getIncludedPatterns() )
219                 {
220                     if ( filter.matchAsInclusive( testClassFile, methodName ) )
221                     {
222                         shouldRun = true;
223                         break;
224                     }
225                 }
226             }
227 
228             if ( shouldRun )
229             {
230                 for ( ResolvedTest filter : getExcludedPatterns() )
231                 {
232                     if ( filter.matchAsExclusive( testClassFile, methodName ) )
233                     {
234                         shouldRun = false;
235                         break;
236                     }
237                 }
238             }
239             return shouldRun;
240         }
241     }
242 
243     @Override
244     public boolean isEmpty()
245     {
246         return equals( EMPTY );
247     }
248 
249     @Override
250     public String getPluginParameterTest()
251     {
252         String aggregatedTest = aggregatedTest( "", getIncludedPatterns() );
253 
254         if ( isNotBlank( aggregatedTest ) && !getExcludedPatterns().isEmpty() )
255         {
256             aggregatedTest += ", ";
257         }
258 
259         aggregatedTest += aggregatedTest( "!", getExcludedPatterns() );
260         return aggregatedTest.length() == 0 ? "" : aggregatedTest;
261     }
262 
263     @Override
264     public Set<ResolvedTest> getIncludedPatterns()
265     {
266         return includedPatterns;
267     }
268 
269     @Override
270     public Set<ResolvedTest> getExcludedPatterns()
271     {
272         return excludedPatterns;
273     }
274 
275     @Override
276     public boolean equals( Object o )
277     {
278         if ( this == o )
279         {
280             return true;
281         }
282         if ( o == null || getClass() != o.getClass() )
283         {
284             return false;
285         }
286 
287         TestListResolver that = (TestListResolver) o;
288 
289         return getIncludedPatterns().equals( that.getIncludedPatterns() )
290             && getExcludedPatterns().equals( that.getExcludedPatterns() );
291 
292     }
293 
294     @Override
295     public int hashCode()
296     {
297         int result = getIncludedPatterns().hashCode();
298         result = 31 * result + getExcludedPatterns().hashCode();
299         return result;
300     }
301 
302     @Override
303     public String toString()
304     {
305         return getPluginParameterTest();
306     }
307 
308     public static String toClassFileName( Class<?> test )
309     {
310         return test == null ? null : toClassFileName( test.getName() );
311     }
312 
313     public static String toClassFileName( String fullyQualifiedTestClass )
314     {
315         return fullyQualifiedTestClass == null
316             ? null
317             : fullyQualifiedTestClass.replace( '.', '/' ) + JAVA_CLASS_FILE_EXTENSION;
318     }
319 
320     static String removeExclamationMark( String s )
321     {
322         return !s.isEmpty() && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s;
323     }
324 
325     private static void updatedFilters( boolean isExcluded, ResolvedTest test, IncludedExcludedPatterns patterns,
326                                         Collection<ResolvedTest> includedFilters,
327                                         Collection<ResolvedTest> excludedFilters )
328     {
329         if ( isExcluded )
330         {
331             excludedFilters.add( test );
332             patterns.hasExcludedMethodPatterns |= test.hasTestMethodPattern();
333         }
334         else
335         {
336             includedFilters.add( test );
337             patterns.hasIncludedMethodPatterns |= test.hasTestMethodPattern();
338         }
339     }
340 
341     private static String aggregatedTest( String testPrefix, Set<ResolvedTest> tests )
342     {
343         StringBuilder aggregatedTest = new StringBuilder();
344         for ( ResolvedTest test : tests )
345         {
346             String readableTest = test.toString();
347             if ( !readableTest.isEmpty() )
348             {
349                 if ( aggregatedTest.length() != 0 )
350                 {
351                     aggregatedTest.append( ", " );
352                 }
353                 aggregatedTest.append( testPrefix )
354                         .append( readableTest );
355             }
356         }
357         return aggregatedTest.toString();
358     }
359 
360     private static Collection<String> mergeIncludedAndExcludedTests( Collection<String> included,
361                                                                      Collection<String> excluded )
362     {
363         ArrayList<String> incExc = new ArrayList<String>( included );
364         incExc.removeAll( Collections.<String>singleton( null ) );
365         for ( String exc : excluded )
366         {
367             if ( exc != null )
368             {
369                 exc = exc.trim();
370                 if ( !exc.isEmpty() )
371                 {
372                     if ( exc.contains( "!" ) )
373                     {
374                         throw new IllegalArgumentException( "Exclamation mark not expected in 'exclusion': " + exc );
375                     }
376                     exc = exc.replace( ",", ",!" );
377                     if ( !exc.startsWith( "!" ) )
378                     {
379                         exc = "!" + exc;
380                     }
381                     incExc.add( exc );
382                 }
383             }
384         }
385         return incExc;
386     }
387 
388     static boolean isRegexPrefixedPattern( String pattern )
389     {
390         int indexOfRegex = pattern.indexOf( REGEX_HANDLER_PREFIX );
391         int prefixLength = REGEX_HANDLER_PREFIX.length();
392         if ( indexOfRegex != -1 )
393         {
394             if ( indexOfRegex != 0
395                          || !pattern.endsWith( PATTERN_HANDLER_SUFFIX )
396                          || !isRegexMinLength( pattern )
397                          || pattern.indexOf( REGEX_HANDLER_PREFIX, prefixLength ) != -1 )
398             {
399                 String msg = "Illegal test|includes|excludes regex '%s'. Expected %%regex[class#method] "
400                     + "or !%%regex[class#method] " + "with optional class or #method.";
401                 throw new IllegalArgumentException( String.format( msg, pattern ) );
402             }
403             return true;
404         }
405         else
406         {
407             return false;
408         }
409     }
410 
411 
412     static boolean isRegexMinLength( String pattern )
413     {
414         //todo bug in maven-shared-utils: '+1' should not appear in the condition
415         //todo cannot reuse code from SelectorUtils.java because method isRegexPrefixedPattern is in private package.
416         return pattern.length() > REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1;
417     }
418 
419     static String[] unwrapRegex( String regex )
420     {
421         regex = regex.trim();
422         int from = REGEX_HANDLER_PREFIX.length();
423         int to = regex.length() - PATTERN_HANDLER_SUFFIX.length();
424         return unwrap( regex.substring( from, to ) );
425     }
426 
427     static String[] unwrap( final String request )
428     {
429         final String[] classAndMethod = { "", "" };
430         final int indexOfHash = request.indexOf( '#' );
431         if ( indexOfHash == -1 )
432         {
433             classAndMethod[0] = request.trim();
434         }
435         else
436         {
437             classAndMethod[0] = request.substring( 0, indexOfHash ).trim();
438             classAndMethod[1] = request.substring( 1 + indexOfHash ).trim();
439         }
440         return classAndMethod;
441     }
442 
443     static void nonRegexClassAndMethods( String clazz, String methods, boolean isExcluded,
444                          IncludedExcludedPatterns patterns,
445                          Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters )
446     {
447         for ( String method : split( methods, "+" ) )
448         {
449             method = method.trim();
450             ResolvedTest test = new ResolvedTest( clazz, method, false );
451             if ( !test.isEmpty() )
452             {
453                 updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters );
454             }
455         }
456     }
457 
458     /**
459      * Requires trimmed {@code request} been not equal to "!".
460      */
461     static void resolveTestRequest( String request, IncludedExcludedPatterns patterns,
462                                     Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters )
463     {
464         final boolean isExcluded = request.startsWith( "!" );
465         ResolvedTest test = null;
466         request = removeExclamationMark( request );
467         if ( isRegexPrefixedPattern( request ) )
468         {
469             final String[] unwrapped = unwrapRegex( request );
470             final boolean hasClass = !unwrapped[0].isEmpty();
471             final boolean hasMethod = !unwrapped[1].isEmpty();
472             if ( hasClass && hasMethod )
473             {
474                 test = new ResolvedTest( unwrapped[0], unwrapped[1], true );
475             }
476             else if ( hasClass )
477             {
478                 test = new ResolvedTest( CLASS, unwrapped[0], true );
479             }
480             else if ( hasMethod )
481             {
482                 test = new ResolvedTest( METHOD, unwrapped[1], true );
483             }
484         }
485         else
486         {
487             final int indexOfMethodSeparator = request.indexOf( '#' );
488             if ( indexOfMethodSeparator == -1 )
489             {
490                 test = new ResolvedTest( CLASS, request, false );
491             }
492             else
493             {
494                 String clazz = request.substring( 0, indexOfMethodSeparator );
495                 String methods = request.substring( 1 + indexOfMethodSeparator );
496                 nonRegexClassAndMethods( clazz, methods, isExcluded, patterns, includedFilters, excludedFilters );
497             }
498         }
499 
500         if ( test != null && !test.isEmpty() )
501         {
502             updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters );
503         }
504     }
505 
506     private static boolean haveMethodPatterns( Set<ResolvedTest> patterns )
507     {
508         for ( ResolvedTest pattern : patterns )
509         {
510             if ( pattern.hasTestMethodPattern() )
511             {
512                 return true;
513             }
514         }
515         return false;
516     }
517 }