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     *     &#064;Test
039     *     public void a() {
040     *         fail();
041     *     }
042     *
043     *     &#064;Category(SlowTests.class)
044     *     &#064;Test
045     *     public void b() {
046     *     }
047     *
048     *     &#064;Category({FastTests.class, SmokeTests.class})
049     *     &#064;Test
050     *     public void c() {
051     *     }
052     * }
053     *
054     * &#064;Category({SlowTests.class, FastTests.class})
055     * public static class B {
056     *     &#064;Test
057     *     public void d() {
058     *     }
059     * }
060     *
061     * &#064;RunWith(Categories.class)
062     * &#064;IncludeCategory(SlowTests.class)
063     * &#064;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     * &#064;RunWith(Categories.class)
073     * &#064;IncludeCategory({FastTests.class, SmokeTests.class})
074     * &#064;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>&quot;[included categories] - [excluded categories]&quot;</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>&quot;categories [all]&quot;</tt> for all included categories and no excluded ones;
190             *  <li> <tt>&quot;categories [all] - [A, B]&quot;</tt> for all included categories and given excluded ones;
191             *  <li> <tt>&quot;categories [A, B] - [C, D]&quot;</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    }