001 package org.junit.experimental.categories;
002
003 import java.lang.annotation.Retention;
004 import java.lang.annotation.RetentionPolicy;
005 import java.util.Arrays;
006 import java.util.Collections;
007 import java.util.HashSet;
008 import java.util.LinkedHashSet;
009 import java.util.Set;
010
011 import org.junit.runner.Description;
012 import org.junit.runner.manipulation.Filter;
013 import org.junit.runner.manipulation.NoTestsRemainException;
014 import org.junit.runners.Suite;
015 import org.junit.runners.model.InitializationError;
016 import org.junit.runners.model.RunnerBuilder;
017
018 /**
019 * From a given set of test classes, runs only the classes and methods that are
020 * annotated with either the category given with the @IncludeCategory
021 * annotation, or a subtype of that category.
022 * <p>
023 * Note that, for now, annotating suites with {@code @Category} has no effect.
024 * Categories must be annotated on the direct method or class.
025 * <p>
026 * Example:
027 * <pre>
028 * public interface FastTests {
029 * }
030 *
031 * public interface SlowTests {
032 * }
033 *
034 * public interface SmokeTests
035 * }
036 *
037 * public static class A {
038 * @Test
039 * public void a() {
040 * fail();
041 * }
042 *
043 * @Category(SlowTests.class)
044 * @Test
045 * public void b() {
046 * }
047 *
048 * @Category({FastTests.class, SmokeTests.class})
049 * @Test
050 * public void c() {
051 * }
052 * }
053 *
054 * @Category({SlowTests.class, FastTests.class})
055 * public static class B {
056 * @Test
057 * public void d() {
058 * }
059 * }
060 *
061 * @RunWith(Categories.class)
062 * @IncludeCategory(SlowTests.class)
063 * @SuiteClasses({A.class, B.class})
064 * // Note that Categories is a kind of Suite
065 * public static class SlowTestSuite {
066 * // Will run A.b and B.d, but not A.a and A.c
067 * }
068 * </pre>
069 * <p>
070 * Example to run multiple categories:
071 * <pre>
072 * @RunWith(Categories.class)
073 * @IncludeCategory({FastTests.class, SmokeTests.class})
074 * @SuiteClasses({A.class, B.class})
075 * public static class FastOrSmokeTestSuite {
076 * // Will run A.c and B.d, but not A.b because it is not any of FastTests or SmokeTests
077 * }
078 * </pre>
079 *
080 * @version 4.12
081 * @see <a href="https://github.com/junit-team/junit4/wiki/Categories">Categories at JUnit wiki</a>
082 */
083 public class Categories extends Suite {
084
085 @Retention(RetentionPolicy.RUNTIME)
086 public @interface IncludeCategory {
087 /**
088 * Determines the tests to run that are annotated with categories specified in
089 * the value of this annotation or their subtypes unless excluded with {@link ExcludeCategory}.
090 */
091 Class<?>[] value() default {};
092
093 /**
094 * If <tt>true</tt>, runs tests annotated with <em>any</em> of the categories in
095 * {@link IncludeCategory#value()}. Otherwise, runs tests only if annotated with <em>all</em> of the categories.
096 */
097 boolean matchAny() default true;
098 }
099
100 @Retention(RetentionPolicy.RUNTIME)
101 public @interface ExcludeCategory {
102 /**
103 * Determines the tests which do not run if they are annotated with categories specified in the
104 * value of this annotation or their subtypes regardless of being included in {@link IncludeCategory#value()}.
105 */
106 Class<?>[] value() default {};
107
108 /**
109 * If <tt>true</tt>, the tests annotated with <em>any</em> of the categories in {@link ExcludeCategory#value()}
110 * do not run. Otherwise, the tests do not run if and only if annotated with <em>all</em> categories.
111 */
112 boolean matchAny() default true;
113 }
114
115 public static class CategoryFilter extends Filter {
116 private final Set<Class<?>> included;
117 private final Set<Class<?>> excluded;
118 private final boolean includedAny;
119 private final boolean excludedAny;
120
121 public static CategoryFilter include(boolean matchAny, Class<?>... categories) {
122 return new CategoryFilter(matchAny, categories, true, null);
123 }
124
125 public static CategoryFilter include(Class<?> category) {
126 return include(true, category);
127 }
128
129 public static CategoryFilter include(Class<?>... categories) {
130 return include(true, categories);
131 }
132
133 public static CategoryFilter exclude(boolean matchAny, Class<?>... categories) {
134 return new CategoryFilter(true, null, matchAny, categories);
135 }
136
137 public static CategoryFilter exclude(Class<?> category) {
138 return exclude(true, category);
139 }
140
141 public static CategoryFilter exclude(Class<?>... categories) {
142 return exclude(true, categories);
143 }
144
145 public static CategoryFilter categoryFilter(boolean matchAnyInclusions, Set<Class<?>> inclusions,
146 boolean matchAnyExclusions, Set<Class<?>> exclusions) {
147 return new CategoryFilter(matchAnyInclusions, inclusions, matchAnyExclusions, exclusions);
148 }
149
150 @Deprecated
151 public CategoryFilter(Class<?> includedCategory, Class<?> excludedCategory) {
152 includedAny = true;
153 excludedAny = true;
154 included = nullableClassToSet(includedCategory);
155 excluded = nullableClassToSet(excludedCategory);
156 }
157
158 protected CategoryFilter(boolean matchAnyIncludes, Set<Class<?>> includes,
159 boolean matchAnyExcludes, Set<Class<?>> excludes) {
160 includedAny = matchAnyIncludes;
161 excludedAny = matchAnyExcludes;
162 included = copyAndRefine(includes);
163 excluded = copyAndRefine(excludes);
164 }
165
166 private CategoryFilter(boolean matchAnyIncludes, Class<?>[] inclusions,
167 boolean matchAnyExcludes, Class<?>[] exclusions) {
168 includedAny = matchAnyIncludes;
169 excludedAny = matchAnyExcludes;
170 included = createSet(inclusions);
171 excluded = createSet(exclusions);
172 }
173
174 /**
175 * @see #toString()
176 */
177 @Override
178 public String describe() {
179 return toString();
180 }
181
182 /**
183 * Returns string in the form <tt>"[included categories] - [excluded categories]"</tt>, where both
184 * sets have comma separated names of categories.
185 *
186 * @return string representation for the relative complement of excluded categories set
187 * in the set of included categories. Examples:
188 * <ul>
189 * <li> <tt>"categories [all]"</tt> for all included categories and no excluded ones;
190 * <li> <tt>"categories [all] - [A, B]"</tt> for all included categories and given excluded ones;
191 * <li> <tt>"categories [A, B] - [C, D]"</tt> for given included categories and given excluded ones.
192 * </ul>
193 * @see Class#toString() name of category
194 */
195 @Override public String toString() {
196 StringBuilder description= new StringBuilder("categories ")
197 .append(included.isEmpty() ? "[all]" : included);
198 if (!excluded.isEmpty()) {
199 description.append(" - ").append(excluded);
200 }
201 return description.toString();
202 }
203
204 @Override
205 public boolean shouldRun(Description description) {
206 if (hasCorrectCategoryAnnotation(description)) {
207 return true;
208 }
209
210 for (Description each : description.getChildren()) {
211 if (shouldRun(each)) {
212 return true;
213 }
214 }
215
216 return false;
217 }
218
219 private boolean hasCorrectCategoryAnnotation(Description description) {
220 final Set<Class<?>> childCategories= categories(description);
221
222 // If a child has no categories, immediately return.
223 if (childCategories.isEmpty()) {
224 return included.isEmpty();
225 }
226
227 if (!excluded.isEmpty()) {
228 if (excludedAny) {
229 if (matchesAnyParentCategories(childCategories, excluded)) {
230 return false;
231 }
232 } else {
233 if (matchesAllParentCategories(childCategories, excluded)) {
234 return false;
235 }
236 }
237 }
238
239 if (included.isEmpty()) {
240 // Couldn't be excluded, and with no suite's included categories treated as should run.
241 return true;
242 } else {
243 if (includedAny) {
244 return matchesAnyParentCategories(childCategories, included);
245 } else {
246 return matchesAllParentCategories(childCategories, included);
247 }
248 }
249 }
250
251 /**
252 * @return <tt>true</tt> if at least one (any) parent category match a child, otherwise <tt>false</tt>.
253 * If empty <tt>parentCategories</tt>, returns <tt>false</tt>.
254 */
255 private boolean matchesAnyParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) {
256 for (Class<?> parentCategory : parentCategories) {
257 if (hasAssignableTo(childCategories, parentCategory)) {
258 return true;
259 }
260 }
261 return false;
262 }
263
264 /**
265 * @return <tt>false</tt> if at least one parent category does not match children, otherwise <tt>true</tt>.
266 * If empty <tt>parentCategories</tt>, returns <tt>true</tt>.
267 */
268 private boolean matchesAllParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) {
269 for (Class<?> parentCategory : parentCategories) {
270 if (!hasAssignableTo(childCategories, parentCategory)) {
271 return false;
272 }
273 }
274 return true;
275 }
276
277 private static Set<Class<?>> categories(Description description) {
278 Set<Class<?>> categories= new HashSet<Class<?>>();
279 Collections.addAll(categories, directCategories(description));
280 Collections.addAll(categories, directCategories(parentDescription(description)));
281 return categories;
282 }
283
284 private static Description parentDescription(Description description) {
285 Class<?> testClass= description.getTestClass();
286 return testClass == null ? null : Description.createSuiteDescription(testClass);
287 }
288
289 private static Class<?>[] directCategories(Description description) {
290 if (description == null) {
291 return new Class<?>[0];
292 }
293
294 Category annotation= description.getAnnotation(Category.class);
295 return annotation == null ? new Class<?>[0] : annotation.value();
296 }
297
298 private static Set<Class<?>> copyAndRefine(Set<Class<?>> classes) {
299 Set<Class<?>> c= new LinkedHashSet<Class<?>>();
300 if (classes != null) {
301 c.addAll(classes);
302 }
303 c.remove(null);
304 return c;
305 }
306 }
307
308 public Categories(Class<?> klass, RunnerBuilder builder) throws InitializationError {
309 super(klass, builder);
310 try {
311 Set<Class<?>> included= getIncludedCategory(klass);
312 Set<Class<?>> excluded= getExcludedCategory(klass);
313 boolean isAnyIncluded= isAnyIncluded(klass);
314 boolean isAnyExcluded= isAnyExcluded(klass);
315
316 filter(CategoryFilter.categoryFilter(isAnyIncluded, included, isAnyExcluded, excluded));
317 } catch (NoTestsRemainException e) {
318 throw new InitializationError(e);
319 }
320 }
321
322 private static Set<Class<?>> getIncludedCategory(Class<?> klass) {
323 IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class);
324 return createSet(annotation == null ? null : annotation.value());
325 }
326
327 private static boolean isAnyIncluded(Class<?> klass) {
328 IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class);
329 return annotation == null || annotation.matchAny();
330 }
331
332 private static Set<Class<?>> getExcludedCategory(Class<?> klass) {
333 ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class);
334 return createSet(annotation == null ? null : annotation.value());
335 }
336
337 private static boolean isAnyExcluded(Class<?> klass) {
338 ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class);
339 return annotation == null || annotation.matchAny();
340 }
341
342 private static boolean hasAssignableTo(Set<Class<?>> assigns, Class<?> to) {
343 for (final Class<?> from : assigns) {
344 if (to.isAssignableFrom(from)) {
345 return true;
346 }
347 }
348 return false;
349 }
350
351 private static Set<Class<?>> createSet(Class<?>[] classes) {
352 // Not throwing a NPE if t is null is a bad idea, but it's the behavior from JUnit 4.12
353 // for include(boolean, Class<?>...) and exclude(boolean, Class<?>...)
354 if (classes == null || classes.length == 0) {
355 return Collections.emptySet();
356 }
357 for (Class<?> category : classes) {
358 if (category == null) {
359 throw new NullPointerException("has null category");
360 }
361 }
362
363 return classes.length == 1
364 ? Collections.<Class<?>>singleton(classes[0])
365 : new LinkedHashSet<Class<?>>(Arrays.asList(classes));
366 }
367
368 private static Set<Class<?>> nullableClassToSet(Class<?> nullableClass) {
369 // Not throwing a NPE if t is null is a bad idea, but it's the behavior from JUnit 4.11
370 // for CategoryFilter(Class<?> includedCategory, Class<?> excludedCategory)
371 return nullableClass == null
372 ? Collections.<Class<?>>emptySet()
373 : Collections.<Class<?>>singleton(nullableClass);
374 }
375 }