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