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 com.android.tools.metalava.model.testing
18 
19 import com.android.tools.metalava.model.ModelOptions
20 import com.android.tools.metalava.model.junit4.CustomizableParameterizedRunner
21 import com.android.tools.metalava.model.provider.Capability
22 import com.android.tools.metalava.model.provider.FilterableCodebaseCreator
23 import com.android.tools.metalava.model.provider.InputFormat
24 import com.android.tools.metalava.model.testing.BaseModelProviderRunner.InstanceRunner
25 import com.android.tools.metalava.model.testing.BaseModelProviderRunner.InstanceRunnerFactory
26 import com.android.tools.metalava.model.testing.BaseModelProviderRunner.ModelProviderWrapper
27 import com.android.tools.metalava.testing.BaselineTestRule
28 import java.lang.reflect.AnnotatedElement
29 import java.util.Locale
30 import org.junit.runner.Runner
31 import org.junit.runners.Parameterized
32 import org.junit.runners.model.FrameworkMethod
33 import org.junit.runners.model.Statement
34 import org.junit.runners.model.TestClass
35 import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters
36 import org.junit.runners.parameterized.ParametersRunnerFactory
37 import org.junit.runners.parameterized.TestWithParameters
38 
39 /**
40  * Base class for JUnit [Runner]s that need to run tests across a number of different codebase
41  * creators.
42  *
43  * The basic approach is:
44  * 1. Invoke the `codebaseCreatorConfigsGetter` lambda to get a list of [CodebaseCreatorConfig]s of
45  *    type [C]. The type of the codebase creator objects can vary across different runners, hence
46  *    why it is specified as a type parameter.
47  * 2. Wrap [CodebaseCreatorConfig] in a [ModelProviderWrapper] to tunnel information needed through
48  *    to [InstanceRunner].
49  * 3. Generate the cross product of the [ModelProviderWrapper]s with any additional test arguments
50  *    provided by the test class. If no test arguments are provided then just return the
51  *    [ModelProviderWrapper]s directly. Either way the returned [TestArguments] object will contain
52  *    an appropriate pattern for the number of arguments in each argument set.
53  * 4. The [Parameterized.RunnersFactory] will take the list of test arguments returned and then use
54  *    them to construct a set of [TestWithParameters] objects, each of which is passed to a
55  *    [ParametersRunnerFactory] to create the [Runner] for the test.
56  * 5. The [ParametersRunnerFactory] is instantiated by [Parameterized.RunnersFactory] directly from
57  *    a class (in this case [InstanceRunnerFactory]) so there is no way for this to pass information
58  *    into the [InstanceRunnerFactory]. So, instead it relies on the information to be passed
59  *    through the [TestWithParameters] object that is passed to
60  *    [ParametersRunnerFactory.createRunnerForTestWithParameters].
61  * 6. The [InstanceRunnerFactory] extracts the [ModelProviderWrapper] from the [TestWithParameters]
62  *    it is given and passes it in alongside the remaining arguments to [InstanceRunner].
63  * 7. The [InstanceRunner] injects the [ModelProviderWrapper.codebaseCreatorConfig] into the test
64  *    class along with any additional parameters and then runs the test as normal.
65  *
66  * @param C the type of the codebase creator object.
67  * @param I the type of the injectable class through which the codebase creator will be injected
68  *   into the test class.
69  * @param clazz the test class to be run, must be assignable to `injectableClass`.
70  * @param codebaseCreatorConfigsGetter a lambda for getting the [CodebaseCreatorConfig]s.
71  * @param baselineResourcePath the resource path to the baseline file that should be consulted for
72  *   known errors to ignore / check.
73  * @param minimumCapabilities the minimum set of capabilities the codebase created must provide in
74  *   order to be used by this runner.
75  */
76 open class BaseModelProviderRunner<C : FilterableCodebaseCreator, I : Any>(
77     clazz: Class<*>,
78     codebaseCreatorConfigsGetter: (TestClass) -> List<CodebaseCreatorConfig<C>>,
79     baselineResourcePath: String,
80     minimumCapabilities: Set<Capability> = emptySet(),
81 ) :
82     CustomizableParameterizedRunner(
83         clazz,
84         { testClass, additionalArguments ->
85             createTestArguments(
86                 testClass,
87                 codebaseCreatorConfigsGetter,
88                 baselineResourcePath,
89                 additionalArguments,
90                 minimumCapabilities,
91             )
92         },
93         InstanceRunnerFactory::class,
94     ) {
95 
96     init {
97         val injectableClass = CodebaseCreatorConfigAware::class.java
98         if (!injectableClass.isAssignableFrom(clazz)) {
99             error("Class ${clazz.name} does not implement ${injectableClass.name}")
100         }
101     }
102 
103     /**
104      * A wrapper around a [CodebaseCreatorConfig] that tunnels information needed by
105      * [InstanceRunnerFactory] through [TestWithParameters].
106      */
107     private class ModelProviderWrapper<C : FilterableCodebaseCreator>(
108         val codebaseCreatorConfig: CodebaseCreatorConfig<C>,
109         val baselineResourcePath: String,
110         val additionalArgumentSet: List<Any> = emptyList(),
111     ) {
withAdditionalArgumentSetnull112         fun withAdditionalArgumentSet(argumentSet: List<Any>) =
113             ModelProviderWrapper(codebaseCreatorConfig, baselineResourcePath, argumentSet)
114 
115         fun injectModelProviderInto(testInstance: Any) {
116             @Suppress("UNCHECKED_CAST")
117             val injectableTestInstance = testInstance as CodebaseCreatorConfigAware<C>
118             injectableTestInstance.codebaseCreatorConfig = codebaseCreatorConfig
119         }
120 
121         /**
122          * Get the string representation which will end up inside `[]` in [TestWithParameters.name].
123          */
toStringnull124         override fun toString() =
125             if (additionalArgumentSet.isEmpty()) codebaseCreatorConfig.toString()
126             else {
127                 buildString {
128                     append(codebaseCreatorConfig.toString())
129                     if (isNotEmpty()) {
130                         append(",")
131                     }
132                     additionalArgumentSet.joinTo(this, separator = ",")
133                 }
134             }
135     }
136 
137     /** [ParametersRunnerFactory] for creating [Runner]s for a set of arguments. */
138     class InstanceRunnerFactory : ParametersRunnerFactory {
139         /**
140          * Create a runner for the [TestWithParameters].
141          *
142          * The [TestWithParameters.parameters] contains at least one argument and the first argument
143          * will be the [ModelProviderWrapper] provided by [createTestArguments]. This extracts that
144          * from the list and passes them to [InstanceRunner] to inject them into the test class.
145          */
createRunnerForTestWithParametersnull146         override fun createRunnerForTestWithParameters(test: TestWithParameters): Runner {
147             val arguments = test.parameters
148 
149             // Get the [ModelProviderWrapper] from the arguments.
150             val modelProviderWrapper = arguments[0] as ModelProviderWrapper<*>
151 
152             // Get any additional arguments from the wrapper.
153             val additionalArguments = modelProviderWrapper.additionalArgumentSet
154 
155             // If the suffix to add to the end of the test name is empty then replace it with an
156             // empty string. This will cause [InstanceRunner] to avoid adding a suffix to the end of
157             // the test so that it can be run directly from the IDE.
158             val suffix = test.name.takeIf { it != "[]" } ?: ""
159 
160             // Create a new set of [TestWithParameters] containing any additional arguments, which
161             // may be an empty set. Keep the name as is as that will describe the codebase creator
162             // as well as the other arguments.
163             val newTest = TestWithParameters(suffix, test.testClass, additionalArguments)
164 
165             // Create a new [InstanceRunner] that will inject the codebase creator into the test
166             // class
167             // when created.
168             return InstanceRunner(modelProviderWrapper, newTest)
169         }
170     }
171 
172     /**
173      * Runner for a test that must implement [I].
174      *
175      * This will use the [modelProviderWrapper] to inject the codebase creator object into the test
176      * class after creation.
177      */
178     private class InstanceRunner(
179         private val modelProviderWrapper: ModelProviderWrapper<*>,
180         test: TestWithParameters
181     ) : BlockJUnit4ClassRunnerWithParameters(test) {
182 
183         /** The suffix to add at the end of the test name. */
184         private val testSuffix = test.name
185 
186         /**
187          * The runner name.
188          *
189          * If [testSuffix] is empty then this will be "[]", otherwise it will be the test suffix.
190          * The "[]" is used because an empty string is not allowed. The name used here has no effect
191          * on the [org.junit.runner.Description] objects generated or the running of the tests but
192          * is visible through the [Runner] hierarchy and so can affect test runner code in Gradle
193          * and IDEs. Using something similar to the standard pattern used by the [Parameterized]
194          * runner minimizes the risk that it will cause issues with that code.
195          */
<lambda>null196         private val runnerName = testSuffix.takeIf { it != "" } ?: "[]"
197 
createTestnull198         override fun createTest(): Any {
199             val testInstance = super.createTest()
200             modelProviderWrapper.injectModelProviderInto(testInstance)
201             return testInstance
202         }
203 
getNamenull204         override fun getName(): String {
205             return runnerName
206         }
207 
testNamenull208         override fun testName(method: FrameworkMethod): String {
209             return method.name + testSuffix
210         }
211 
212         /**
213          * Override [methodInvoker] to allow the [Statement] it returns to be wrapped by a
214          * [BaselineTestRule] to take into account known issues listed in a baseline file.
215          */
methodInvokernull216         override fun methodInvoker(method: FrameworkMethod, test: Any): Statement {
217             val statement = super.methodInvoker(method, test)
218             val baselineTestRule =
219                 BaselineTestRule(
220                     modelProviderWrapper.codebaseCreatorConfig.toString(),
221                     modelProviderWrapper.baselineResourcePath,
222                 )
223             return baselineTestRule.apply(statement, describeChild(method))
224         }
225 
getChildrennull226         override fun getChildren(): List<FrameworkMethod> {
227             return super.getChildren().filter { frameworkMethod ->
228                 // Create a predicate from any annotations on the methods.
229                 val predicate = createCreatorPredicate(sequenceOf(frameworkMethod.method))
230 
231                 // Apply the predicate to the [CodebaseCreatorConfig] that would be used for this
232                 // method.
233                 predicate(modelProviderWrapper.codebaseCreatorConfig)
234             }
235         }
236     }
237 
238     companion object {
createTestArgumentsnull239         private fun <C : FilterableCodebaseCreator> createTestArguments(
240             testClass: TestClass,
241             codebaseCreatorConfigsGetter: (TestClass) -> List<CodebaseCreatorConfig<C>>,
242             baselineResourcePath: String,
243             additionalArguments: List<Array<Any>>?,
244             minimumCapabilities: Set<Capability>,
245         ): TestArguments {
246             // Generate a sequence that traverse the super class hierarchy starting with the test
247             // class.
248             val hierarchy = generateSequence(testClass.javaClass) { it.superclass }
249 
250             val predicate =
251                 // Create a predicate from annotations on the test class and its ancestors.
252                 createCreatorPredicate(hierarchy)
253                     // AND that with a predicate to check for minimum capabilities.
254                     .and(createCapabilitiesPredicate(minimumCapabilities))
255 
256             // Get the list of [CodebaseCreatorConfig]s over which this must run the tests.
257             val creatorConfigs =
258                 codebaseCreatorConfigsGetter(testClass)
259                     // Filter out any [CodebaseCreatorConfig]s as requested.
260                     .filter(predicate)
261 
262             // Wrap each codebase creator object with information needed by [InstanceRunnerFactory].
263             val wrappers =
264                 creatorConfigs.map { creatorConfig ->
265                     ModelProviderWrapper(creatorConfig, baselineResourcePath)
266                 }
267 
268             return if (additionalArguments == null) {
269                 // No additional arguments were provided so just return the wrappers.
270                 TestArguments("{0}", wrappers)
271             } else {
272                 // Convert each argument set from Array<Any> to List<Any>
273                 val additionalArgumentSetLists = additionalArguments.map { it.toList() }
274                 // Duplicate every wrapper with each argument set.
275                 val combined =
276                     wrappers.flatMap { wrapper ->
277                         additionalArgumentSetLists.map { argumentSet ->
278                             wrapper.withAdditionalArgumentSet(argumentSet)
279                         }
280                     }
281                 TestArguments("{0}", combined)
282             }
283         }
284 
285         private data class ProviderOptions(val provider: String, val options: String)
286 
287         /**
288          * Create a [CreatorPredicate] for [CodebaseCreatorConfig]s based on the annotations on the
289          * [annotatedElements],
290          */
createCreatorPredicatenull291         private fun createCreatorPredicate(annotatedElements: Sequence<AnnotatedElement>) =
292             predicateFromFilterByProvider(annotatedElements)
293                 .and(predicateFromRequiredCapabilities(annotatedElements))
294 
295         /** Create a [CreatorPredicate] from [FilterByProvider] annotations. */
296         private fun predicateFromFilterByProvider(
297             annotatedElements: Sequence<AnnotatedElement>
298         ): CreatorPredicate {
299             val providerToAction = mutableMapOf<String, FilterAction>()
300             val providerOptionsToAction = mutableMapOf<ProviderOptions, FilterAction>()
301 
302             // Iterate over the annotated elements
303             for (element in annotatedElements) {
304                 val annotations = element.getAnnotationsByType(FilterByProvider::class.java)
305                 for (annotation in annotations) {
306                     val specifiedOptions = annotation.specifiedOptions
307                     if (specifiedOptions == null) {
308                         providerToAction.putIfAbsent(annotation.provider, annotation.action)
309                     } else {
310                         val key = ProviderOptions(annotation.provider, specifiedOptions)
311                         providerOptionsToAction.putIfAbsent(key, annotation.action)
312                     }
313                 }
314             }
315 
316             // Create a predicate from the [FilterByProvider] annotations.
317             return if (providerToAction.isEmpty() && providerOptionsToAction.isEmpty())
318                 alwaysTruePredicate
319             else
320                 { config ->
321                     val providerName = config.providerName
322                     val key = ProviderOptions(providerName, config.modelOptions.toString())
323                     val action = providerOptionsToAction[key] ?: providerToAction[providerName]
324                     action != FilterAction.EXCLUDE
325                 }
326         }
327 
328         /** Create a [CreatorPredicate] from [RequiresCapabilities]. */
predicateFromRequiredCapabilitiesnull329         private fun predicateFromRequiredCapabilities(
330             annotatedElements: Sequence<AnnotatedElement>
331         ): CreatorPredicate {
332             // Iterate over the annotated elements stopping at the first which is annotated with
333             // [RequiresCapabilities] and return the set of [RequiresCapabilities.required]
334             // [Capability]s.
335             for (element in annotatedElements) {
336                 val requires = element.getAnnotation(RequiresCapabilities::class.java)
337                 if (requires != null) {
338                     return createCapabilitiesPredicate(requires.required.toSet())
339                 }
340             }
341 
342             return alwaysTruePredicate
343         }
344 
345         /**
346          * Create a [CreatorPredicate] to select [CodebaseCreatorConfig]s with the [required]
347          * capabilities.
348          */
createCapabilitiesPredicatenull349         private fun createCapabilitiesPredicate(required: Set<Capability>): CreatorPredicate =
350             if (required.isEmpty()) alwaysTruePredicate
351             else { config -> config.creator.capabilities.containsAll(required) }
352     }
353 }
354 
355 /** Encapsulates the configuration information needed by a codebase creator */
356 class CodebaseCreatorConfig<C : FilterableCodebaseCreator>(
357     /** The creator that will create the codebase. */
358     val creator: C,
359     /**
360      * The optional [InputFormat] of the files from which the codebase will be created. If this is
361      * not specified then files of any [InputFormat] supported by the [creator] can be used.
362      */
363     val inputFormat: InputFormat? = null,
364 
365     /** Any additional options passed to the codebase creator. */
366     val modelOptions: ModelOptions = ModelOptions.empty,
367     includeProviderNameInTestName: Boolean = true,
368     includeInputFormatInTestName: Boolean = false,
369 ) {
370     val providerName = creator.providerName
371 
<lambda>null372     private val toStringValue = buildString {
373         var separator = ""
374         if (includeProviderNameInTestName) {
375             append(creator.providerName)
376             separator = ","
377         }
378 
379         // If the [inputFormat] is specified and required then include it in the test name,
380         // otherwise ignore it.
381         if (includeInputFormatInTestName && inputFormat != null) {
382             append(separator)
383             append(inputFormat.name.lowercase(Locale.US))
384             separator = ","
385         }
386 
387         // If the [ModelOptions] is not empty, then include it in the test name, otherwise ignore
388         // it.
389         if (modelOptions != ModelOptions.empty) {
390             append(separator)
391             append(modelOptions)
392         }
393     }
394 
395     /** Override this to return the string that will be used in the test name. */
toStringnull396     override fun toString() = toStringValue
397 }
398 
399 /** A predicate for use when filtering [CodebaseCreatorConfig]s. */
400 typealias CreatorPredicate = (CodebaseCreatorConfig<*>) -> Boolean
401 
402 /** The always `true` predicate. */
403 private val alwaysTruePredicate: (CodebaseCreatorConfig<*>) -> Boolean = { true }
404 
405 /** AND this predicate with the [other] predicate. */
CreatorPredicatenull406 fun CreatorPredicate.and(other: CreatorPredicate) =
407     if (this == alwaysTruePredicate) other
408     else if (other == alwaysTruePredicate) this else { config -> this(config) && other(config) }
409