001package org.junit.experimental.categories;
002
003import java.lang.annotation.Retention;
004import java.lang.annotation.RetentionPolicy;
005import java.util.Collections;
006import java.util.HashSet;
007import java.util.Set;
008
009import org.junit.runner.Description;
010import org.junit.runner.manipulation.Filter;
011import org.junit.runner.manipulation.NoTestsRemainException;
012import org.junit.runners.Suite;
013import org.junit.runners.model.InitializationError;
014import 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 *     &#064;Test
037 *     public void a() {
038 *         fail();
039 *     }
040 *
041 *     &#064;Category(SlowTests.class)
042 *     &#064;Test
043 *     public void b() {
044 *     }
045 *
046 *     &#064;Category({FastTests.class, SmokeTests.class})
047 *     &#064;Test
048 *     public void c() {
049 *     }
050 * }
051 *
052 * &#064;Category({SlowTests.class, FastTests.class})
053 * public static class B {
054 *     &#064;Test
055 *     public void d() {
056 *     }
057 * }
058 *
059 * &#064;RunWith(Categories.class)
060 * &#064;IncludeCategory(SlowTests.class)
061 * &#064;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 * &#064;RunWith(Categories.class)
071 * &#064;IncludeCategory({FastTests.class, SmokeTests.class})
072 * &#064;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/junit/wiki/Categories">Categories at JUnit wiki</a>
080 */
081public 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>&quot;[included categories] - [excluded categories]&quot;</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>&quot;categories [all]&quot;</tt> for all included categories and no excluded ones;
178         *  <li> <tt>&quot;categories [all] - [A, B]&quot;</tt> for all included categories and given excluded ones;
179         *  <li> <tt>&quot;categories [A, B] - [C, D]&quot;</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}