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