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