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