org.junit.experimental.categories.Categories Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of junit Show documentation
Show all versions of junit Show documentation
JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck.
package org.junit.experimental.categories;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
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 interface SmokeTests
* }
*
* public static class A {
* @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})
* public static class B {
* @Test
* public void d() {
* }
* }
*
* @RunWith(Categories.class)
* @IncludeCategory(SlowTests.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
* }
*
*
* Example to run multiple categories:
*
* @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
* }
*
*
* @version 4.12
* @see Categories at JUnit wiki
*/
public class Categories extends Suite {
@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 true, runs tests annotated with any of the categories in
* {@link IncludeCategory#value()}. Otherwise, runs tests only if annotated with all 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 true, the tests annotated with any of the categories in {@link ExcludeCategory#value()}
* do not run. Otherwise, the tests do not run if and only if annotated with all categories.
*/
public boolean matchAny() default true;
}
public static class CategoryFilter extends Filter {
private final Set> included;
private final Set> 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> inclusions,
boolean matchAnyExclusions, Set> exclusions) {
return new CategoryFilter(matchAnyInclusions, inclusions, matchAnyExclusions, exclusions);
}
protected CategoryFilter(boolean matchAnyIncludes, Set> includes,
boolean matchAnyExcludes, Set> excludes) {
includedAny = matchAnyIncludes;
excludedAny = matchAnyExcludes;
included = copyAndRefine(includes);
excluded = copyAndRefine(excludes);
}
/**
* @see #toString()
*/
@Override
public String describe() {
return toString();
}
/**
* Returns string in the form "[included categories] - [excluded categories]", 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:
*
* - "categories [all]" for all included categories and no excluded ones;
*
- "categories [all] - [A, B]" for all included categories and given excluded ones;
*
- "categories [A, B] - [C, D]" for given included categories and given excluded ones.
*
* @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> 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 true if at least one (any) parent category match a child, otherwise false.
* If empty parentCategories, returns false.
*/
private boolean matchesAnyParentCategories(Set> childCategories, Set> parentCategories) {
for (Class parentCategory : parentCategories) {
if (hasAssignableTo(childCategories, parentCategory)) {
return true;
}
}
return false;
}
/**
* @return false if at least one parent category does not match children, otherwise true.
* If empty parentCategories, returns true.
*/
private boolean matchesAllParentCategories(Set> childCategories, Set> parentCategories) {
for (Class parentCategory : parentCategories) {
if (!hasAssignableTo(childCategories, parentCategory)) {
return false;
}
}
return true;
}
private static Set> categories(Description description) {
Set> categories= new HashSet>();
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> copyAndRefine(Set> classes) {
HashSet> c= new HashSet>();
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> included= getIncludedCategory(klass);
Set> 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> 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> 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> assigns, Class to) {
for (final Class from : assigns) {
if (to.isAssignableFrom(from)) {
return true;
}
}
return false;
}
private static Set> createSet(Class... t) {
final Set> set= new HashSet>();
if (t != null) {
Collections.addAll(set, t);
}
return set;
}
}