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     *     &#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/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>&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    }