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: * * <pre> * 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 { * } * </pre> */ 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<Class<?>> 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<Class<?>> categories(Description description) { ArrayList<Class<?>> categories = new ArrayList<Class<?>>(); 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; } }