diff options
Diffstat (limited to 'src/main/java/org/junit/experimental/categories/Categories.java')
-rw-r--r-- | src/main/java/org/junit/experimental/categories/Categories.java | 505 |
1 files changed, 349 insertions, 156 deletions
diff --git a/src/main/java/org/junit/experimental/categories/Categories.java b/src/main/java/org/junit/experimental/categories/Categories.java index d57b4d3..290c180 100644 --- a/src/main/java/org/junit/experimental/categories/Categories.java +++ b/src/main/java/org/junit/experimental/categories/Categories.java @@ -1,13 +1,10 @@ -/** - * - */ 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 java.util.Collections; +import java.util.HashSet; +import java.util.Set; import org.junit.runner.Description; import org.junit.runner.manipulation.Filter; @@ -20,173 +17,369 @@ 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. - * + * <p> * Note that, for now, annotating suites with {@code @Category} has no effect. * Categories must be annotated on the direct method or class. - * + * <p> * Example: - * * <pre> * public interface FastTests { * } - * + * * public interface SlowTests { * } - * + * + * public interface SmokeTests + * } + * * public static class A { - * @Test - * public void a() { - * fail(); - * } - * - * @Category(SlowTests.class) - * @Test - * public void b() { - * } + * @Test + * public void a() { + * fail(); + * } + * + * @Category(SlowTests.class) + * @Test + * public void b() { + * } + * + * @Category({FastTests.class, SmokeTests.class}) + * @Test + * public void c() { + * } * } - * - * @Category( { SlowTests.class, FastTests.class }) + * + * @Category({SlowTests.class, FastTests.class}) * public static class B { - * @Test - * public void c() { - * - * } + * @Test + * public void d() { + * } * } - * + * * @RunWith(Categories.class) * @IncludeCategory(SlowTests.class) - * @SuiteClasses( { A.class, B.class }) + * @SuiteClasses({A.class, B.class}) * // Note that Categories is a kind of Suite * public static class SlowTestSuite { + * // Will run A.b and B.d, but not A.a and A.c + * } + * </pre> + * <p> + * Example to run multiple categories: + * <pre> + * @RunWith(Categories.class) + * @IncludeCategory({FastTests.class, SmokeTests.class}) + * @SuiteClasses({A.class, B.class}) + * public static class FastOrSmokeTestSuite { + * // Will run A.c and B.d, but not A.b because it is not any of FastTests or SmokeTests * } * </pre> + * + * @version 4.12 + * @see <a href="https://github.com/junit-team/junit/wiki/Categories">Categories at JUnit wiki</a> */ 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; - } -}
\ No newline at end of file + + @Retention(RetentionPolicy.RUNTIME) + public @interface IncludeCategory { + /** + * Determines the tests to run that are annotated with categories specified in + * the value of this annotation or their subtypes unless excluded with {@link ExcludeCategory}. + */ + public Class<?>[] value() default {}; + + /** + * If <tt>true</tt>, runs tests annotated with <em>any</em> of the categories in + * {@link IncludeCategory#value()}. Otherwise, runs tests only if annotated with <em>all</em> of the categories. + */ + public boolean matchAny() default true; + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface ExcludeCategory { + /** + * Determines the tests which do not run if they are annotated with categories specified in the + * value of this annotation or their subtypes regardless of being included in {@link IncludeCategory#value()}. + */ + public Class<?>[] value() default {}; + + /** + * If <tt>true</tt>, the tests annotated with <em>any</em> of the categories in {@link ExcludeCategory#value()} + * do not run. Otherwise, the tests do not run if and only if annotated with <em>all</em> categories. + */ + public boolean matchAny() default true; + } + + public static class CategoryFilter extends Filter { + private final Set<Class<?>> included; + private final Set<Class<?>> excluded; + private final boolean includedAny; + private final boolean excludedAny; + + public static CategoryFilter include(boolean matchAny, Class<?>... categories) { + if (hasNull(categories)) { + throw new NullPointerException("has null category"); + } + return categoryFilter(matchAny, createSet(categories), true, null); + } + + public static CategoryFilter include(Class<?> category) { + return include(true, category); + } + + public static CategoryFilter include(Class<?>... categories) { + return include(true, categories); + } + + public static CategoryFilter exclude(boolean matchAny, Class<?>... categories) { + if (hasNull(categories)) { + throw new NullPointerException("has null category"); + } + return categoryFilter(true, null, matchAny, createSet(categories)); + } + + public static CategoryFilter exclude(Class<?> category) { + return exclude(true, category); + } + + public static CategoryFilter exclude(Class<?>... categories) { + return exclude(true, categories); + } + + public static CategoryFilter categoryFilter(boolean matchAnyInclusions, Set<Class<?>> inclusions, + boolean matchAnyExclusions, Set<Class<?>> exclusions) { + return new CategoryFilter(matchAnyInclusions, inclusions, matchAnyExclusions, exclusions); + } + + protected CategoryFilter(boolean matchAnyIncludes, Set<Class<?>> includes, + boolean matchAnyExcludes, Set<Class<?>> excludes) { + includedAny = matchAnyIncludes; + excludedAny = matchAnyExcludes; + included = copyAndRefine(includes); + excluded = copyAndRefine(excludes); + } + + /** + * @see #toString() + */ + @Override + public String describe() { + return toString(); + } + + /** + * Returns string in the form <tt>"[included categories] - [excluded categories]"</tt>, where both + * sets have comma separated names of categories. + * + * @return string representation for the relative complement of excluded categories set + * in the set of included categories. Examples: + * <ul> + * <li> <tt>"categories [all]"</tt> for all included categories and no excluded ones; + * <li> <tt>"categories [all] - [A, B]"</tt> for all included categories and given excluded ones; + * <li> <tt>"categories [A, B] - [C, D]"</tt> for given included categories and given excluded ones. + * </ul> + * @see Class#toString() name of category + */ + @Override public String toString() { + StringBuilder description= new StringBuilder("categories ") + .append(included.isEmpty() ? "[all]" : included); + if (!excluded.isEmpty()) { + description.append(" - ").append(excluded); + } + return description.toString(); + } + + @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) { + final Set<Class<?>> childCategories= categories(description); + + // If a child has no categories, immediately return. + if (childCategories.isEmpty()) { + return included.isEmpty(); + } + + if (!excluded.isEmpty()) { + if (excludedAny) { + if (matchesAnyParentCategories(childCategories, excluded)) { + return false; + } + } else { + if (matchesAllParentCategories(childCategories, excluded)) { + return false; + } + } + } + + if (included.isEmpty()) { + // Couldn't be excluded, and with no suite's included categories treated as should run. + return true; + } else { + if (includedAny) { + return matchesAnyParentCategories(childCategories, included); + } else { + return matchesAllParentCategories(childCategories, included); + } + } + } + + /** + * @return <tt>true</tt> if at least one (any) parent category match a child, otherwise <tt>false</tt>. + * If empty <tt>parentCategories</tt>, returns <tt>false</tt>. + */ + private boolean matchesAnyParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) { + for (Class<?> parentCategory : parentCategories) { + if (hasAssignableTo(childCategories, parentCategory)) { + return true; + } + } + return false; + } + + /** + * @return <tt>false</tt> if at least one parent category does not match children, otherwise <tt>true</tt>. + * If empty <tt>parentCategories</tt>, returns <tt>true</tt>. + */ + private boolean matchesAllParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) { + for (Class<?> parentCategory : parentCategories) { + if (!hasAssignableTo(childCategories, parentCategory)) { + return false; + } + } + return true; + } + + private static Set<Class<?>> categories(Description description) { + Set<Class<?>> categories= new HashSet<Class<?>>(); + Collections.addAll(categories, directCategories(description)); + Collections.addAll(categories, directCategories(parentDescription(description))); + return categories; + } + + private static Description parentDescription(Description description) { + Class<?> testClass= description.getTestClass(); + return testClass == null ? null : Description.createSuiteDescription(testClass); + } + + private static Class<?>[] directCategories(Description description) { + if (description == null) { + return new Class<?>[0]; + } + + Category annotation= description.getAnnotation(Category.class); + return annotation == null ? new Class<?>[0] : annotation.value(); + } + + private static Set<Class<?>> copyAndRefine(Set<Class<?>> classes) { + HashSet<Class<?>> c= new HashSet<Class<?>>(); + if (classes != null) { + c.addAll(classes); + } + c.remove(null); + return c; + } + + private static boolean hasNull(Class<?>... classes) { + if (classes == null) return false; + for (Class<?> clazz : classes) { + if (clazz == null) { + return true; + } + } + return false; + } + } + + public Categories(Class<?> klass, RunnerBuilder builder) throws InitializationError { + super(klass, builder); + try { + Set<Class<?>> included= getIncludedCategory(klass); + Set<Class<?>> excluded= getExcludedCategory(klass); + boolean isAnyIncluded= isAnyIncluded(klass); + boolean isAnyExcluded= isAnyExcluded(klass); + + filter(CategoryFilter.categoryFilter(isAnyIncluded, included, isAnyExcluded, excluded)); + } catch (NoTestsRemainException e) { + throw new InitializationError(e); + } + assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription()); + } + + private static Set<Class<?>> getIncludedCategory(Class<?> klass) { + IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class); + return createSet(annotation == null ? null : annotation.value()); + } + + private static boolean isAnyIncluded(Class<?> klass) { + IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class); + return annotation == null || annotation.matchAny(); + } + + private static Set<Class<?>> getExcludedCategory(Class<?> klass) { + ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class); + return createSet(annotation == null ? null : annotation.value()); + } + + private static boolean isAnyExcluded(Class<?> klass) { + ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class); + return annotation == null || annotation.matchAny(); + } + + private static void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError { + if (!canHaveCategorizedChildren(description)) { + assertNoDescendantsHaveCategoryAnnotations(description); + } + for (Description each : description.getChildren()) { + assertNoCategorizedDescendentsOfUncategorizeableParents(each); + } + } + + private static 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; + } + + private static boolean hasAssignableTo(Set<Class<?>> assigns, Class<?> to) { + for (final Class<?> from : assigns) { + if (to.isAssignableFrom(from)) { + return true; + } + } + return false; + } + + private static Set<Class<?>> createSet(Class<?>... t) { + final Set<Class<?>> set= new HashSet<Class<?>>(); + if (t != null) { + Collections.addAll(set, t); + } + return set; + } +} |