1
+ // Copied from https://github.com/junit-team/junit4/pull/1663/files until the junit team merges the PR.
2
+ package org .junit .experimental .categories ;
3
+
4
+ import java .lang .annotation .Retention ;
5
+ import java .lang .annotation .RetentionPolicy ;
6
+ import java .util .Arrays ;
7
+ import java .util .Collections ;
8
+ import java .util .HashSet ;
9
+ import java .util .LinkedHashSet ;
10
+ import java .util .Set ;
11
+
12
+ import org .junit .runner .Description ;
13
+ import org .junit .runner .manipulation .Filter ;
14
+ import org .junit .runner .manipulation .NoTestsRemainException ;
15
+ import org .junit .runners .Suite ;
16
+ import org .junit .runners .model .InitializationError ;
17
+ import org .junit .runners .model .RunnerBuilder ;
18
+
19
+ /**
20
+ * From a given set of test classes, runs only the classes and methods that are
21
+ * annotated with either the category given with the @IncludeCategory
22
+ * annotation, or a subtype of that category.
23
+ * <p>
24
+ * Note that, for now, annotating suites with {@code @Category} has no effect.
25
+ * Categories must be annotated on the direct method or class.
26
+ * <p>
27
+ * Example:
28
+ * <pre>
29
+ * public interface FastTests {
30
+ * }
31
+ *
32
+ * public interface SlowTests {
33
+ * }
34
+ *
35
+ * public interface SmokeTests
36
+ * }
37
+ *
38
+ * public static class A {
39
+ * @Test
40
+ * public void a() {
41
+ * fail();
42
+ * }
43
+ *
44
+ * @Category(SlowTests.class)
45
+ * @Test
46
+ * public void b() {
47
+ * }
48
+ *
49
+ * @Category({FastTests.class, SmokeTests.class})
50
+ * @Test
51
+ * public void c() {
52
+ * }
53
+ * }
54
+ *
55
+ * @Category({SlowTests.class, FastTests.class})
56
+ * public static class B {
57
+ * @Test
58
+ * public void d() {
59
+ * }
60
+ * }
61
+ *
62
+ * @RunWith(Categories.class)
63
+ * @IncludeCategory(SlowTests.class)
64
+ * @SuiteClasses({A.class, B.class})
65
+ * // Note that Categories is a kind of Suite
66
+ * public static class SlowTestSuite {
67
+ * // Will run A.b and B.d, but not A.a and A.c
68
+ * }
69
+ * </pre>
70
+ * <p>
71
+ * Example to run multiple categories:
72
+ * <pre>
73
+ * @RunWith(Categories.class)
74
+ * @IncludeCategory({FastTests.class, SmokeTests.class})
75
+ * @SuiteClasses({A.class, B.class})
76
+ * public static class FastOrSmokeTestSuite {
77
+ * // Will run A.c and B.d, but not A.b because it is not any of FastTests or SmokeTests
78
+ * }
79
+ * </pre>
80
+ *
81
+ * @version 4.12
82
+ * @see <a href="https://github.com/junit-team/junit4/wiki/Categories">Categories at JUnit wiki</a>
83
+ */
84
+ public class Categories extends Suite {
85
+
86
+ @ Retention (RetentionPolicy .RUNTIME )
87
+ public @interface IncludeCategory {
88
+ /**
89
+ * Determines the tests to run that are annotated with categories specified in
90
+ * the value of this annotation or their subtypes unless excluded with {@link ExcludeCategory}.
91
+ */
92
+ Class <?>[] value () default {};
93
+
94
+ /**
95
+ * If <tt>true</tt>, runs tests annotated with <em>any</em> of the categories in
96
+ * {@link IncludeCategory#value()}. Otherwise, runs tests only if annotated with <em>all</em> of the categories.
97
+ */
98
+ boolean matchAny () default true ;
99
+ }
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
+ 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
+ boolean matchAny () default true ;
114
+ }
115
+
116
+ public static class CategoryFilter extends Filter {
117
+ private final Set <Class <?>> included ;
118
+ private final Set <Class <?>> excluded ;
119
+ private final boolean includedAny ;
120
+ private final boolean excludedAny ;
121
+
122
+ public static CategoryFilter include (boolean matchAny , Class <?>... categories ) {
123
+ return new CategoryFilter (matchAny , 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
+ return new CategoryFilter (true , null , matchAny , categories );
136
+ }
137
+
138
+ public static CategoryFilter exclude (Class <?> category ) {
139
+ return exclude (true , category );
140
+ }
141
+
142
+ public static CategoryFilter exclude (Class <?>... categories ) {
143
+ return exclude (true , categories );
144
+ }
145
+
146
+ public static CategoryFilter categoryFilter (boolean matchAnyInclusions , Set <Class <?>> inclusions ,
147
+ boolean matchAnyExclusions , Set <Class <?>> exclusions ) {
148
+ return new CategoryFilter (matchAnyInclusions , inclusions , matchAnyExclusions , exclusions );
149
+ }
150
+
151
+ @ Deprecated
152
+ public CategoryFilter (Class <?> includedCategory , Class <?> excludedCategory ) {
153
+ includedAny = true ;
154
+ excludedAny = true ;
155
+ included = nullableClassToSet (includedCategory );
156
+ excluded = nullableClassToSet (excludedCategory );
157
+ }
158
+
159
+ protected CategoryFilter (boolean matchAnyIncludes , Set <Class <?>> includes ,
160
+ boolean matchAnyExcludes , Set <Class <?>> excludes ) {
161
+ includedAny = matchAnyIncludes ;
162
+ excludedAny = matchAnyExcludes ;
163
+ included = copyAndRefine (includes );
164
+ excluded = copyAndRefine (excludes );
165
+ }
166
+
167
+ private CategoryFilter (boolean matchAnyIncludes , Class <?>[] inclusions ,
168
+ boolean matchAnyExcludes , Class <?>[] exclusions ) {
169
+ includedAny = matchAnyIncludes ;
170
+ excludedAny = matchAnyExcludes ;
171
+ included = createSet (inclusions );
172
+ excluded = createSet (exclusions );
173
+ }
174
+
175
+ /**
176
+ * @see #toString()
177
+ */
178
+ @ Override
179
+ public String describe () {
180
+ return toString ();
181
+ }
182
+
183
+ /**
184
+ * Returns string in the form <tt>"[included categories] - [excluded categories]"</tt>, where both
185
+ * sets have comma separated names of categories.
186
+ *
187
+ * @return string representation for the relative complement of excluded categories set
188
+ * in the set of included categories. Examples:
189
+ * <ul>
190
+ * <li> <tt>"categories [all]"</tt> for all included categories and no excluded ones;
191
+ * <li> <tt>"categories [all] - [A, B]"</tt> for all included categories and given excluded ones;
192
+ * <li> <tt>"categories [A, B] - [C, D]"</tt> for given included categories and given excluded ones.
193
+ * </ul>
194
+ * @see Class#toString() name of category
195
+ */
196
+ @ Override public String toString () {
197
+ StringBuilder description = new StringBuilder ("categories " )
198
+ .append (included .isEmpty () ? "[all]" : included );
199
+ if (!excluded .isEmpty ()) {
200
+ description .append (" - " ).append (excluded );
201
+ }
202
+ return description .toString ();
203
+ }
204
+
205
+ @ Override
206
+ public boolean shouldRun (Description description ) {
207
+ if (hasCorrectCategoryAnnotation (description )) {
208
+ return true ;
209
+ }
210
+
211
+ for (Description each : description .getChildren ()) {
212
+ if (shouldRun (each )) {
213
+ return true ;
214
+ }
215
+ }
216
+
217
+ return false ;
218
+ }
219
+
220
+ private boolean hasCorrectCategoryAnnotation (Description description ) {
221
+ final Set <Class <?>> childCategories = categories (description );
222
+
223
+ // If a child has no categories, immediately return.
224
+ if (childCategories .isEmpty ()) {
225
+ return included .isEmpty ();
226
+ }
227
+
228
+ if (!excluded .isEmpty ()) {
229
+ if (excludedAny ) {
230
+ if (matchesAnyParentCategories (childCategories , excluded )) {
231
+ return false ;
232
+ }
233
+ } else {
234
+ if (matchesAllParentCategories (childCategories , excluded )) {
235
+ return false ;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (included .isEmpty ()) {
241
+ // Couldn't be excluded, and with no suite's included categories treated as should run.
242
+ return true ;
243
+ } else {
244
+ if (includedAny ) {
245
+ return matchesAnyParentCategories (childCategories , included );
246
+ } else {
247
+ return matchesAllParentCategories (childCategories , included );
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * @return <tt>true</tt> if at least one (any) parent category match a child, otherwise <tt>false</tt>.
254
+ * If empty <tt>parentCategories</tt>, returns <tt>false</tt>.
255
+ */
256
+ private boolean matchesAnyParentCategories (Set <Class <?>> childCategories , Set <Class <?>> parentCategories ) {
257
+ for (Class <?> parentCategory : parentCategories ) {
258
+ if (hasAssignableTo (childCategories , parentCategory )) {
259
+ return true ;
260
+ }
261
+ }
262
+ return false ;
263
+ }
264
+
265
+ /**
266
+ * @return <tt>false</tt> if at least one parent category does not match children, otherwise <tt>true</tt>.
267
+ * If empty <tt>parentCategories</tt>, returns <tt>true</tt>.
268
+ */
269
+ private boolean matchesAllParentCategories (Set <Class <?>> childCategories , Set <Class <?>> parentCategories ) {
270
+ for (Class <?> parentCategory : parentCategories ) {
271
+ if (!hasAssignableTo (childCategories , parentCategory )) {
272
+ return false ;
273
+ }
274
+ }
275
+ return true ;
276
+ }
277
+
278
+ private static Set <Class <?>> categories (Description description ) {
279
+ Set <Class <?>> categories = new HashSet <Class <?>>();
280
+ Collections .addAll (categories , directCategories (description ));
281
+ Collections .addAll (categories , directCategories (parentDescription (description )));
282
+ Collections .addAll (categories , directCategories (declaringDescription (description )));
283
+ return categories ;
284
+ }
285
+
286
+ private static Description parentDescription (Description description ) {
287
+ Class <?> testClass = description .getTestClass ();
288
+ return testClass == null ? null : Description .createSuiteDescription (testClass );
289
+ }
290
+
291
+ private static Description declaringDescription (Description description ) {
292
+ Class <?> testClass = description .getTestClass ();
293
+ if (testClass != null ) {
294
+ testClass = testClass .getDeclaringClass ();
295
+ }
296
+ return testClass == null ? null : Description .createSuiteDescription (testClass );
297
+ }
298
+
299
+ private static Class <?>[] directCategories (Description description ) {
300
+ if (description == null ) {
301
+ return new Class <?>[0 ];
302
+ }
303
+
304
+ Category annotation = description .getAnnotation (Category .class );
305
+ return annotation == null ? new Class <?>[0 ] : annotation .value ();
306
+ }
307
+
308
+ private static Set <Class <?>> copyAndRefine (Set <Class <?>> classes ) {
309
+ Set <Class <?>> c = new LinkedHashSet <Class <?>>();
310
+ if (classes != null ) {
311
+ c .addAll (classes );
312
+ }
313
+ c .remove (null );
314
+ return c ;
315
+ }
316
+ }
317
+
318
+ public Categories (Class <?> klass , RunnerBuilder builder ) throws InitializationError {
319
+ super (klass , builder );
320
+ try {
321
+ Set <Class <?>> included = getIncludedCategory (klass );
322
+ Set <Class <?>> excluded = getExcludedCategory (klass );
323
+ boolean isAnyIncluded = isAnyIncluded (klass );
324
+ boolean isAnyExcluded = isAnyExcluded (klass );
325
+
326
+ filter (CategoryFilter .categoryFilter (isAnyIncluded , included , isAnyExcluded , excluded ));
327
+ } catch (NoTestsRemainException e ) {
328
+ throw new InitializationError (e );
329
+ }
330
+ }
331
+
332
+ private static Set <Class <?>> getIncludedCategory (Class <?> klass ) {
333
+ IncludeCategory annotation = klass .getAnnotation (IncludeCategory .class );
334
+ return createSet (annotation == null ? null : annotation .value ());
335
+ }
336
+
337
+ private static boolean isAnyIncluded (Class <?> klass ) {
338
+ IncludeCategory annotation = klass .getAnnotation (IncludeCategory .class );
339
+ return annotation == null || annotation .matchAny ();
340
+ }
341
+
342
+ private static Set <Class <?>> getExcludedCategory (Class <?> klass ) {
343
+ ExcludeCategory annotation = klass .getAnnotation (ExcludeCategory .class );
344
+ return createSet (annotation == null ? null : annotation .value ());
345
+ }
346
+
347
+ private static boolean isAnyExcluded (Class <?> klass ) {
348
+ ExcludeCategory annotation = klass .getAnnotation (ExcludeCategory .class );
349
+ return annotation == null || annotation .matchAny ();
350
+ }
351
+
352
+ private static boolean hasAssignableTo (Set <Class <?>> assigns , Class <?> to ) {
353
+ for (final Class <?> from : assigns ) {
354
+ if (to .isAssignableFrom (from )) {
355
+ return true ;
356
+ }
357
+ }
358
+ return false ;
359
+ }
360
+
361
+ private static Set <Class <?>> createSet (Class <?>[] classes ) {
362
+ // Not throwing a NPE if t is null is a bad idea, but it's the behavior from JUnit 4.12
363
+ // for include(boolean, Class<?>...) and exclude(boolean, Class<?>...)
364
+ if (classes == null || classes .length == 0 ) {
365
+ return Collections .emptySet ();
366
+ }
367
+ for (Class <?> category : classes ) {
368
+ if (category == null ) {
369
+ throw new NullPointerException ("has null category" );
370
+ }
371
+ }
372
+
373
+ return classes .length == 1
374
+ ? Collections .<Class <?>>singleton (classes [0 ])
375
+ : new LinkedHashSet <Class <?>>(Arrays .asList (classes ));
376
+ }
377
+
378
+ private static Set <Class <?>> nullableClassToSet (Class <?> nullableClass ) {
379
+ // Not throwing a NPE if t is null is a bad idea, but it's the behavior from JUnit 4.11
380
+ // for CategoryFilter(Class<?> includedCategory, Class<?> excludedCategory)
381
+ return nullableClass == null
382
+ ? Collections .<Class <?>>emptySet ()
383
+ : Collections .<Class <?>>singleton (nullableClass );
384
+ }
385
+ }
0 commit comments