View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.api.testset;
20  
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.LinkedHashSet;
25  import java.util.Set;
26  
27  import static java.util.Collections.singleton;
28  import static java.util.Collections.unmodifiableSet;
29  import static org.apache.maven.surefire.api.testset.ResolvedTest.Type.CLASS;
30  import static org.apache.maven.surefire.api.testset.ResolvedTest.Type.METHOD;
31  import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
32  import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
33  import static org.apache.maven.surefire.shared.utils.StringUtils.split;
34  import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX;
35  import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX;
36  
37  // TODO In Surefire 3.0 see SUREFIRE-1309 and use normal fully qualified class name regex instead.
38  /**
39   * Resolved multi pattern filter e.g. -Dtest=MyTest#test,!AnotherTest#otherTest into an object model
40   * composed of included and excluded tests.<br>
41   * The methods {@link #shouldRun(String, String)} are filters easily used in JUnit filter or TestNG.
42   * This class is independent of JUnit and TestNG API.<br>
43   * It is accessed by Java Reflection API in {@code org.apache.maven.surefire.booter.SurefireReflector}
44   * using specific ClassLoader.
45   */
46  public class TestListResolver implements GenericTestPattern<ResolvedTest, String, String> {
47      private static final String JAVA_CLASS_FILE_EXTENSION = ".class";
48  
49      private static final TestListResolver WILDCARD = new TestListResolver("*" + JAVA_CLASS_FILE_EXTENSION);
50  
51      private static final TestListResolver EMPTY = new TestListResolver("");
52  
53      private final Set<ResolvedTest> includedPatterns;
54  
55      private final Set<ResolvedTest> excludedPatterns;
56  
57      private final boolean hasIncludedMethodPatterns;
58  
59      private final boolean hasExcludedMethodPatterns;
60  
61      public TestListResolver(Collection<String> tests) {
62          final IncludedExcludedPatterns patterns = new IncludedExcludedPatterns();
63          final Set<ResolvedTest> includedFilters = new LinkedHashSet<>(0);
64          final Set<ResolvedTest> excludedFilters = new LinkedHashSet<>(0);
65  
66          for (final String csvTests : tests) {
67              if (isNotBlank(csvTests)) {
68                  for (String request : split(csvTests, ",")) {
69                      request = request.trim();
70                      if (!request.isEmpty() && !request.equals("!")) {
71                          resolveTestRequest(request, patterns, includedFilters, excludedFilters);
72                      }
73                  }
74              }
75          }
76  
77          this.includedPatterns = unmodifiableSet(includedFilters);
78          this.excludedPatterns = unmodifiableSet(excludedFilters);
79          this.hasIncludedMethodPatterns = patterns.hasIncludedMethodPatterns;
80          this.hasExcludedMethodPatterns = patterns.hasExcludedMethodPatterns;
81      }
82  
83      public TestListResolver(String csvTests) {
84          this(csvTests == null ? Collections.<String>emptySet() : singleton(csvTests));
85      }
86  
87      public TestListResolver(Collection<String> included, Collection<String> excluded) {
88          this(mergeIncludedAndExcludedTests(included, excluded));
89      }
90  
91      /**
92       * Used only in method filter.
93       */
94      private TestListResolver(
95              boolean hasIncludedMethodPatterns,
96              boolean hasExcludedMethodPatterns,
97              Set<ResolvedTest> includedPatterns,
98              Set<ResolvedTest> excludedPatterns) {
99          this.includedPatterns = includedPatterns;
100         this.excludedPatterns = excludedPatterns;
101         this.hasIncludedMethodPatterns = hasIncludedMethodPatterns;
102         this.hasExcludedMethodPatterns = hasExcludedMethodPatterns;
103     }
104 
105     public static TestListResolver newTestListResolver(
106             Set<ResolvedTest> includedPatterns, Set<ResolvedTest> excludedPatterns) {
107         return new TestListResolver(
108                 haveMethodPatterns(includedPatterns),
109                 haveMethodPatterns(excludedPatterns),
110                 includedPatterns,
111                 excludedPatterns);
112     }
113 
114     @Override
115     public boolean hasIncludedMethodPatterns() {
116         return hasIncludedMethodPatterns;
117     }
118 
119     @Override
120     public boolean hasExcludedMethodPatterns() {
121         return hasExcludedMethodPatterns;
122     }
123 
124     @Override
125     public boolean hasMethodPatterns() {
126         return hasIncludedMethodPatterns() || hasExcludedMethodPatterns();
127     }
128 
129     /**
130      *
131      * @param resolver    filter possibly having method patterns
132      * @return {@code resolver} if {@link TestListResolver#hasMethodPatterns() resolver.hasMethodPatterns()}
133      * returns {@code true}; Otherwise wildcard filter {@code *.class} is returned.
134      */
135     public static TestListResolver optionallyWildcardFilter(TestListResolver resolver) {
136         return resolver.hasMethodPatterns() ? resolver : WILDCARD;
137     }
138 
139     public static TestListResolver getEmptyTestListResolver() {
140         return EMPTY;
141     }
142 
143     public final boolean isWildcard() {
144         return equals(WILDCARD);
145     }
146 
147     public TestFilter<String, String> and(final TestListResolver another) {
148         return new TestFilter<String, String>() {
149             @Override
150             public boolean shouldRun(String testClass, String methodName) {
151                 return TestListResolver.this.shouldRun(testClass, methodName)
152                         && another.shouldRun(testClass, methodName);
153             }
154         };
155     }
156 
157     public TestFilter<String, String> or(final TestListResolver another) {
158         return new TestFilter<String, String>() {
159             @Override
160             public boolean shouldRun(String testClass, String methodName) {
161                 return TestListResolver.this.shouldRun(testClass, methodName)
162                         || another.shouldRun(testClass, methodName);
163             }
164         };
165     }
166 
167     public boolean shouldRun(Class<?> testClass, String methodName) {
168         return shouldRun(toClassFileName(testClass), methodName);
169     }
170 
171     /**
172      * Returns {@code true} if satisfies {@code testClassFile} and {@code methodName} filter.
173      *
174      * @param testClassFile format must be e.g. "my/package/MyTest.class" including class extension; or null
175      * @param methodName real test-method name; or null
176      */
177     @Override
178     public boolean shouldRun(String testClassFile, String methodName) {
179         if (isEmpty() || isBlank(testClassFile) && isBlank(methodName)) {
180             return true;
181         } else {
182             boolean shouldRun = false;
183 
184             if (getIncludedPatterns().isEmpty()) {
185                 shouldRun = true;
186             } else {
187                 for (ResolvedTest filter : getIncludedPatterns()) {
188                     if (filter.matchAsInclusive(testClassFile, methodName)) {
189                         shouldRun = true;
190                         break;
191                     }
192                 }
193             }
194 
195             if (shouldRun) {
196                 for (ResolvedTest filter : getExcludedPatterns()) {
197                     if (filter.matchAsExclusive(testClassFile, methodName)) {
198                         shouldRun = false;
199                         break;
200                     }
201                 }
202             }
203             return shouldRun;
204         }
205     }
206 
207     @Override
208     public boolean isEmpty() {
209         return equals(EMPTY);
210     }
211 
212     @Override
213     public String getPluginParameterTest() {
214         String aggregatedTest = aggregatedTest("", getIncludedPatterns());
215 
216         if (isNotBlank(aggregatedTest) && !getExcludedPatterns().isEmpty()) {
217             aggregatedTest += ", ";
218         }
219 
220         aggregatedTest += aggregatedTest("!", getExcludedPatterns());
221         return aggregatedTest.isEmpty() ? "" : aggregatedTest;
222     }
223 
224     @Override
225     public Set<ResolvedTest> getIncludedPatterns() {
226         return includedPatterns;
227     }
228 
229     @Override
230     public Set<ResolvedTest> getExcludedPatterns() {
231         return excludedPatterns;
232     }
233 
234     @Override
235     public boolean equals(Object o) {
236         if (this == o) {
237             return true;
238         }
239         if (o == null || getClass() != o.getClass()) {
240             return false;
241         }
242 
243         TestListResolver that = (TestListResolver) o;
244 
245         return getIncludedPatterns().equals(that.getIncludedPatterns())
246                 && getExcludedPatterns().equals(that.getExcludedPatterns());
247     }
248 
249     @Override
250     public int hashCode() {
251         int result = getIncludedPatterns().hashCode();
252         result = 31 * result + getExcludedPatterns().hashCode();
253         return result;
254     }
255 
256     @Override
257     public String toString() {
258         return getPluginParameterTest();
259     }
260 
261     public static String toClassFileName(Class<?> test) {
262         return test == null ? null : toClassFileName(test.getName());
263     }
264 
265     public static String toClassFileName(String fullyQualifiedTestClass) {
266         return fullyQualifiedTestClass == null
267                 ? null
268                 : fullyQualifiedTestClass.replace('.', '/') + JAVA_CLASS_FILE_EXTENSION;
269     }
270 
271     static String removeExclamationMark(String s) {
272         return !s.isEmpty() && s.charAt(0) == '!' ? s.substring(1) : s;
273     }
274 
275     private static void updatedFilters(
276             boolean isExcluded,
277             ResolvedTest test,
278             IncludedExcludedPatterns patterns,
279             Collection<ResolvedTest> includedFilters,
280             Collection<ResolvedTest> excludedFilters) {
281         if (isExcluded) {
282             excludedFilters.add(test);
283             patterns.hasExcludedMethodPatterns |= test.hasTestMethodPattern();
284         } else {
285             includedFilters.add(test);
286             patterns.hasIncludedMethodPatterns |= test.hasTestMethodPattern();
287         }
288     }
289 
290     private static String aggregatedTest(String testPrefix, Set<ResolvedTest> tests) {
291         StringBuilder aggregatedTest = new StringBuilder();
292         for (ResolvedTest test : tests) {
293             String readableTest = test.toString();
294             if (!readableTest.isEmpty()) {
295                 if (aggregatedTest.length() != 0) {
296                     aggregatedTest.append(", ");
297                 }
298                 aggregatedTest.append(testPrefix).append(readableTest);
299             }
300         }
301         return aggregatedTest.toString();
302     }
303 
304     private static Collection<String> mergeIncludedAndExcludedTests(
305             Collection<String> included, Collection<String> excluded) {
306         ArrayList<String> incExc = new ArrayList<>(included);
307         incExc.removeAll(Collections.<String>singleton(null));
308         for (String exc : excluded) {
309             if (exc != null) {
310                 exc = exc.trim();
311                 if (!exc.isEmpty()) {
312                     if (exc.contains("!")) {
313                         throw new IllegalArgumentException("Exclamation mark not expected in 'exclusion': " + exc);
314                     }
315                     exc = exc.replace(",", ",!");
316                     if (!exc.startsWith("!")) {
317                         exc = "!" + exc;
318                     }
319                     incExc.add(exc);
320                 }
321             }
322         }
323         return incExc;
324     }
325 
326     static boolean isRegexPrefixedPattern(String pattern) {
327         int indexOfRegex = pattern.indexOf(REGEX_HANDLER_PREFIX);
328         int prefixLength = REGEX_HANDLER_PREFIX.length();
329         if (indexOfRegex != -1) {
330             if (indexOfRegex != 0
331                     || !pattern.endsWith(PATTERN_HANDLER_SUFFIX)
332                     || !isRegexMinLength(pattern)
333                     || pattern.indexOf(REGEX_HANDLER_PREFIX, prefixLength) != -1) {
334                 String msg = "Illegal test|includes|excludes regex '%s'. Expected %%regex[class#method] "
335                         + "or !%%regex[class#method] " + "with optional class or #method.";
336                 throw new IllegalArgumentException(String.format(msg, pattern));
337             }
338             return true;
339         } else {
340             return false;
341         }
342     }
343 
344     static boolean isRegexMinLength(String pattern) {
345         // todo bug in maven-shared-utils: '+1' should not appear in the condition
346         // todo cannot reuse code from SelectorUtils.java because method isRegexPrefixedPattern is in private package.
347         return pattern.length() > REGEX_HANDLER_PREFIX.length() + PATTERN_HANDLER_SUFFIX.length() + 1;
348     }
349 
350     static String[] unwrapRegex(String regex) {
351         regex = regex.trim();
352         int from = REGEX_HANDLER_PREFIX.length();
353         int to = regex.length() - PATTERN_HANDLER_SUFFIX.length();
354         return unwrap(regex.substring(from, to));
355     }
356 
357     static String[] unwrap(final String request) {
358         final String[] classAndMethod = {"", ""};
359         final int indexOfHash = request.indexOf('#');
360         if (indexOfHash == -1) {
361             classAndMethod[0] = request.trim();
362         } else {
363             classAndMethod[0] = request.substring(0, indexOfHash).trim();
364             classAndMethod[1] = request.substring(1 + indexOfHash).trim();
365         }
366         return classAndMethod;
367     }
368 
369     static void nonRegexClassAndMethods(
370             String clazz,
371             String methods,
372             boolean isExcluded,
373             IncludedExcludedPatterns patterns,
374             Collection<ResolvedTest> includedFilters,
375             Collection<ResolvedTest> excludedFilters) {
376         for (String method : split(methods, "+")) {
377             method = method.trim();
378             ResolvedTest test = new ResolvedTest(clazz, method, false);
379             if (!test.isEmpty()) {
380                 updatedFilters(isExcluded, test, patterns, includedFilters, excludedFilters);
381             }
382         }
383     }
384 
385     /**
386      * Requires trimmed {@code request} been not equal to "!".
387      */
388     static void resolveTestRequest(
389             String request,
390             IncludedExcludedPatterns patterns,
391             Collection<ResolvedTest> includedFilters,
392             Collection<ResolvedTest> excludedFilters) {
393         final boolean isExcluded = request.startsWith("!");
394         ResolvedTest test = null;
395         request = removeExclamationMark(request);
396         if (isRegexPrefixedPattern(request)) {
397             final String[] unwrapped = unwrapRegex(request);
398             final boolean hasClass = !unwrapped[0].isEmpty();
399             final boolean hasMethod = !unwrapped[1].isEmpty();
400             if (hasClass && hasMethod) {
401                 test = new ResolvedTest(unwrapped[0], unwrapped[1], true);
402             } else if (hasClass) {
403                 test = new ResolvedTest(CLASS, unwrapped[0], true);
404             } else if (hasMethod) {
405                 test = new ResolvedTest(METHOD, unwrapped[1], true);
406             }
407         } else {
408             final int indexOfMethodSeparator = request.indexOf('#');
409             if (indexOfMethodSeparator == -1) {
410                 test = new ResolvedTest(CLASS, request, false);
411             } else {
412                 String clazz = request.substring(0, indexOfMethodSeparator);
413                 String methods = request.substring(1 + indexOfMethodSeparator);
414                 nonRegexClassAndMethods(clazz, methods, isExcluded, patterns, includedFilters, excludedFilters);
415             }
416         }
417 
418         if (test != null && !test.isEmpty()) {
419             updatedFilters(isExcluded, test, patterns, includedFilters, excludedFilters);
420         }
421     }
422 
423     private static boolean haveMethodPatterns(Set<ResolvedTest> patterns) {
424         for (ResolvedTest pattern : patterns) {
425             if (pattern.hasTestMethodPattern()) {
426                 return true;
427             }
428         }
429         return false;
430     }
431 }