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