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