Source code
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.
* <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() {
* }
*
* @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
* }
* </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/junit-team/junit/issues/issue/172
@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<?>> fIncluded;
private final Set<Class<?>> fExcluded;
private final boolean fIncludedAny;
private final boolean fExcludedAny;
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) {
fIncludedAny= matchAnyIncludes;
fExcludedAny= matchAnyExcludes;
fIncluded= copyAndRefine(includes);
fExcluded= 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(fIncluded.isEmpty() ? "[all]" : fIncluded);
if (!fExcluded.isEmpty()) {
description.append(" - ").append(fExcluded);
}
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 fIncluded.isEmpty();
}
if (!fExcluded.isEmpty()) {
if (fExcludedAny) {
if (matchesAnyParentCategories(childCategories, fExcluded)) {
return false;
}
} else {
if (matchesAllParentCategories(childCategories, fExcluded)) {
return false;
}
}
}
if (fIncluded.isEmpty()) {
// Couldn't be excluded, and with no suite's included categories treated as should run.
return true;
} else {
if (fIncludedAny) {
return matchesAnyParentCategories(childCategories, fIncluded);
} else {
return matchesAllParentCategories(childCategories, fIncluded);
}
}
}
/**
* @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;
}
}
Run 1
The code is well-structured and readable overall. It has clear naming conventions, logical method organization, and good use of helper methods to avoid duplication. The class-level Javadoc provides helpful context and examples. Nesting is mostly controlled, with methods broken down by responsibility. However, some issues reduce readability: the `hasCorrectCategoryAnnotation` method contains multi-level nested conditionals that could be flattened or extracted; several static helper methods like `parentDescription` and `directCategories` have null-based control flow that feels awkward; and the `toString` method uses a slightly compressed conditional expression. Also, the comment at the top explains why the filter logic is complicated, which helps set expectations. Overall, the code is readable for experienced developers but could be improved with minor refactoring.
Run 2
The code has clear structure with logical separation of annotations, filter logic, and helper methods. However, readability is hindered by deep nesting in the `hasCorrectCategoryAnnotation` method, where multiple conditionals are nested inside each other. The `filter` static factory methods are somewhat redundant and add length without clarity. Naming conventions like `fIncluded`, `fExcluded` are cryptic, and one-letter variable names like `c`, `t`, and `each` reduce clarity. Several methods are long and perform multiple actions, like the `Categories` constructor and the `toString` method. The code includes helpful Javadocs for the API, but the implementation logic is convoluted. Overall, the code is moderately readable but could benefit from flattening nesting, simplifying factory methods, and improving variable names.
Run 3
The code is well-structured with clear separation of concerns between the public API (Categories, annotations), the filter logic (CategoryFilter), and utility methods. Javadoc is thorough and examples clarify usage. Nesting is minimal and methods are generally concise, though a few methods (like hasCorrectCategoryAnnotation) contain multi-level conditionals that slightly reduce readability. Naming is descriptive and action granularity is mostly one operation per line. The code follows established patterns from the JUnit framework. Some minor issues include inconsistent spacing around the assignment operator (e.g., 'fIncludedAny= matchAnyIncludes') and the 'hasNull' method could be simplified with a stream. The overall intent—filtering tests by category annotations—is clear, but the logic for handling includes/excludes requires careful reading. The length is justified by the complexity, though the interplay between recursive filtering and category matching could be better explained.
Run 1
Code Structure: Logically organized into annotations, filtering logic, suite initialization, and focused helpers, though helper placement requires some navigation. Nesting: Mostly shallow, with early returns; category inclusion/exclusion logic has avoidable nested conditionals. Clarity of intent: Descriptive method names and extensive documentation explain behavior well, but f-prefixed fields, abbreviated variables, and exceptionally long validation names reduce clarity. Code length: Small methods are generally concise, although repeated overloads, annotation lookups, and lengthy examples increase the reading burden. Action granularity: Almost every statement performs one action; chained builder calls and an inline conditional return are minor exceptions to the otherwise consistent presentation. Reading flow: Public entry points lead naturally into supporting logic, but recursive traversal, scattered helpers, inconsistent spacing, and redundant else blocks occasionally interrupt the flow.
Run 2
Code Structure: Logically organized into annotations, filtering logic, runner initialization, and focused helpers, though numerous overloads and utilities add navigation overhead. Nesting: Mostly shallow with early returns, but inclusion/exclusion matching contains avoidable nested conditionals. Clarity of intent: Descriptive method names and extensive documentation explain category behavior; abbreviated variables, prefixed fields, and exceptionally long validation names reduce clarity. Code length: Most methods are concise, but lengthy examples, repeated annotation retrieval, and parallel matching logic increase overall size. Action granularity: Generally one action per line, with only minor compact formatting such as an inline null guard and chained builder calls. Reading flow: Public factories lead naturally into implementation and helpers, but inconsistent spacing, redundant else branches after returns, and recursive validation require additional attention.
Run 3
Code Structure: Well organized into annotations, filtering logic, suite initialization, and focused helpers, although numerous overloads and scattered utilities add navigation overhead. Nesting: Generally shallow, with early returns; category inclusion/exclusion logic has avoidable three-level conditionals. Clarity of intent: Descriptive method names and extensive documentation explain the behavior, but abbreviated variables, prefixed fields, boolean-heavy calls, and exceptionally long validation names reduce clarity. Code length: Most methods are concise; repeated include/exclude logic and lengthy documentation make the overall file somewhat verbose. Action granularity: Statements generally perform one action per line, with only minor compact formatting and chained calls. Reading flow: Related methods are mostly grouped naturally, and recursive traversal is straightforward, but inconsistent spacing, redundant else branches, and jumps between inner-class logic and outer helpers interrupt the flow.