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.length() != 0 && !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     public boolean hasIncludedMethodPatterns()
121     {
122         return hasIncludedMethodPatterns;
123     }
124 
125     public boolean hasExcludedMethodPatterns()
126     {
127         return hasExcludedMethodPatterns;
128     }
129 
130     public boolean hasMethodPatterns()
131     {
132         return hasIncludedMethodPatterns() || hasExcludedMethodPatterns();
133     }
134 
135     /**
136      *
137      * @param resolver    filter possibly having method patterns
138      * @return {@code resolver} if {@link TestListResolver#hasMethodPatterns() resolver.hasMethodPatterns()}
139      * returns <tt>true</tt>; Otherwise wildcard filter <em>*.class</em> is returned.
140      */
141     public static TestListResolver optionallyWildcardFilter( TestListResolver resolver )
142     {
143         return resolver.hasMethodPatterns() ? resolver : WILDCARD;
144     }
145 
146     public static TestListResolver getWildcard()
147     {
148         return WILDCARD;
149     }
150 
151     public static TestListResolver getEmptyTestListResolver()
152     {
153         return EMPTY;
154     }
155 
156     public final boolean isWildcard()
157     {
158         return equals( WILDCARD );
159     }
160 
161     public TestFilter<String, String> and( final TestListResolver another )
162     {
163         return new TestFilter<String, String>()
164         {
165             public boolean shouldRun( String testClass, String methodName )
166             {
167                 return TestListResolver.this.shouldRun( testClass, methodName )
168                     && another.shouldRun( testClass, methodName );
169             }
170         };
171     }
172 
173     public TestFilter<String, String> or( final TestListResolver another )
174     {
175         return new TestFilter<String, String>()
176         {
177             public boolean shouldRun( String testClass, String methodName )
178             {
179                 return TestListResolver.this.shouldRun( testClass, methodName )
180                     || another.shouldRun( testClass, methodName );
181             }
182         };
183     }
184 
185     public boolean shouldRun( Class<?> testClass, String methodName )
186     {
187         return shouldRun( toClassFileName( testClass ), methodName );
188     }
189 
190     /**
191      * Returns {@code true} if satisfies {@code testClassFile} and {@code methodName} filter.
192      *
193      * @param testClassFile format must be e.g. "my/package/MyTest.class" including class extension; or null
194      * @param methodName real test-method name; or null
195      */
196     public boolean shouldRun( String testClassFile, String methodName )
197     {
198         if ( isEmpty() || isBlank( testClassFile ) && isBlank( methodName ) )
199         {
200             return true;
201         }
202         else
203         {
204             boolean shouldRun = false;
205 
206             if ( getIncludedPatterns().isEmpty() )
207             {
208                 shouldRun = true;
209             }
210             else
211             {
212                 for ( ResolvedTest filter : getIncludedPatterns() )
213                 {
214                     if ( filter.matchAsInclusive( testClassFile, methodName ) )
215                     {
216                         shouldRun = true;
217                         break;
218                     }
219                 }
220             }
221 
222             if ( shouldRun )
223             {
224                 for ( ResolvedTest filter : getExcludedPatterns() )
225                 {
226                     if ( filter.matchAsExclusive( testClassFile, methodName ) )
227                     {
228                         shouldRun = false;
229                         break;
230                     }
231                 }
232             }
233             return shouldRun;
234         }
235     }
236 
237     public boolean isEmpty()
238     {
239         return equals( EMPTY );
240     }
241 
242     public String getPluginParameterTest()
243     {
244         String aggregatedTest = aggregatedTest( "", getIncludedPatterns() );
245 
246         if ( isNotBlank( aggregatedTest ) && !getExcludedPatterns().isEmpty() )
247         {
248             aggregatedTest += ", ";
249         }
250 
251         aggregatedTest += aggregatedTest( "!", getExcludedPatterns() );
252         return aggregatedTest.length() == 0 ? "" : aggregatedTest;
253     }
254 
255     public Set<ResolvedTest> getIncludedPatterns()
256     {
257         return includedPatterns;
258     }
259 
260     public Set<ResolvedTest> getExcludedPatterns()
261     {
262         return excludedPatterns;
263     }
264 
265     @Override
266     public boolean equals( Object o )
267     {
268         if ( this == o )
269         {
270             return true;
271         }
272         if ( o == null || getClass() != o.getClass() )
273         {
274             return false;
275         }
276 
277         TestListResolver that = (TestListResolver) o;
278 
279         return getIncludedPatterns().equals( that.getIncludedPatterns() )
280             && getExcludedPatterns().equals( that.getExcludedPatterns() );
281 
282     }
283 
284     @Override
285     public int hashCode()
286     {
287         int result = getIncludedPatterns().hashCode();
288         result = 31 * result + getExcludedPatterns().hashCode();
289         return result;
290     }
291 
292     @Override
293     public String toString()
294     {
295         return getPluginParameterTest();
296     }
297 
298     public static String toClassFileName( Class<?> test )
299     {
300         return test == null ? null : toClassFileName( test.getName() );
301     }
302 
303     public static String toClassFileName( String fullyQualifiedTestClass )
304     {
305         return fullyQualifiedTestClass == null
306             ? null
307             : fullyQualifiedTestClass.replace( '.', '/' ) + JAVA_CLASS_FILE_EXTENSION;
308     }
309 
310     static String removeExclamationMark( String s )
311     {
312         return s.length() != 0 && s.charAt( 0 ) == '!' ? s.substring( 1 ) : s;
313     }
314 
315     private static void updatedFilters( boolean isExcluded, ResolvedTest test, IncludedExcludedPatterns patterns,
316                                         Collection<ResolvedTest> includedFilters,
317                                         Collection<ResolvedTest> excludedFilters )
318     {
319         if ( isExcluded )
320         {
321             excludedFilters.add( test );
322             patterns.hasExcludedMethodPatterns |= test.hasTestMethodPattern();
323         }
324         else
325         {
326             includedFilters.add( test );
327             patterns.hasIncludedMethodPatterns |= test.hasTestMethodPattern();
328         }
329     }
330 
331     private static String aggregatedTest( String testPrefix, Set<ResolvedTest> tests )
332     {
333         StringBuilder aggregatedTest = new StringBuilder();
334         for ( ResolvedTest test : tests )
335         {
336             String readableTest = test.toString();
337             if ( readableTest.length() != 0 )
338             {
339                 if ( aggregatedTest.length() != 0 )
340                 {
341                     aggregatedTest.append( ", " );
342                 }
343                 aggregatedTest.append( testPrefix )
344                         .append( readableTest );
345             }
346         }
347         return aggregatedTest.toString();
348     }
349 
350     private static Collection<String> mergeIncludedAndExcludedTests( Collection<String> included,
351                                                                      Collection<String> excluded )
352     {
353         ArrayList<String> incExc = new ArrayList<String>( included );
354         incExc.removeAll( Collections.<String>singleton( null ) );
355         for ( String exc : excluded )
356         {
357             if ( exc != null )
358             {
359                 exc = exc.trim();
360                 if ( exc.length() != 0 )
361                 {
362                     if ( exc.contains( "!" ) )
363                     {
364                         throw new IllegalArgumentException( "Exclamation mark not expected in 'exclusion': " + exc );
365                     }
366                     exc = exc.replace( ",", ",!" );
367                     if ( !exc.startsWith( "!" ) )
368                     {
369                         exc = "!" + exc;
370                     }
371                     incExc.add( exc );
372                 }
373             }
374         }
375         return incExc;
376     }
377 
378     static boolean isRegexPrefixedPattern( String pattern )
379     {
380         int indexOfRegex = pattern.indexOf( REGEX_HANDLER_PREFIX );
381         int prefixLength = REGEX_HANDLER_PREFIX.length();
382         if ( indexOfRegex != -1 )
383         {
384             if ( indexOfRegex != 0
385                          || !pattern.endsWith( PATTERN_HANDLER_SUFFIX )
386                          || !isRegexMinLength( pattern )
387                          || pattern.indexOf( REGEX_HANDLER_PREFIX, prefixLength ) != -1 )
388             {
389                 String msg = "Illegal test|includes|excludes regex '%s'. Expected %%regex[class#method] "
390                     + "or !%%regex[class#method] " + "with optional class or #method.";
391                 throw new IllegalArgumentException( String.format( msg, pattern ) );
392             }
393             return true;
394         }
395         else
396         {
397             return false;
398         }
399     }
400 
401 
402     static boolean isRegexMinLength( String pattern )
403     {
404         //todo bug in maven-shared-utils: '+1' should not appear in the condition
405         //todo cannot reuse code from SelectorUtils.java because method isRegexPrefixedPattern is in private package.
406         return pattern.length() > REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1;
407     }
408 
409     static String[] unwrapRegex( String regex )
410     {
411         regex = regex.trim();
412         int from = REGEX_HANDLER_PREFIX.length();
413         int to = regex.length() - PATTERN_HANDLER_SUFFIX.length();
414         return unwrap( regex.substring( from, to ) );
415     }
416 
417     static String[] unwrap( final String request )
418     {
419         final String[] classAndMethod = { "", "" };
420         final int indexOfHash = request.indexOf( '#' );
421         if ( indexOfHash == -1 )
422         {
423             classAndMethod[0] = request.trim();
424         }
425         else
426         {
427             classAndMethod[0] = request.substring( 0, indexOfHash ).trim();
428             classAndMethod[1] = request.substring( 1 + indexOfHash ).trim();
429         }
430         return classAndMethod;
431     }
432 
433     static void nonRegexClassAndMethods( String clazz, String methods, boolean isExcluded,
434                          IncludedExcludedPatterns patterns,
435                          Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters )
436     {
437         for ( String method : split( methods, "+" ) )
438         {
439             method = method.trim();
440             ResolvedTest test = new ResolvedTest( clazz, method, false );
441             if ( !test.isEmpty() )
442             {
443                 updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters );
444             }
445         }
446     }
447 
448     /**
449      * Requires trimmed <code>request</code> been not equal to "!".
450      */
451     static void resolveTestRequest( String request, IncludedExcludedPatterns patterns,
452                                     Collection<ResolvedTest> includedFilters, Collection<ResolvedTest> excludedFilters )
453     {
454         final boolean isExcluded = request.startsWith( "!" );
455         ResolvedTest test = null;
456         request = removeExclamationMark( request );
457         if ( isRegexPrefixedPattern( request ) )
458         {
459             final String[] unwrapped = unwrapRegex( request );
460             final boolean hasClass = unwrapped[0].length() != 0;
461             final boolean hasMethod = unwrapped[1].length() != 0;
462             if ( hasClass && hasMethod )
463             {
464                 test = new ResolvedTest( unwrapped[0], unwrapped[1], true );
465             }
466             else if ( hasClass )
467             {
468                 test = new ResolvedTest( CLASS, unwrapped[0], true );
469             }
470             else if ( hasMethod )
471             {
472                 test = new ResolvedTest( METHOD, unwrapped[1], true );
473             }
474         }
475         else
476         {
477             final int indexOfMethodSeparator = request.indexOf( '#' );
478             if ( indexOfMethodSeparator == -1 )
479             {
480                 test = new ResolvedTest( CLASS, request, false );
481             }
482             else
483             {
484                 String clazz = request.substring( 0, indexOfMethodSeparator );
485                 String methods = request.substring( 1 + indexOfMethodSeparator );
486                 nonRegexClassAndMethods( clazz, methods, isExcluded, patterns, includedFilters, excludedFilters );
487             }
488         }
489 
490         if ( test != null && !test.isEmpty() )
491         {
492             updatedFilters( isExcluded, test, patterns, includedFilters, excludedFilters );
493         }
494     }
495 
496     private static boolean haveMethodPatterns( Set<ResolvedTest> patterns )
497     {
498         for ( ResolvedTest pattern : patterns )
499         {
500             if ( pattern.hasTestMethodPattern() )
501             {
502                 return true;
503             }
504         }
505         return false;
506     }
507 }