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