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 * @Test 38 * public void a() { 39 * fail(); 40 * } 41 * 42 * @Category(SlowTests.class) 43 * @Test 44 * public void b() { 45 * } 46 * } 47 * 48 * @Category( { SlowTests.class, FastTests.class }) 49 * public static class B { 50 * @Test 51 * public void c() { 52 * 53 * } 54 * } 55 * 56 * @RunWith(Categories.class) 57 * @IncludeCategory(SlowTests.class) 58 * @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}