/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package libcore.java.lang.reflect.annotations; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.StringJoiner; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; /** * Utility methods and annotation definitions for use when testing implementations of * AnnotatedElement. * *

For compactness, the repeated annotation methods that take strings use a format based on Java * syntax rather than the toString() of annotations. For example, "@Repeated(1)" rather than * "@libcore.java.lang.reflect.annotations.AnnotatedElementTestSupport.Repeated(value=1)". Use * {@link #EXPECT_EMPTY} to indicate "no annotationed expected". */ public class AnnotatedElementTestSupport { @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationA {} @Inherited @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationB {} @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationC {} @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationD {} @Retention(RetentionPolicy.RUNTIME) @Inherited @Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.PACKAGE }) public @interface Container { Repeated[] value(); } @Repeatable(Container.class) @Retention(RetentionPolicy.RUNTIME) @Inherited @Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.PACKAGE }) public @interface Repeated { int value(); } /** * A named constant that can be used with assert methods below that take * "String[] expectedAnnotationStrings" as their final argument to indicate "none". */ public static final String[] EXPECT_EMPTY = new String[0]; private AnnotatedElementTestSupport() { } /** * Test the {@link AnnotatedElement} methods associated with "presence". i.e. methods that * deal with annotations being "present" (i.e. "direct" or "inherited" annotations, not * "indirect"). * *

Asserts that calling {@link AnnotatedElement#getAnnotations()} on the supplied element * returns annotations of the supplied expected classes. * *

Where the expected classes contains some subset from * {@link AnnotationA}, {@link AnnotationB}, {@link AnnotationC}, {@link AnnotationD} this * method also asserts that {@link AnnotatedElement#isAnnotationPresent(Class)} and * {@link AnnotatedElement#getAnnotation(Class)} works as expected. * *

This method also confirms that {@link AnnotatedElement#isAnnotationPresent(Class)} and * {@link AnnotatedElement#getAnnotation(Class)} work correctly with a {@code null} argument. */ static void checkAnnotatedElementPresentMethods( AnnotatedElement element, Class... expectedAnnotations) { Set> actualTypes = annotationsToTypes(element.getAnnotations()); Set> expectedTypes = set(expectedAnnotations); assertEquals(expectedTypes, actualTypes); // getAnnotations() should be consistent with isAnnotationPresent() and getAnnotation() assertPresent(expectedTypes.contains(AnnotationA.class), element, AnnotationA.class); assertPresent(expectedTypes.contains(AnnotationB.class), element, AnnotationB.class); assertPresent(expectedTypes.contains(AnnotationC.class), element, AnnotationC.class); assertPresent(expectedTypes.contains(AnnotationD.class), element, AnnotationD.class); try { element.isAnnotationPresent(null); fail(); } catch (NullPointerException expected) { } try { element.getAnnotation(null); fail(); } catch (NullPointerException expected) { } } /** * Test the {@link AnnotatedElement} methods associated with "direct" annotations. * *

Asserts that calling {@link AnnotatedElement#getDeclaredAnnotations()} on the supplied * element returns annotations of the supplied expected classes. * *

Where the expected classes contains some subset from * {@link AnnotationA}, {@link AnnotationB} and {@link AnnotationC}, this method also asserts * that {@link AnnotatedElement#getDeclaredAnnotation(Class)} works as expected. * *

This method also confirms that {@link AnnotatedElement#isAnnotationPresent(Class)} and * {@link AnnotatedElement#getAnnotation(Class)} work correctly with a {@code null} argument. */ static void checkAnnotatedElementDirectMethods( AnnotatedElement element, Class... expectedDeclaredAnnotations) { Set> actualTypes = annotationsToTypes(element.getDeclaredAnnotations()); Set> expectedTypes = set(expectedDeclaredAnnotations); assertEquals(expectedTypes, actualTypes); assertDeclared(expectedTypes.contains(AnnotationA.class), element, AnnotationA.class); assertDeclared(expectedTypes.contains(AnnotationB.class), element, AnnotationB.class); assertDeclared(expectedTypes.contains(AnnotationC.class), element, AnnotationC.class); try { element.getDeclaredAnnotation(null); fail(); } catch (NullPointerException expected) { } } /** * Extracts the annotation types ({@link Annotation#annotationType()} from the supplied * annotations. */ static Set> annotationsToTypes(Annotation[] annotations) { Set> result = new HashSet>(); for (Annotation annotation : annotations) { result.add(annotation.annotationType()); } return result; } private static void assertPresent(boolean present, AnnotatedElement element, Class annotation) { if (present) { assertNotNull(element.getAnnotation(annotation)); assertTrue(element.isAnnotationPresent(annotation)); } else { assertNull(element.getAnnotation(annotation)); assertFalse(element.isAnnotationPresent(annotation)); } } private static void assertDeclared(boolean present, AnnotatedElement element, Class annotation) { if (present) { assertNotNull(element.getDeclaredAnnotation(annotation)); } else { assertNull(element.getDeclaredAnnotation(annotation)); } } @SafeVarargs static Set set(T... instances) { return new HashSet<>(Arrays.asList(instances)); } /** * Asserts that {@link AnnotatedElement#isAnnotationPresent(Class)} returns the expected result. */ static void assertIsAnnotationPresent( AnnotatedElement element, Class annotationType, boolean expected) { assertEquals("element.isAnnotationPresent() for " + element + " and " + annotationType, expected, element.isAnnotationPresent(annotationType)); } /** * Asserts that {@link AnnotatedElement#getDeclaredAnnotation(Class)} returns the expected * result. The result is specified using a String. See {@link AnnotatedElementTestSupport} for * the string syntax. */ static void assertGetDeclaredAnnotation(AnnotatedElement annotatedElement, Class annotationType, String expectedAnnotationString) { Annotation annotation = annotatedElement.getDeclaredAnnotation(annotationType); assertAnnotationMatches(annotation, expectedAnnotationString); } /** * Asserts that {@link AnnotatedElement#getDeclaredAnnotationsByType(Class)} returns the * expected result. The result is specified using a String. See * {@link AnnotatedElementTestSupport} for the string syntax. */ static void assertGetDeclaredAnnotationsByType( AnnotatedElement annotatedElement, Class annotationType, String[] expectedAnnotationStrings) { Annotation[] annotations = annotatedElement.getDeclaredAnnotationsByType(annotationType); assertAnnotationsMatch(annotations, expectedAnnotationStrings); } /** * Asserts that {@link AnnotatedElement#getAnnotationsByType(Class)} returns the * expected result. The result is specified using a String. See * {@link AnnotatedElementTestSupport} for the string syntax. */ static void assertGetAnnotationsByType(AnnotatedElement annotatedElement, Class annotationType, String[] expectedAnnotationStrings) throws Exception { Annotation[] annotations = annotatedElement.getAnnotationsByType(annotationType); assertAnnotationsMatch(annotations, expectedAnnotationStrings); } private static void assertAnnotationMatches( Annotation annotation, String expectedAnnotationString) { if (expectedAnnotationString == null) { assertNull(annotation); } else { assertNotNull(annotation); assertEquals(expectedAnnotationString, createAnnotationTestString(annotation)); } } /** * Asserts that the supplied annotations match the expectation Strings. See * {@link AnnotatedElementTestSupport} for the string syntax. */ static void assertAnnotationsMatch(Annotation[] annotations, String[] expectedAnnotationStrings) { // Due to Android's dex format insisting that Annotations are sorted by name the ordering of // annotations is determined by the (simple?) name of the Annotation, not just the order // that they are defined in the source. Tests have to be sensitive to that when handling // mixed usage of "Container" and "Repeated" - the "Container" annotations will be // discovered before "Repeated" due to their sort ordering. // // This code assumes that repeated annotations with the same name will be specified in the // source their natural sort order when attributes are considered, just to make the testing // simpler. // e.g. @Repeated(1) @Repeated(2), never @Repeated(2) @Repeated(1) // Sorting the expected and actual strings _should_ work providing the assumptions above // hold. It may mask random ordering issues but it's harder to deal with that while the // source ordering is no observed. Providing no developers are ascribing meaning to the // relative order of annotations things should be ok. Arrays.sort(expectedAnnotationStrings); String[] actualAnnotationStrings = createAnnotationTestStrings(annotations); Arrays.sort(actualAnnotationStrings); assertEquals( Arrays.asList(expectedAnnotationStrings), Arrays.asList(actualAnnotationStrings)); } private static String[] createAnnotationTestStrings(Annotation[] annotations) { String[] annotationStrings = new String[annotations.length]; for (int i = 0; i < annotations.length; i++) { annotationStrings[i] = createAnnotationTestString(annotations[i]); } return annotationStrings; } private static String createAnnotationTestString(Annotation annotation) { return "@" + annotation.annotationType().getSimpleName() + createArgumentsTestString( annotation); } private static String createArgumentsTestString(Annotation annotation) { if (annotation instanceof Repeated) { Repeated repeated = (Repeated) annotation; return "(" + repeated.value() + ")"; } else if (annotation instanceof Container) { Container container = (Container) annotation; String[] repeatedValues = createAnnotationTestStrings(container.value()); StringJoiner joiner = new StringJoiner(", ", "{", "}"); for (String repeatedValue : repeatedValues) { joiner.add(repeatedValue); } String repeatedValuesString = joiner.toString(); return "(" + repeatedValuesString + ")"; } throw new AssertionError("Unknown annotation: " + annotation); } }