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