    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    }