/** * */ package org.junit.experimental.categories; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.runner.Description; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.NoTestsRemainException; import org.junit.runners.Suite; import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; /** * From a given set of test classes, runs only the classes and methods that are * annotated with either the category given with the @IncludeCategory * annotation, or a subtype of that category. * * Note that, for now, annotating suites with {@code @Category} has no effect. * Categories must be annotated on the direct method or class. * * Example: * *
 * public interface FastTests {
 * }
 * 	
 * public interface SlowTests {
 * }
 * 
 * public static class A {
 * 	@Test
 * 	public void a() {
 * 		fail();
 * 	}
 * 
 * 	@Category(SlowTests.class)
 * 	@Test
 * 	public void b() {
 * 	}
 * }
 * 
 * @Category( { SlowTests.class, FastTests.class })
 * public static class B {
 * 	@Test
 * 	public void c() {
 * 
 * 	}
 * }
 * 
 * @RunWith(Categories.class)
 * @IncludeCategory(SlowTests.class)
 * @SuiteClasses( { A.class, B.class })
 * // Note that Categories is a kind of Suite
 * public static class SlowTestSuite {
 * }
 * 
*/ public class Categories extends Suite { // the way filters are implemented makes this unnecessarily complicated, // buggy, and difficult to specify. A new way of handling filters could // someday enable a better new implementation. // https://github.com/KentBeck/junit/issues/issue/172 @Retention(RetentionPolicy.RUNTIME) public @interface IncludeCategory { public Class value(); } @Retention(RetentionPolicy.RUNTIME) public @interface ExcludeCategory { public Class value(); } public static class CategoryFilter extends Filter { public static CategoryFilter include(Class categoryType) { return new CategoryFilter(categoryType, null); } private final Class fIncluded; private final Class fExcluded; public CategoryFilter(Class includedCategory, Class excludedCategory) { fIncluded= includedCategory; fExcluded= excludedCategory; } @Override public String describe() { return "category " + fIncluded; } @Override public boolean shouldRun(Description description) { if (hasCorrectCategoryAnnotation(description)) return true; for (Description each : description.getChildren()) if (shouldRun(each)) return true; return false; } private boolean hasCorrectCategoryAnnotation(Description description) { List> categories= categories(description); if (categories.isEmpty()) return fIncluded == null; for (Class each : categories) if (fExcluded != null && fExcluded.isAssignableFrom(each)) return false; for (Class each : categories) if (fIncluded == null || fIncluded.isAssignableFrom(each)) return true; return false; } private List> categories(Description description) { ArrayList> categories= new ArrayList>(); categories.addAll(Arrays.asList(directCategories(description))); categories.addAll(Arrays.asList(directCategories(parentDescription(description)))); return categories; } private Description parentDescription(Description description) { Class testClass= description.getTestClass(); if (testClass == null) return null; return Description.createSuiteDescription(testClass); } private Class[] directCategories(Description description) { if (description == null) return new Class[0]; Category annotation= description.getAnnotation(Category.class); if (annotation == null) return new Class[0]; return annotation.value(); } } public Categories(Class klass, RunnerBuilder builder) throws InitializationError { super(klass, builder); try { filter(new CategoryFilter(getIncludedCategory(klass), getExcludedCategory(klass))); } catch (NoTestsRemainException e) { throw new InitializationError(e); } assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription()); } private Class getIncludedCategory(Class klass) { IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class); return annotation == null ? null : annotation.value(); } private Class getExcludedCategory(Class klass) { ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class); return annotation == null ? null : annotation.value(); } private void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError { if (!canHaveCategorizedChildren(description)) assertNoDescendantsHaveCategoryAnnotations(description); for (Description each : description.getChildren()) assertNoCategorizedDescendentsOfUncategorizeableParents(each); } private void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError { for (Description each : description.getChildren()) { if (each.getAnnotation(Category.class) != null) throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods."); assertNoDescendantsHaveCategoryAnnotations(each); } } // If children have names like [0], our current magical category code can't determine their // parentage. private static boolean canHaveCategorizedChildren(Description description) { for (Description each : description.getChildren()) if (each.getTestClass() == null) return false; return true; } }