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