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