001package org.junit.experimental.categories; 002 003import java.lang.annotation.Retention; 004import java.lang.annotation.RetentionPolicy; 005import java.util.Set; 006import java.util.Collections; 007import java.util.HashSet; 008import org.junit.runner.Description; 009import org.junit.runner.manipulation.Filter; 010import org.junit.runner.manipulation.NoTestsRemainException; 011import org.junit.runners.Suite; 012import org.junit.runners.model.InitializationError; 013import org.junit.runners.model.RunnerBuilder; 014 015/** 016 * From a given set of test classes, runs only the classes and methods that are 017 * annotated with either the category given with the @IncludeCategory 018 * annotation, or a subtype of that category. 019 * <p> 020 * Note that, for now, annotating suites with {@code @Category} has no effect. 021 * Categories must be annotated on the direct method or class. 022 * <p> 023 * Example: 024 * <pre> 025 * public interface FastTests { 026 * } 027 * 028 * public interface SlowTests { 029 * } 030 * 031 * public interface SmokeTests 032 * } 033 * 034 * public static class A { 035 * @Test 036 * public void a() { 037 * fail(); 038 * } 039 * 040 * @Category(SlowTests.class) 041 * @Test 042 * public void b() { 043 * } 044 * 045 * @Category({FastTests.class, SmokeTests.class}) 046 * @Test 047 * public void c() { 048 * } 049 * } 050 * 051 * @Category({SlowTests.class, FastTests.class}) 052 * public static class B { 053 * @Test 054 * public void d() { 055 * } 056 * } 057 * 058 * @RunWith(Categories.class) 059 * @IncludeCategory(SlowTests.class) 060 * @SuiteClasses({A.class, B.class}) 061 * // Note that Categories is a kind of Suite 062 * public static class SlowTestSuite { 063 * // Will run A.b and B.d, but not A.a and A.c 064 * } 065 * </pre> 066 * 067 * Example to run multiple categories: 068 * <pre> 069 * @RunWith(Categories.class) 070 * @IncludeCategory({FastTests.class, SmokeTests.class}) 071 * @SuiteClasses({A.class, B.class}) 072 * public static class FastOrSmokeTestSuite { 073 * // Will run A.c and B.d, but not A.b because it is not any of FastTests or SmokeTests 074 * } 075 * </pre> 076 * 077 * @version 4.12 078 * @see <a href="https://github.com/KentBeck/junit/wiki/Categories">Categories at JUnit wiki</a> 079 */ 080public class Categories extends Suite { 081 // the way filters are implemented makes this unnecessarily complicated, 082 // buggy, and difficult to specify. A new way of handling filters could 083 // someday enable a better new implementation. 084 // https://github.com/KentBeck/junit/issues/issue/172 085 086 @Retention(RetentionPolicy.RUNTIME) 087 public @interface IncludeCategory { 088 /** 089 * Determines the tests to run that are annotated with categories specified in 090 * the value of this annotation or their subtypes unless excluded with {@link ExcludeCategory}. 091 */ 092 public Class<?>[] value() default {}; 093 094 /** 095 * If <tt>true</tt>, runs tests annotated with <em>any</em> of the categories in 096 * {@link IncludeCategory#value()}. Otherwise, runs tests only if annotated with <em>all</em> of the categories. 097 */ 098 public boolean matchAny() default true; 099 } 100 101 @Retention(RetentionPolicy.RUNTIME) 102 public @interface ExcludeCategory { 103 /** 104 * Determines the tests which do not run if they are annotated with categories specified in the 105 * value of this annotation or their subtypes regardless of being included in {@link IncludeCategory#value()}. 106 */ 107 public Class<?>[] value() default {}; 108 109 /** 110 * If <tt>true</tt>, the tests annotated with <em>any</em> of the categories in {@link ExcludeCategory#value()} 111 * do not run. Otherwise, the tests do not run if and only if annotated with <em>all</em> categories. 112 */ 113 public boolean matchAny() default true; 114 } 115 116 public static class CategoryFilter extends Filter { 117 private final Set<Class<?>> fIncluded; 118 private final Set<Class<?>> fExcluded; 119 private final boolean fIncludedAny; 120 private final boolean fExcludedAny; 121 122 public static CategoryFilter include(boolean matchAny, Class<?>... categories) { 123 if (hasNull(categories)) { 124 throw new NullPointerException("has null category"); 125 } 126 return categoryFilter(matchAny, createSet(categories), true, null); 127 } 128 129 public static CategoryFilter include(Class<?> category) { 130 return include(true, category); 131 } 132 133 public static CategoryFilter include(Class<?>... categories) { 134 return include(true, categories); 135 } 136 137 public static CategoryFilter exclude(boolean matchAny, Class<?>... categories) { 138 if (hasNull(categories)) { 139 throw new NullPointerException("has null category"); 140 } 141 return categoryFilter(true, null, matchAny, createSet(categories)); 142 } 143 144 public static CategoryFilter exclude(Class<?> category) { 145 return exclude(true, category); 146 } 147 148 public static CategoryFilter exclude(Class<?>... categories) { 149 return exclude(true, categories); 150 } 151 152 public static CategoryFilter categoryFilter(boolean matchAnyInclusions, Set<Class<?>> inclusions, 153 boolean matchAnyExclusions, Set<Class<?>> exclusions) { 154 return new CategoryFilter(matchAnyInclusions, inclusions, matchAnyExclusions, exclusions); 155 } 156 157 private CategoryFilter(boolean matchAnyIncludes, Set<Class<?>> includes, 158 boolean matchAnyExcludes, Set<Class<?>> excludes) { 159 fIncludedAny= matchAnyIncludes; 160 fExcludedAny= matchAnyExcludes; 161 fIncluded= copyAndRefine(includes); 162 fExcluded= copyAndRefine(excludes); 163 } 164 165 /** 166 * @see #toString() 167 */ 168 @Override 169 public String describe() { 170 return toString(); 171 } 172 173 /** 174 * Returns string in the form <tt>"[included categories] - [excluded categories]"</tt>, where both 175 * sets have comma separated names of categories. 176 * 177 * @return string representation for the relative complement of excluded categories set 178 * in the set of included categories. Examples: 179 * <ul> 180 * <li> <tt>"categories [all]"</tt> for all included categories and no excluded ones; 181 * <li> <tt>"categories [all] - [A, B]"</tt> for all included categories and given excluded ones; 182 * <li> <tt>"categories [A, B] - [C, D]"</tt> for given included categories and given excluded ones. 183 * </ul> 184 * @see Class#toString() name of category 185 */ 186 @Override public String toString() { 187 StringBuilder description= new StringBuilder("categories ") 188 .append(fIncluded.isEmpty() ? "[all]" : fIncluded); 189 if (!fExcluded.isEmpty()) { 190 description.append(" - ").append(fExcluded); 191 } 192 return description.toString(); 193 } 194 195 @Override 196 public boolean shouldRun(Description description) { 197 if (hasCorrectCategoryAnnotation(description)) { 198 return true; 199 } 200 201 for (Description each : description.getChildren()) { 202 if (shouldRun(each)) { 203 return true; 204 } 205 } 206 207 return false; 208 } 209 210 private boolean hasCorrectCategoryAnnotation(Description description) { 211 final Set<Class<?>> childCategories= categories(description); 212 213 // If a child has no categories, immediately return. 214 if (childCategories.isEmpty()) { 215 return fIncluded.isEmpty(); 216 } 217 218 if (!fExcluded.isEmpty()) { 219 if (fExcludedAny) { 220 if (matchesAnyParentCategories(childCategories, fExcluded)) { 221 return false; 222 } 223 } else { 224 if (matchesAllParentCategories(childCategories, fExcluded)) { 225 return false; 226 } 227 } 228 } 229 230 if (fIncluded.isEmpty()) { 231 // Couldn't be excluded, and with no suite's included categories treated as should run. 232 return true; 233 } else { 234 if (fIncludedAny) { 235 return matchesAnyParentCategories(childCategories, fIncluded); 236 } else { 237 return matchesAllParentCategories(childCategories, fIncluded); 238 } 239 } 240 } 241 242 /** 243 * @return <tt>true</tt> if at least one (any) parent category match a child, otherwise <tt>false</tt>. 244 * If empty <tt>parentCategories</tt>, returns <tt>false</tt>. 245 */ 246 private boolean matchesAnyParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) { 247 for (Class<?> parentCategory : parentCategories) { 248 if (hasAssignableTo(childCategories, parentCategory)) { 249 return true; 250 } 251 } 252 return false; 253 } 254 255 /** 256 * @return <tt>false</tt> if at least one parent category does not match children, otherwise <tt>true</tt>. 257 * If empty <tt>parentCategories</tt>, returns <tt>true</tt>. 258 */ 259 private boolean matchesAllParentCategories(Set<Class<?>> childCategories, Set<Class<?>> parentCategories) { 260 for (Class<?> parentCategory : parentCategories) { 261 if (!hasAssignableTo(childCategories, parentCategory)) { 262 return false; 263 } 264 } 265 return true; 266 } 267 268 private static Set<Class<?>> categories(Description description) { 269 Set<Class<?>> categories= new HashSet<Class<?>>(); 270 Collections.addAll(categories, directCategories(description)); 271 Collections.addAll(categories, directCategories(parentDescription(description))); 272 return categories; 273 } 274 275 private static Description parentDescription(Description description) { 276 Class<?> testClass= description.getTestClass(); 277 return testClass == null ? null : Description.createSuiteDescription(testClass); 278 } 279 280 private static Class<?>[] directCategories(Description description) { 281 if (description == null) { 282 return new Class<?>[0]; 283 } 284 285 Category annotation= description.getAnnotation(Category.class); 286 return annotation == null ? new Class<?>[0] : annotation.value(); 287 } 288 289 private static Set<Class<?>> copyAndRefine(Set<Class<?>> classes) { 290 HashSet<Class<?>> c= new HashSet<Class<?>>(); 291 if (classes != null) { 292 c.addAll(classes); 293 } 294 c.remove(null); 295 return c; 296 } 297 298 private static boolean hasNull(Class<?>... classes) { 299 if (classes == null) return false; 300 for (Class<?> clazz : classes) { 301 if (clazz == null) { 302 return true; 303 } 304 } 305 return false; 306 } 307 } 308 309 public Categories(Class<?> klass, RunnerBuilder builder) throws InitializationError { 310 super(klass, builder); 311 try { 312 Set<Class<?>> included= getIncludedCategory(klass); 313 Set<Class<?>> excluded= getExcludedCategory(klass); 314 boolean isAnyIncluded= isAnyIncluded(klass); 315 boolean isAnyExcluded= isAnyExcluded(klass); 316 317 filter(CategoryFilter.categoryFilter(isAnyIncluded, included, isAnyExcluded, excluded)); 318 } catch (NoTestsRemainException e) { 319 throw new InitializationError(e); 320 } catch (ClassNotFoundException e) { 321 throw new InitializationError(e); 322 } 323 assertNoCategorizedDescendentsOfUncategorizeableParents(getDescription()); 324 } 325 326 private static Set<Class<?>> getIncludedCategory(Class<?> klass) throws ClassNotFoundException { 327 IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class); 328 return createSet(annotation == null ? null : annotation.value()); 329 } 330 331 private static boolean isAnyIncluded(Class<?> klass) { 332 IncludeCategory annotation= klass.getAnnotation(IncludeCategory.class); 333 return annotation == null || annotation.matchAny(); 334 } 335 336 private static Set<Class<?>> getExcludedCategory(Class<?> klass) throws ClassNotFoundException { 337 ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class); 338 return createSet(annotation == null ? null : annotation.value()); 339 } 340 341 private static boolean isAnyExcluded(Class<?> klass) { 342 ExcludeCategory annotation= klass.getAnnotation(ExcludeCategory.class); 343 return annotation == null || annotation.matchAny(); 344 } 345 346 private static void assertNoCategorizedDescendentsOfUncategorizeableParents(Description description) throws InitializationError { 347 if (!canHaveCategorizedChildren(description)) { 348 assertNoDescendantsHaveCategoryAnnotations(description); 349 } 350 for (Description each : description.getChildren()) { 351 assertNoCategorizedDescendentsOfUncategorizeableParents(each); 352 } 353 } 354 355 private static void assertNoDescendantsHaveCategoryAnnotations(Description description) throws InitializationError { 356 for (Description each : description.getChildren()) { 357 if (each.getAnnotation(Category.class) != null) { 358 throw new InitializationError("Category annotations on Parameterized classes are not supported on individual methods."); 359 } 360 assertNoDescendantsHaveCategoryAnnotations(each); 361 } 362 } 363 364 // If children have names like [0], our current magical category code can't determine their parentage. 365 private static boolean canHaveCategorizedChildren(Description description) { 366 for (Description each : description.getChildren()) { 367 if (each.getTestClass() == null) { 368 return false; 369 } 370 } 371 return true; 372 } 373 374 private static boolean hasAssignableTo(Set<Class<?>> assigns, Class<?> to) { 375 for (final Class<?> from : assigns) { 376 if (to.isAssignableFrom(from)) { 377 return true; 378 } 379 } 380 return false; 381 } 382 383 private static Set<Class<?>> createSet(Class<?>... t) { 384 final Set<Class<?>> set= new HashSet<Class<?>>(); 385 if (t != null) { 386 Collections.addAll(set, t); 387 } 388 return set; 389 } 390}