001package org.junit.experimental.categories;
002
003import java.lang.annotation.Retention;
004import java.lang.annotation.RetentionPolicy;
005import java.util.Arrays;
006import java.util.Collections;
007import java.util.HashSet;
008import java.util.LinkedHashSet;
009import java.util.Set;
010
011import org.junit.runner.Description;
012import org.junit.runner.manipulation.Filter;
013import org.junit.runner.manipulation.NoTestsRemainException;
014import org.junit.runners.Suite;
015import org.junit.runners.model.InitializationError;
016import 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 */
083public 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}