Skip to content

Commit da0a727

Browse files
author
David Saff
committed
Merge pull request #684 from codingricky/annotation-validators
Add AnnotationValidator, and validation checks for Category (fix for issue #93)
2 parents a71f8c1 + d905414 commit da0a727

File tree

11 files changed

+640
-21
lines changed

11 files changed

+640
-21
lines changed

src/main/java/org/junit/experimental/categories/Category.java

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.lang.annotation.Retention;
55
import java.lang.annotation.RetentionPolicy;
66

7+
import org.junit.validator.ValidateWith;
8+
79
/**
810
* Marks a test class or test method as belonging to one or more categories of tests.
911
* The value is an array of arbitrary classes.
@@ -40,6 +42,7 @@
4042
*/
4143
@Retention(RetentionPolicy.RUNTIME)
4244
@Inherited
45+
@ValidateWith(CategoryValidator.class)
4346
public @interface Category {
4447
Class<?>[] value();
4548
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.junit.experimental.categories;
2+
3+
import static java.util.Collections.unmodifiableList;
4+
import static java.util.Collections.unmodifiableSet;
5+
import static java.util.Arrays.asList;
6+
7+
import java.lang.annotation.Annotation;
8+
import java.util.ArrayList;
9+
import java.util.HashSet;
10+
import java.util.List;
11+
import java.util.Set;
12+
13+
import org.junit.After;
14+
import org.junit.AfterClass;
15+
import org.junit.Before;
16+
import org.junit.BeforeClass;
17+
import org.junit.runners.model.FrameworkMethod;
18+
import org.junit.validator.AnnotationValidator;
19+
20+
/**
21+
* Validates that there are no errors in the use of the {@code Category}
22+
* annotation. If there is, a {@code Throwable} object will be added to the list
23+
* of errors.
24+
*
25+
* @since 4.12
26+
*/
27+
public final class CategoryValidator extends AnnotationValidator {
28+
29+
private static final Set<Class<? extends Annotation>> INCOMPATIBLE_ANNOTATIONS = unmodifiableSet(new HashSet<Class<? extends Annotation>>(
30+
asList(BeforeClass.class, AfterClass.class, Before.class, After.class)));
31+
32+
/**
33+
* Adds to {@code errors} a throwable for each problem detected. Looks for
34+
* {@code BeforeClass}, {@code AfterClass}, {@code Before} and {@code After}
35+
* annotations.
36+
*
37+
* @param method the method that is being validated
38+
* @return A list of exceptions detected
39+
*
40+
* @since 4.12
41+
*/
42+
@Override
43+
public List<Exception> validateAnnotatedMethod(FrameworkMethod method) {
44+
List<Exception> errors = new ArrayList<Exception>();
45+
Annotation[] annotations = method.getAnnotations();
46+
for (Annotation annotation : annotations) {
47+
for (Class clazz : INCOMPATIBLE_ANNOTATIONS) {
48+
if (annotation.annotationType().isAssignableFrom(clazz)) {
49+
addErrorMessage(errors, clazz);
50+
}
51+
}
52+
}
53+
return unmodifiableList(errors);
54+
}
55+
56+
private void addErrorMessage(List<Exception> errors, Class clazz) {
57+
String message = String.format("@%s can not be combined with @Category",
58+
clazz.getSimpleName());
59+
errors.add(new Exception(message));
60+
}
61+
}

src/main/java/org/junit/runners/ParentRunner.java

+56-1
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111
import java.util.Comparator;
1212
import java.util.Iterator;
1313
import java.util.List;
14+
import java.util.Map;
1415

1516
import org.junit.AfterClass;
1617
import org.junit.BeforeClass;
1718
import org.junit.ClassRule;
1819
import org.junit.Rule;
20+
import org.junit.validator.AnnotationValidator;
21+
import org.junit.validator.AnnotationValidatorFactory;
22+
import org.junit.validator.ValidateWith;
1923
import org.junit.internal.AssumptionViolatedException;
2024
import org.junit.internal.runners.model.EachTestNotifier;
2125
import org.junit.internal.runners.statements.RunAfters;
@@ -31,9 +35,9 @@
3135
import org.junit.runner.manipulation.Sorter;
3236
import org.junit.runner.notification.RunNotifier;
3337
import org.junit.runner.notification.StoppedByUserException;
38+
import org.junit.runners.model.FrameworkField;
3439
import org.junit.runners.model.FrameworkMethod;
3540
import org.junit.runners.model.InitializationError;
36-
import org.junit.runners.model.MultipleFailureException;
3741
import org.junit.runners.model.RunnerScheduler;
3842
import org.junit.runners.model.Statement;
3943
import org.junit.runners.model.TestClass;
@@ -59,6 +63,9 @@ public abstract class ParentRunner<T> extends Runner implements Filterable,
5963
// Guarded by fChildrenLock
6064
private volatile Collection<T> fFilteredChildren = null;
6165

66+
private final AnnotationValidatorFactory fAnnotationValidatorFactory =
67+
new AnnotationValidatorFactory();
68+
6269
private volatile RunnerScheduler fScheduler = new RunnerScheduler() {
6370
public void schedule(Runnable childStatement) {
6471
childStatement.run();
@@ -114,6 +121,54 @@ protected void collectInitializationErrors(List<Throwable> errors) {
114121
validatePublicVoidNoArgMethods(BeforeClass.class, true, errors);
115122
validatePublicVoidNoArgMethods(AfterClass.class, true, errors);
116123
validateClassRules(errors);
124+
invokeValidators(errors);
125+
}
126+
127+
private void invokeValidators(List<Throwable> errors) {
128+
invokeValidatorsOnClass(errors);
129+
invokeValidatorsOnMethods(errors);
130+
invokeValidatorsOnFields(errors);
131+
}
132+
133+
private void invokeValidatorsOnClass(List<Throwable> errors) {
134+
Annotation[] annotations = getTestClass().getAnnotations();
135+
for (Annotation annotation : annotations) {
136+
Class<? extends Annotation> annotationType = annotation.annotationType();
137+
ValidateWith validateWithAnnotation = annotationType.getAnnotation(ValidateWith.class);
138+
if (validateWithAnnotation != null) {
139+
AnnotationValidator annotationValidator =
140+
fAnnotationValidatorFactory.createAnnotationValidator(validateWithAnnotation);
141+
errors.addAll(annotationValidator.validateAnnotatedClass(getTestClass()));
142+
}
143+
}
144+
}
145+
146+
private void invokeValidatorsOnMethods(List<Throwable> errors) {
147+
Map<Class<? extends Annotation>, List<FrameworkMethod>> annotationMap = getTestClass().getAnnotationToMethods();
148+
for (Class<? extends Annotation> annotationType : annotationMap.keySet()) {
149+
ValidateWith validateWithAnnotation = annotationType.getAnnotation(ValidateWith.class);
150+
if (validateWithAnnotation != null) {
151+
for (FrameworkMethod frameworkMethod : annotationMap.get(annotationType)) {
152+
AnnotationValidator annotationValidator =
153+
fAnnotationValidatorFactory.createAnnotationValidator(validateWithAnnotation);
154+
errors.addAll(annotationValidator.validateAnnotatedMethod(frameworkMethod));
155+
}
156+
}
157+
}
158+
}
159+
160+
private void invokeValidatorsOnFields(List<Throwable> errors) {
161+
Map<Class<? extends Annotation>, List<FrameworkField>> annotationMap = getTestClass().getAnnotationToFields();
162+
for (Class<? extends Annotation> annotationType : annotationMap.keySet()) {
163+
ValidateWith validateWithAnnotation = annotationType.getAnnotation(ValidateWith.class);
164+
if (validateWithAnnotation != null) {
165+
for (FrameworkField frameworkField : annotationMap.get(annotationType)) {
166+
AnnotationValidator annotationValidator =
167+
fAnnotationValidatorFactory.createAnnotationValidator(validateWithAnnotation);
168+
errors.addAll(annotationValidator.validateAnnotatedField(frameworkField));
169+
}
170+
}
171+
}
117172
}
118173

119174
/**

src/main/java/org/junit/runners/model/TestClass.java

+59-10
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import java.lang.reflect.Field;
88
import java.lang.reflect.Method;
99
import java.util.ArrayList;
10+
import java.util.Arrays;
1011
import java.util.Collections;
11-
import java.util.HashMap;
12+
import java.util.Comparator;
13+
import java.util.LinkedHashMap;
1214
import java.util.List;
1315
import java.util.Map;
1416

@@ -24,8 +26,8 @@
2426
*/
2527
public class TestClass {
2628
private final Class<?> fClass;
27-
private final Map<Class<?>, List<FrameworkMethod>> fMethodsForAnnotations;
28-
private final Map<Class<?>, List<FrameworkField>> fFieldsForAnnotations;
29+
private final Map<Class<? extends Annotation>, List<FrameworkMethod>> fMethodsForAnnotations;
30+
private final Map<Class<? extends Annotation>, List<FrameworkField>> fFieldsForAnnotations;
2931

3032
/**
3133
* Creates a {@code TestClass} wrapping {@code klass}. Each time this
@@ -40,22 +42,38 @@ public TestClass(Class<?> klass) {
4042
"Test class can only have one constructor");
4143
}
4244

43-
Map<Class<?>, List<FrameworkMethod>> methodsForAnnotations = new HashMap<Class<?>, List<FrameworkMethod>>();
44-
Map<Class<?>, List<FrameworkField>> fieldsForAnnotations = new HashMap<Class<?>, List<FrameworkField>>();
45+
Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations =
46+
new LinkedHashMap<Class<? extends Annotation>, List<FrameworkMethod>>();
47+
Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations =
48+
new LinkedHashMap<Class<? extends Annotation>, List<FrameworkField>>();
49+
4550
for (Class<?> eachClass : getSuperClasses(fClass)) {
4651
for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) {
4752
addToAnnotationLists(new FrameworkMethod(eachMethod), methodsForAnnotations);
4853
}
49-
for (Field eachField : eachClass.getDeclaredFields()) {
54+
// ensuring fields are sorted to make sure that entries are inserted
55+
// and read from fieldForAnnotations in a deterministic order
56+
for (Field eachField : getSortedDeclaredFields(eachClass)) {
5057
addToAnnotationLists(new FrameworkField(eachField), fieldsForAnnotations);
5158
}
5259
}
53-
fMethodsForAnnotations = Collections.unmodifiableMap(methodsForAnnotations);
54-
fFieldsForAnnotations = Collections.unmodifiableMap(fieldsForAnnotations);
60+
61+
fMethodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
62+
fFieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
63+
}
64+
65+
private static Field[] getSortedDeclaredFields(Class<?> clazz) {
66+
Field[] declaredFields = clazz.getDeclaredFields();
67+
Arrays.sort(declaredFields, new Comparator<Field>() {
68+
public int compare(Field field1, Field field2) {
69+
return field1.getName().compareTo(field2.getName());
70+
}
71+
});
72+
return declaredFields;
5573
}
5674

5775
private static <T extends FrameworkMember<T>> void addToAnnotationLists(T member,
58-
Map<Class<?>, List<T>> map) {
76+
Map<Class<? extends Annotation>, List<T>> map) {
5977
for (Annotation each : member.getAnnotations()) {
6078
Class<? extends Annotation> type = each.annotationType();
6179
List<T> members = getAnnotatedMembers(map, type, true);
@@ -70,6 +88,17 @@ private static <T extends FrameworkMember<T>> void addToAnnotationLists(T member
7088
}
7189
}
7290

91+
private static <T extends FrameworkMember<T>> Map<Class<? extends Annotation>, List<T>>
92+
makeDeeplyUnmodifiable(Map<Class<? extends Annotation>, List<T>> source) {
93+
LinkedHashMap<Class<? extends Annotation>, List<T>> copy =
94+
new LinkedHashMap<Class<? extends Annotation>, List<T>>();
95+
for (Map.Entry<Class<? extends Annotation>, List<T>> entry : source.entrySet()) {
96+
copy.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
97+
}
98+
return Collections.unmodifiableMap(copy);
99+
}
100+
101+
73102
/**
74103
* Returns, efficiently, all the non-overridden methods in this class and
75104
* its superclasses that are annotated with {@code annotationClass}.
@@ -88,7 +117,27 @@ public List<FrameworkField> getAnnotatedFields(
88117
return Collections.unmodifiableList(getAnnotatedMembers(fFieldsForAnnotations, annotationClass, false));
89118
}
90119

91-
private static <T> List<T> getAnnotatedMembers(Map<Class<?>, List<T>> map,
120+
/**
121+
* Gets a {@code Map} between annotations and methods that have
122+
* the annotation in this class or its superclasses.
123+
*
124+
* @since 4.12
125+
*/
126+
public Map<Class<? extends Annotation>, List<FrameworkMethod>> getAnnotationToMethods() {
127+
return fMethodsForAnnotations;
128+
}
129+
130+
/**
131+
* Gets a {@code Map} between annotations and fields that have
132+
* the annotation in this class or its superclasses.
133+
*
134+
* @since 4.12
135+
*/
136+
public Map<Class<? extends Annotation>, List<FrameworkField>> getAnnotationToFields() {
137+
return fFieldsForAnnotations;
138+
}
139+
140+
private static <T> List<T> getAnnotatedMembers(Map<Class<? extends Annotation>, List<T>> map,
92141
Class<? extends Annotation> type, boolean fillIfAbsent) {
93142
if (!map.containsKey(type) && fillIfAbsent) {
94143
map.put(type, new ArrayList<T>());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.junit.validator;
2+
3+
import org.junit.runners.model.FrameworkField;
4+
import org.junit.runners.model.FrameworkMethod;
5+
import org.junit.runners.model.TestClass;
6+
7+
import static java.util.Collections.emptyList;
8+
9+
import java.util.List;
10+
11+
/**
12+
* Validates annotations on classes and methods. To be validated,
13+
* an annotation should be annotated with {@link ValidateWith}
14+
*
15+
* Instances of this class are shared by multiple test runners, so they should
16+
* be immutable and thread-safe.
17+
*
18+
* @since 4.12
19+
*/
20+
public abstract class AnnotationValidator {
21+
22+
private static final List<Exception> NO_VALIDATION_ERRORS = emptyList();
23+
24+
/**
25+
* Validates annotation on the given class.
26+
*
27+
* @param testClass that is being validated
28+
* @return A list of exceptions. Default behavior is to return an empty list.
29+
*
30+
* @since 4.12
31+
*/
32+
public List<Exception> validateAnnotatedClass(TestClass testClass) {
33+
return NO_VALIDATION_ERRORS;
34+
}
35+
36+
/**
37+
* Validates annotation on the given field.
38+
*
39+
* @param field that is being validated
40+
* @return A list of exceptions. Default behavior is to return an empty list.
41+
*
42+
* @since 4.12
43+
*/
44+
public List<Exception> validateAnnotatedField(FrameworkField field) {
45+
return NO_VALIDATION_ERRORS;
46+
47+
}
48+
49+
/**
50+
* Validates annotation on the given method.
51+
*
52+
* @param method that is being validated
53+
* @return A list of exceptions. Default behavior is to return an empty list.
54+
*
55+
* @since 4.12
56+
*/
57+
public List<Exception> validateAnnotatedMethod(FrameworkMethod method) {
58+
return NO_VALIDATION_ERRORS;
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.junit.validator;
2+
3+
import java.util.concurrent.ConcurrentHashMap;
4+
5+
/**
6+
* Creates instances of Annotation Validators.
7+
*
8+
* @since 4.12
9+
*/
10+
public class AnnotationValidatorFactory {
11+
12+
private static ConcurrentHashMap<ValidateWith, AnnotationValidator> fAnnotationTypeToValidatorMap =
13+
new ConcurrentHashMap<ValidateWith, AnnotationValidator>();
14+
15+
/**
16+
* Creates the AnnotationValidator specified by the value in
17+
* {@link org.junit.validator.ValidateWith}. Instances are
18+
* cached.
19+
*
20+
* @param validateWithAnnotation
21+
* @return An instance of the AnnotationValidator.
22+
*
23+
* @since 4.12
24+
*/
25+
public AnnotationValidator createAnnotationValidator(ValidateWith validateWithAnnotation) {
26+
AnnotationValidator validator = fAnnotationTypeToValidatorMap.get(validateWithAnnotation);
27+
if (validator != null) {
28+
return validator;
29+
}
30+
31+
Class<? extends AnnotationValidator> clazz = validateWithAnnotation.value();
32+
if (clazz == null) {
33+
throw new IllegalArgumentException("Can't create validator, value is null in annotation " + validateWithAnnotation.getClass().getName());
34+
}
35+
try {
36+
AnnotationValidator annotationValidator = clazz.newInstance();
37+
fAnnotationTypeToValidatorMap.putIfAbsent(validateWithAnnotation, annotationValidator);
38+
return fAnnotationTypeToValidatorMap.get(validateWithAnnotation);
39+
} catch (Exception e) {
40+
throw new RuntimeException("Exception received when creating AnnotationValidator class " + clazz.getName(), e);
41+
}
42+
}
43+
44+
}

0 commit comments

Comments
 (0)