replaced <code></code> with more concise {@code}
[idea/community.git] / platform / testFramework / src / com / intellij / GroupBasedTestClassFilter.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij;
17
18 import com.intellij.util.containers.ContainerUtil;
19 import com.intellij.util.containers.MultiMap;
20 import org.jetbrains.annotations.NotNull;
21
22 import java.io.BufferedReader;
23 import java.io.IOException;
24 import java.io.Reader;
25 import java.util.Collection;
26 import java.util.List;
27 import java.util.Set;
28 import java.util.regex.Pattern;
29 import java.util.stream.Collectors;
30
31 /**
32  * Encapsulates logic of filtering test classes (classes that contain test-cases).
33  * <p/>
34  * We want to have such facility in order to be able to execute different sets of tests like {@code 'fast tests'},
35  * {@code 'problem tests'} etc.
36  * <p/>
37  * I.e. assumed usage scenario is to create object of this class with necessary filtering criteria and use it's
38  * {@link TestClassesFilter#matches(String, String)} method for determining if particular test should be executed.
39  * <p/>
40  * The filtering is performed by fully-qualified test class name. There are two ways to define the criteria at the moment:
41  * <ul>
42  *   <li>
43  *     Define target class name filters (at regexp format) explicitly using
44  *     {@link PatternListTestClassFilter#PatternListTestClassFilter(List) PatternListTestClassFilter};
45  *   </li>
46  *   <li>
47  *     Read class name filters (at regexp format) from the given stream - see {@link #createOn(Reader, List)};
48  *   </li>
49  * </ul>
50  */
51 public class GroupBasedTestClassFilter extends TestClassesFilter {
52   /**
53    * Holds reserved test group name that serves as a negation of matching result.
54    *
55    * @see TestClassesFilter#matches(String, String)
56    */
57   public static final String ALL_EXCLUDE_DEFINED = "ALL_EXCLUDE_DEFINED";
58
59   private final List<Group> myGroups = ContainerUtil.newSmartList();
60   private final Set<String> myTestGroupNames;
61
62   public GroupBasedTestClassFilter(MultiMap<String, String> filters, List<String> testGroupNames) {
63     //empty group means all patterns from each defined group should be excluded
64     myTestGroupNames = ContainerUtil.newTroveSet(testGroupNames);
65
66     for (String groupName : filters.keySet()) {
67       Collection<String> groupFilters = filters.get(groupName);
68       List<Pattern> includePatterns = compilePatterns(ContainerUtil.filter(groupFilters, s -> !s.startsWith("-")));
69       List<Pattern> excludedPatterns = compilePatterns(groupFilters.stream()
70                                                          .filter(s -> s.startsWith("-") && s.length() > 1)
71                                                          .map(s -> s.substring(1))
72                                                          .collect(Collectors.toList()));
73       myGroups.add(new Group(groupName, includePatterns, excludedPatterns));
74     }
75   }
76
77   /**
78    * Creates {@code TestClassesFilter} object assuming that the given stream contains grouped test class filters
79    * at the following format:
80    * <p/>
81    * <ul>
82    *   <li>
83    *      every line that starts with {@code '['} symbol and ends with {@code ']'} symbol defines start
84    *      of the new test group. That means that all test class filters that follows this line belongs to the same
85    *      test group which name is defined by the text contained between {@code '['} and {@code ']'}
86    *   </li>
87    *   <li>every line that is not a test-group definition is considered to be a test class filter at regexp format;</li>
88    * </ul>
89    * <p/>
90    * <b>Example</b>
91    * Consider that given stream points to the following data:
92    * <pre>
93    *    [CVS]
94    *    com.intellij.cvsSupport2.*
95    *    [STRESS_TESTS]
96    *    com.intellij.application.InspectionPerformanceTest
97    *    com.intellij.application.TraverseUITest
98    * </pre>
99    * <p/>
100    * It defines two test groups:
101    * <ul>
102    *   <li><b>CVS</b> group with the single test class name pattern {@code 'com.intellij.cvsSupport2.*'};</li>
103    *   <li>
104    *     <b>STRESS_TESTS</b> group with the following test class name patterns:
105    *     <ul>
106    *       <li>com.intellij.application.InspectionPerformanceTest</li>
107    *       <li>com.intellij.application.TraverseUITest</li>
108    *     </ul>
109    *   </li>
110    * </ul>
111    * <p/>
112    * This method doesn't suppose itself to be owner of the given stream reader, i.e. it assumes that the stream should be
113    * closed on caller side.
114    *
115    *
116    * @param reader   reader that points to the target test groups config
117    * @param testGroupNames
118    * @return newly created {@link GroupBasedTestClassFilter} object with the data contained at the given reader
119    * @see TestClassesFilter#matches(String, String)
120    */
121   @NotNull
122   public static TestClassesFilter createOn(@NotNull Reader reader, @NotNull List<String> testGroupNames) throws IOException {
123     return new GroupBasedTestClassFilter(readGroups(reader), testGroupNames);
124   }
125
126   public static MultiMap<String, String> readGroups(Reader reader) throws IOException {
127     MultiMap<String, String> groupNameToPatternsMap = MultiMap.createLinked();
128     String currentGroupName = "";
129
130     @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) BufferedReader bufferedReader = new BufferedReader(reader);
131     String line;
132     while ((line = bufferedReader.readLine()) != null) {
133       if (line.startsWith("#")) continue;
134       if (line.startsWith("[") && line.endsWith("]")) {
135         currentGroupName = line.substring(1, line.length() - 1);
136       }
137       else {
138         groupNameToPatternsMap.putValue(currentGroupName, line);
139       }
140     }
141     return groupNameToPatternsMap;
142   }
143
144   /**
145    * Allows to check if given class name belongs to the test group with the given name based on filtering rules encapsulated
146    * at the current {@link GroupBasedTestClassFilter} object. I.e. this method returns {@code true} if given test class name
147    * is matched with any test class name filter configured for the test group with the given name.
148    * <p/>
149    * <b>Note:</b> there is a special case processing when given group name is {@link #ALL_EXCLUDE_DEFINED}. This method
150    * returns {@code true} only if all registered patterns (for all test groups) don't match given test class name.
151    *
152    * @param className   target test class name to check
153    * @param moduleName
154    * @return            {@code true} if given test group name is defined (not {@code null}) and test class with given
155    *                    name belongs to the test group with given name;
156    *                    {@code true} if given group if undefined or equal to {@link #ALL_EXCLUDE_DEFINED} and given test
157    *                    class name is not matched by all registered patterns;
158    *                    {@code false} otherwise
159    */
160   @Override
161   public boolean matches(String className, String moduleName) {
162     if (myGroups.stream().filter(g -> myTestGroupNames.contains(g.name)).anyMatch(g -> g.matches(className))) return true;
163     return containsAllExcludeDefinedGroup(myTestGroupNames) && myGroups.stream().noneMatch(g -> g.matches(className));
164   }
165
166   private static boolean containsAllExcludeDefinedGroup(Set<String> groupNames) {
167     return groupNames.isEmpty() || groupNames.contains(ALL_EXCLUDE_DEFINED);
168   }
169
170   private static class Group {
171     private final String name;
172     private final List<Pattern> included;
173     private final List<Pattern> excluded;
174
175     private Group(String name, List<Pattern> included, List<Pattern> excluded) {
176       this.name = name;
177       this.excluded = excluded;
178       this.included = included;
179     }
180     
181     private boolean matches(String className) {
182       return !matchesAnyPattern(excluded, className) && matchesAnyPattern(included, className);
183     }
184   }
185 }