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