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