1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.platform.test.rule 18 19 import kotlin.reflect.KClass 20 import kotlin.reflect.cast 21 import kotlin.reflect.full.hasAnnotation 22 import org.junit.runner.Description 23 24 /** 25 * Marks an annotation class as a "meta-annotation", which for our purposes means that it bundles 26 * together one or more test-behavior-affecting annotation values for reuse in multiple places in 27 * the test suite, if the runner or rule in question uses [TestAnnotationScanner]. For example: 28 * ``` 29 * @Retention(AnnotationRetention.RUNTIME) 30 * annotation class Flavor(val value: String = "") 31 * 32 * @Retention(AnnotationRetention.RUNTIME) 33 * @MetaAnnotation 34 * @Flavor("umami") 35 * annotation class Umami() 36 * 37 * // This test should be treated as if annotation with `@Flavor("umami")` 38 * @Umami 39 * class SushiTest { 40 * } 41 * ``` 42 */ 43 @Retention(AnnotationRetention.RUNTIME) annotation class MetaAnnotation 44 45 /** 46 * Scans for annotations on test methods and classes that may affect test runners or rules. 47 * 48 * (Encapsulated as an object to allow potentially caching results in the future, based on 49 * experience on annotation reflection performance in JUnit 4.) 50 * 51 * @see [find] for primary usage 52 */ 53 class TestAnnotationScanner { 54 /** inline reified version of [find] for more concise usage in Kotlin */ 55 inline fun <reified T : Annotation> find(description: Description) = find(T::class, description) 56 57 /** 58 * Find the most-relevant instance of [annotationClass] for the leaf-level test method described 59 * by [description], or null if none exists. The rules include: 60 * - if there is no such annotation on the test method, looks for an annotation on the class 61 * - if [annotationClass] is marked with [java.lang.annotation.Inherited], then the superclass 62 * hierarchy will be searched for a relevant annotation 63 * - if there are any annotations on the method or class, (or superclasses if [Inherited]) that 64 * are marked with [MetaAnnotation], then the annotations on the meta-annotation will be 65 * recursively searched (see [MetaAnnotation]) 66 */ 67 fun <T : Annotation> find(annotationClass: KClass<T>, description: Description): T? { 68 findAnnotation(annotationClass, description.annotations)?.let { 69 return it 70 } 71 val testClass = 72 description.testClass 73 ?: throw IllegalArgumentException( 74 "Could not find class for test: ${description.displayName}" 75 ) 76 findAnnotation(annotationClass, testClass.annotations.toList())?.let { 77 return it 78 } 79 return null 80 } 81 82 private fun <T : Annotation> findAnnotation( 83 annotationClass: KClass<T>, 84 annotations: Collection<Annotation>, 85 ): T? { 86 annotations.forEach { annotation -> 87 if (annotationClass.isInstance(annotation)) { 88 return annotationClass.cast(annotation) 89 } 90 val maybeMeta = annotation.annotationClass 91 if (maybeMeta.hasAnnotation<MetaAnnotation>()) { 92 findAnnotation(annotationClass, maybeMeta.annotations)?.let { 93 return it 94 } 95 } 96 } 97 return null 98 } 99 } 100