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