1/**
2 *
3 */
4package org.junit.experimental.categories;
5
6import java.lang.annotation.Retention;
7import java.lang.annotation.RetentionPolicy;
8import java.util.ArrayList;
9import java.util.Arrays;
10import java.util.List;
11
12import org.junit.runner.Description;
13import org.junit.runner.manipulation.Filter;
14import org.junit.runner.manipulation.NoTestsRemainException;
15import org.junit.runners.Suite;
16import org.junit.runners.model.InitializationError;
17import org.junit.runners.model.RunnerBuilder;
18
19/**
20 * From a given set of test classes, runs only the classes and methods that are
21 * annotated with either the category given with the @IncludeCategory
22 * annotation, or a subtype of that category.
23 *
24 * Note that, for now, annotating suites with {@code @Category} has no effect.
25 * Categories must be annotated on the direct method or class.
26 *
27 * Example:
28 *
29 * <pre>
30 * public interface FastTests {
31 * }
32 *
33 * public interface SlowTests {
34 * }
35 *
36 * public static class A {
37 * 	&#064;Test
38 * 	public void a() {
39 * 		fail();
40 * 	}
41 *
42 * 	&#064;Category(SlowTests.class)
43 * 	&#064;Test
44 * 	public void b() {
45 * 	}
46 * }
47 *
48 * &#064;Category( { SlowTests.class, FastTests.class })
49 * public static class B {
50 * 	&#064;Test
51 * 	public void c() {
52 *
53 * 	}
54 * }
55 *
56 * &#064;RunWith(Categories.class)
57 * &#064;IncludeCategory(SlowTests.class)
58 * &#064;SuiteClasses( { A.class, B.class })
59 * // Note that Categories is a kind of Suite
60 * public static class SlowTestSuite {
61 * }
62 * </pre>
63 */
64public class Categories extends Suite {
65	// the way filters are implemented makes this unnecessarily complicated,
66	// buggy, and difficult to specify.  A new way of handling filters could
67	// someday enable a better new implementation.
68        // https://github.com/KentBeck/junit/issues/issue/172
69
70	@Retention(RetentionPolicy.RUNTIME)
71	public @interface IncludeCategory {
72		public Class<?> value();
73	}
74
75	@Retention(RetentionPolicy.RUNTIME)
76	public @interface ExcludeCategory {
77		public Class<?> value();
78	}
79
80	public static class CategoryFilter extends Filter {
81		public static CategoryFilter include(Class<?> categoryType) {
82			return new CategoryFilter(categoryType, null);
83		}
84
85		private final Class<?> fIncluded;
86
87		private final Class<?> fExcluded;
88
89		public CategoryFilter(Class<?> includedCategory,
90				Class<?> excludedCategory) {
91			fIncluded= includedCategory;
92			fExcluded= excludedCategory;
93		}
94
95		@Override
96		public String describe() {
97			return "category " + fIncluded;
98		}
99
100		@Override
101		public boolean shouldRun(Description description) {
102			if (hasCorrectCategoryAnnotation(description))
103				return true;
104			for (Description each : description.getChildren())
105				if (shouldRun(each))
106					return true;
107			return false;
108		}
109
110		private boolean hasCorrectCategoryAnnotation(Description description) {
111			List<Class<?>> categories= categories(description);
112			if (categories.isEmpty())
113				return fIncluded == null;
114			for (Class<?> each : categories)
115				if (fExcluded != null && fExcluded.isAssignableFrom(each))
116					return false;
117			for (Class<?> each : categories)
118				if (fIncluded == null || fIncluded.isAssignableFrom(each))
119					return true;
120			return false;
121		}
122
123		private List<Class<?>> categories(Description description) {
124			ArrayList<Class<?>> categories= new ArrayList<Class<?>>();
125			categories.addAll(Arrays.asList(directCategories(description)));
126			categories.addAll(Arrays.asList(directCategories(parentDescription(description))));
127			return categories;
128		}
129
130		private Description parentDescription(Description description) {
131			Class<?> testClass= description.getTestClass();
132			if (testClass == null)
133				return null;
134			return Description.createSuiteDescription(testClass);
135		}
136
137		private Class<?>[] directCategories(Description description) {
138			if (description == null)
139				return new Class<?>[0];
140			Category annotation= description.getAnnotation(Category.class);
141			if (annotation == null)
142				return new Class<?>[0];
143			return annotation.value();
144		}
145	}
146
147	public Categories(Class<?> klass, RunnerBuilder builder)
148			throws InitializationError {
149		super(klass, builder);
150		try {
151			filter(new CategoryFilter(getIncludedCategory(klass),
152					getExcludedCategory(klass)));
153		} catch (NoTestsRemainException e) {
154			throw new InitializationError(e);
155		}
156		assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription());
157	}
158
159	private Class<?> getIncludedCategory(Class<?> klass) {
160		IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class);
161		return annotation == null ? null : annotation.value();
162	}
163
164	private Class<?> getExcludedCategory(Class<?> klass) {
165		ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class);
166		return annotation == null ? null : annotation.value();
167	}
168
169	private void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError {
170		if (!canHaveCategorizedChildren(description))
171			assertNoDescendantsHaveCategoryAnnotations(description);
172		for (Description each : description.getChildren())
173			assertNoCategorizedDescendentsOfUncategorizeableParents(each);
174	}
175
176	private void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError {
177		for (Description each : description.getChildren()) {
178			if (each.getAnnotation(Category.class) != null)
179				throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods.");
180			assertNoDescendantsHaveCategoryAnnotations(each);
181		}
182	}
183
184	// If children have names like [0], our current magical category code can't determine their
185	// parentage.
186	private static boolean canHaveCategorizedChildren(Description description) {
187		for (Description each : description.getChildren())
188			if (each.getTestClass() == null)
189				return false;
190		return true;
191	}
192}