• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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<ModelProviderWrapper<C>>(
83         clazz,
84         { testClass, additionalArguments ->
85             createTestArguments(
86                 testClass,
87                 codebaseCreatorConfigsGetter,
88                 baselineResourcePath,
89                 additionalArguments,
90                 minimumCapabilities,
91             )
92         },
93         InstanceRunnerFactory(),
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      * Apply [parametersFilterMethod] to [argument] by combining
105      * [ModelProviderWrapper.codebaseCreatorConfig] and [ModelProviderWrapper.additionalArgumentSet]
106      * into a single list of arguments and the invoking [parametersFilterMethod] with them.
107      */
invokeFilterMethodnull108     override fun invokeFilterMethod(
109         parametersFilterMethod: FrameworkMethod,
110         argument: ModelProviderWrapper<C>
111     ): Boolean {
112         val args = buildList {
113             add(argument.codebaseCreatorConfig)
114             addAll(argument.additionalArgumentSet)
115         }
116         return parametersFilterMethod.invokeExplosively(null, *args.toTypedArray()) as Boolean
117     }
118 
119     /**
120      * A wrapper around a [CodebaseCreatorConfig] that tunnels information needed by
121      * [InstanceRunnerFactory] through [TestWithParameters].
122      */
123     class ModelProviderWrapper<C : FilterableCodebaseCreator>(
124         val codebaseCreatorConfig: CodebaseCreatorConfig<C>,
125         val baselineResourcePath: String,
126         val additionalArgumentSet: List<Any> = emptyList(),
127     ) {
withAdditionalArgumentSetnull128         fun withAdditionalArgumentSet(argumentSet: List<Any>) =
129             ModelProviderWrapper(codebaseCreatorConfig, baselineResourcePath, argumentSet)
130 
131         fun injectModelProviderInto(testInstance: Any) {
132             @Suppress("UNCHECKED_CAST")
133             val injectableTestInstance = testInstance as CodebaseCreatorConfigAware<C>
134             injectableTestInstance.codebaseCreatorConfig = codebaseCreatorConfig
135         }
136 
137         /**
138          * Get the string representation which will end up inside `[]` in [TestWithParameters.name].
139          */
toStringnull140         override fun toString() =
141             if (additionalArgumentSet.isEmpty()) codebaseCreatorConfig.toString()
142             else {
143                 buildString {
144                     append(codebaseCreatorConfig.toString())
145                     if (isNotEmpty()) {
146                         append(",")
147                     }
148                     additionalArgumentSet.joinTo(this, separator = ",")
149                 }
150             }
151     }
152 
153     /** [ParametersRunnerFactory] for creating [Runner]s for a set of arguments. */
154     class InstanceRunnerFactory : ParametersRunnerFactory {
155         /**
156          * Create a runner for the [TestWithParameters].
157          *
158          * The [TestWithParameters.parameters] contains at least one argument and the first argument
159          * will be the [ModelProviderWrapper] provided by [createTestArguments]. This extracts that
160          * from the list and passes them to [InstanceRunner] to inject them into the test class.
161          */
createRunnerForTestWithParametersnull162         override fun createRunnerForTestWithParameters(test: TestWithParameters): Runner {
163             val arguments = test.parameters
164 
165             // Get the [ModelProviderWrapper] from the arguments.
166             val modelProviderWrapper = arguments[0] as ModelProviderWrapper<*>
167 
168             // Get any additional arguments from the wrapper.
169             val additionalArguments = modelProviderWrapper.additionalArgumentSet
170 
171             // If the suffix to add to the end of the test name is empty then replace it with an
172             // empty string. This will cause [InstanceRunner] to avoid adding a suffix to the end of
173             // the test so that it can be run directly from the IDE.
174             val suffix = test.name.takeIf { it != "[]" } ?: ""
175 
176             // Create a new set of [TestWithParameters] containing any additional arguments, which
177             // may be an empty set. Keep the name as is as that will describe the codebase creator
178             // as well as the other arguments.
179             val newTest = TestWithParameters(suffix, test.testClass, additionalArguments)
180 
181             // Create a new [InstanceRunner] that will inject the codebase creator into the test
182             // class
183             // when created.
184             return InstanceRunner(modelProviderWrapper, newTest)
185         }
186     }
187 
188     /**
189      * Runner for a test that must implement [I].
190      *
191      * This will use the [modelProviderWrapper] to inject the codebase creator object into the test
192      * class after creation.
193      */
194     private class InstanceRunner(
195         private val modelProviderWrapper: ModelProviderWrapper<*>,
196         test: TestWithParameters
197     ) : BlockJUnit4ClassRunnerWithParameters(test) {
198 
199         /** The suffix to add at the end of the test name. */
200         private val testSuffix = test.name
201 
202         /**
203          * The runner name.
204          *
205          * If [testSuffix] is empty then this will be "[]", otherwise it will be the test suffix.
206          * The "[]" is used because an empty string is not allowed. The name used here has no effect
207          * on the [org.junit.runner.Description] objects generated or the running of the tests but
208          * is visible through the [Runner] hierarchy and so can affect test runner code in Gradle
209          * and IDEs. Using something similar to the standard pattern used by the [Parameterized]
210          * runner minimizes the risk that it will cause issues with that code.
211          */
<lambda>null212         private val runnerName = testSuffix.takeIf { it != "" } ?: "[]"
213 
createTestnull214         override fun createTest(): Any {
215             val testInstance = super.createTest()
216             modelProviderWrapper.injectModelProviderInto(testInstance)
217             return testInstance
218         }
219 
getNamenull220         override fun getName(): String {
221             return runnerName
222         }
223 
testNamenull224         override fun testName(method: FrameworkMethod): String {
225             return method.name + testSuffix
226         }
227 
228         /**
229          * Override [methodInvoker] to allow the [Statement] it returns to be wrapped by a
230          * [BaselineTestRule] to take into account known issues listed in a baseline file.
231          */
methodInvokernull232         override fun methodInvoker(method: FrameworkMethod, test: Any): Statement {
233             val statement = super.methodInvoker(method, test)
234             val baselineTestRule =
235                 BaselineTestRule(
236                     modelProviderWrapper.codebaseCreatorConfig.toString(),
237                     modelProviderWrapper.baselineResourcePath,
238                 )
239             return baselineTestRule.apply(statement, describeChild(method))
240         }
241 
getChildrennull242         override fun getChildren(): List<FrameworkMethod> {
243             return super.getChildren().filter { frameworkMethod ->
244                 // Create a predicate from any annotations on the methods.
245                 val predicate = createCreatorPredicate(sequenceOf(frameworkMethod.method))
246 
247                 // Apply the predicate to the [CodebaseCreatorConfig] that would be used for this
248                 // method.
249                 predicate(modelProviderWrapper.codebaseCreatorConfig)
250             }
251         }
252     }
253 
254     companion object {
createTestArgumentsnull255         private fun <C : FilterableCodebaseCreator> createTestArguments(
256             testClass: TestClass,
257             codebaseCreatorConfigsGetter: (TestClass) -> List<CodebaseCreatorConfig<C>>,
258             baselineResourcePath: String,
259             additionalArguments: List<Array<Any>>?,
260             minimumCapabilities: Set<Capability>,
261         ): TestArguments<ModelProviderWrapper<C>> {
262             // Generate a sequence that traverse the super class hierarchy starting with the test
263             // class.
264             val hierarchy = generateSequence(testClass.javaClass) { it.superclass }
265 
266             val predicate =
267                 // Create a predicate from annotations on the test class and its ancestors.
268                 createCreatorPredicate(hierarchy)
269                     // AND that with a predicate to check for minimum capabilities.
270                     .and(createCapabilitiesPredicate(minimumCapabilities))
271 
272             // Get the list of [CodebaseCreatorConfig]s over which this must run the tests.
273             val creatorConfigs =
274                 codebaseCreatorConfigsGetter(testClass)
275                     // Filter out any [CodebaseCreatorConfig]s as requested.
276                     .filter(predicate)
277 
278             // Wrap each codebase creator object with information needed by [InstanceRunnerFactory].
279             val wrappers =
280                 creatorConfigs.map { creatorConfig ->
281                     ModelProviderWrapper(creatorConfig, baselineResourcePath)
282                 }
283 
284             return if (additionalArguments == null) {
285                 // No additional arguments were provided so just return the wrappers.
286                 TestArguments("{0}", wrappers)
287             } else {
288                 // Convert each argument set from Array<Any> to List<Any>
289                 val additionalArgumentSetLists = additionalArguments.map { it.toList() }
290                 // Duplicate every wrapper with each argument set.
291                 val combined =
292                     wrappers.flatMap { wrapper ->
293                         additionalArgumentSetLists.map { argumentSet ->
294                             wrapper.withAdditionalArgumentSet(argumentSet)
295                         }
296                     }
297                 TestArguments("{0}", combined)
298             }
299         }
300 
301         private data class ProviderOptions(val provider: String, val options: String)
302 
303         /**
304          * Create a [CreatorPredicate] for [CodebaseCreatorConfig]s based on the annotations on the
305          * [annotatedElements],
306          */
createCreatorPredicatenull307         private fun createCreatorPredicate(annotatedElements: Sequence<AnnotatedElement>) =
308             predicateFromFilterByProvider(annotatedElements)
309                 .and(predicateFromRequiredCapabilities(annotatedElements))
310 
311         /** Create a [CreatorPredicate] from [FilterByProvider] annotations. */
312         private fun predicateFromFilterByProvider(
313             annotatedElements: Sequence<AnnotatedElement>
314         ): CreatorPredicate {
315             val providerToAction = mutableMapOf<String, FilterAction>()
316             val providerOptionsToAction = mutableMapOf<ProviderOptions, FilterAction>()
317 
318             // Iterate over the annotated elements
319             for (element in annotatedElements) {
320                 val annotations = element.getAnnotationsByType(FilterByProvider::class.java)
321                 for (annotation in annotations) {
322                     val specifiedOptions = annotation.specifiedOptions
323                     if (specifiedOptions == null) {
324                         providerToAction.putIfAbsent(annotation.provider, annotation.action)
325                     } else {
326                         val key = ProviderOptions(annotation.provider, specifiedOptions)
327                         providerOptionsToAction.putIfAbsent(key, annotation.action)
328                     }
329                 }
330             }
331 
332             // Create a predicate from the [FilterByProvider] annotations.
333             return if (providerToAction.isEmpty() && providerOptionsToAction.isEmpty())
334                 alwaysTruePredicate
335             else
336                 { config ->
337                     val providerName = config.providerName
338                     val key = ProviderOptions(providerName, config.modelOptions.toString())
339                     val action = providerOptionsToAction[key] ?: providerToAction[providerName]
340                     action != FilterAction.EXCLUDE
341                 }
342         }
343 
344         /** Create a [CreatorPredicate] from [RequiresCapabilities]. */
predicateFromRequiredCapabilitiesnull345         private fun predicateFromRequiredCapabilities(
346             annotatedElements: Sequence<AnnotatedElement>
347         ): CreatorPredicate {
348             // Iterate over the annotated elements stopping at the first which is annotated with
349             // [RequiresCapabilities] and return the set of [RequiresCapabilities.required]
350             // [Capability]s.
351             for (element in annotatedElements) {
352                 val requires = element.getAnnotation(RequiresCapabilities::class.java)
353                 if (requires != null) {
354                     return createCapabilitiesPredicate(requires.required.toSet())
355                 }
356             }
357 
358             return alwaysTruePredicate
359         }
360 
361         /**
362          * Create a [CreatorPredicate] to select [CodebaseCreatorConfig]s with the [required]
363          * capabilities.
364          */
createCapabilitiesPredicatenull365         private fun createCapabilitiesPredicate(required: Set<Capability>): CreatorPredicate =
366             if (required.isEmpty()) alwaysTruePredicate
367             else { config -> config.creator.capabilities.containsAll(required) }
368     }
369 }
370 
371 /** Encapsulates the configuration information needed by a codebase creator */
372 class CodebaseCreatorConfig<C : FilterableCodebaseCreator>(
373     /** The creator that will create the codebase. */
374     val creator: C,
375     /**
376      * The optional [InputFormat] of the files from which the codebase will be created. If this is
377      * not specified then files of any [InputFormat] supported by the [creator] can be used.
378      */
379     val inputFormat: InputFormat? = null,
380 
381     /** Any additional options passed to the codebase creator. */
382     val modelOptions: ModelOptions = ModelOptions.empty,
383     includeProviderNameInTestName: Boolean = true,
384     includeInputFormatInTestName: Boolean = false,
385 ) {
386     val providerName = creator.providerName
387 
<lambda>null388     private val toStringValue = buildString {
389         var separator = ""
390         if (includeProviderNameInTestName) {
391             append(creator.providerName)
392             separator = ","
393         }
394 
395         // If the [inputFormat] is specified and required then include it in the test name,
396         // otherwise ignore it.
397         if (includeInputFormatInTestName && inputFormat != null) {
398             append(separator)
399             append(inputFormat.name.lowercase(Locale.US))
400             separator = ","
401         }
402 
403         // If the [ModelOptions] is not empty, then include it in the test name, otherwise ignore
404         // it.
405         if (modelOptions != ModelOptions.empty) {
406             append(separator)
407             append(modelOptions)
408         }
409     }
410 
411     /** Override this to return the string that will be used in the test name. */
toStringnull412     override fun toString() = toStringValue
413 }
414 
415 /** A predicate for use when filtering [CodebaseCreatorConfig]s. */
416 typealias CreatorPredicate = (CodebaseCreatorConfig<*>) -> Boolean
417 
418 /** The always `true` predicate. */
419 private val alwaysTruePredicate: (CodebaseCreatorConfig<*>) -> Boolean = { true }
420 
421 /** AND this predicate with the [other] predicate. */
CreatorPredicatenull422 fun CreatorPredicate.and(other: CreatorPredicate) =
423     if (this == alwaysTruePredicate) other
424     else if (other == alwaysTruePredicate) this else { config -> this(config) && other(config) }
425