1 /*
2  * 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.junit4
18 
19 import java.lang.reflect.Constructor
20 import java.lang.reflect.Field
21 import java.lang.reflect.InaccessibleObjectException
22 import java.lang.reflect.Method
23 import java.lang.reflect.Modifier
24 import kotlin.reflect.KClass
25 import kotlin.reflect.KProperty
26 import org.junit.AssumptionViolatedException
27 import org.junit.runner.Description
28 import org.junit.runner.Runner
29 import org.junit.runner.notification.RunNotifier
30 import org.junit.runners.Parameterized
31 import org.junit.runners.Parameterized.Parameters
32 import org.junit.runners.Parameterized.UseParametersRunnerFactory
33 import org.junit.runners.ParentRunner
34 import org.junit.runners.model.FrameworkMethod
35 import org.junit.runners.model.TestClass
36 import org.junit.runners.parameterized.ParametersRunnerFactory
37 
38 /**
39  * A customizable wrapper around the JUnit [Parameterized] runner.
40  *
41  * While it is very capable unfortunately, is not very customizable, e.g.
42  * * Test arguments can only be retrieved from a function annotated with [Parameters], so there is
43  *   no way to provide arguments automatically by the runner.
44  * * The function that provides the arguments is not given the test [Class] that is being run (which
45  *   may be a subclass of the class with the [Parameters] function). That means the argument
46  *   generation cannot take into account information from the test class, e.g. class annotations.
47  * * Once provided the arguments cannot be filtered.
48  * * A custom [ParametersRunnerFactory] can be provided through the [UseParametersRunnerFactory]
49  *   annotation, but it has to be specified on the test class, and cannot be inherited from the
50  *   class that specifies the `@RunWith(Parameterized.class)`. There is no way to provide a single
51  *   `@RunWith(ExtensionOfParameterized.class)` annotation that hard codes the
52  *   [ParametersRunnerFactory].
53  *
54  * JUnit 4 is no longer under active development so there is no chance of getting these capabilities
55  * added to the [Parameterized] runner. The JUnit Params library is a more capable parameterized
56  * runner but unfortunately, it is not being actively maintained. JUnit 5 parameterization does not
57  * support parameterizing the whole class.
58  *
59  * So, this class is being provided to rectify those limitations by providing a wrapper around a
60  * [Parameterized] instance, using copious quantities of reflection to construct and manipulate it.
61  * On top of that wrapper it will provide support for doing some (or all) of the above as needed.
62  *
63  * @param clazz the test class to run.
64  * @param argumentsProvider provider of [TestArguments] used by this runner. Is also passed any
65  *   additional parameters (provided by the test class using the standard [Parameterized]
66  *   mechanism), if any. They can be filtered and/or combined in some way with parameters provides
67  *   by this.
68  */
69 abstract class CustomizableParameterizedRunner(
70     clazz: Class<*>,
71     argumentsProvider: (TestClass, List<Array<Any>>?) -> TestArguments,
72     parametersRunnerFactoryClass: KClass<out ParametersRunnerFactory>? = null,
73 ) : ParentRunner<Runner>(clazz) {
74 
75     /** The set of test arguments to use. */
76     class TestArguments(
77         /**
78          * The pattern describing how to construct the test name suffix for a set of arguments.
79          *
80          * See [Parameters.name] for more details.
81          */
82         val pattern: String,
83 
84         /**
85          * The sets of arguments.
86          *
87          * Each entry can be either an `Array<Any>` (for multiple arguments) or any other value (for
88          * a single argument).
89          */
90         val argumentSets: List<Any>,
91     )
92 
93     /** The wrapped [Parameterized] class. */
94     private val parameterized: Parameterized =
<lambda>null95         ParameterizedBuilder.build(clazz) {
96 
97             // Create a [TestClass] for the real test class and inject it.
98             val testClass =
99                 InjectedTestClass(clazz, parametersRunnerFactoryClass).also {
100                     // Inject it into [runnersFactory]
101                     testClass = it
102                 }
103 
104             // Get additional arguments (if any) from the actual test class.
105             val additionalArguments = getAdditionalArguments(testClass)
106 
107             // Obtain [TestArguments] from the provider and store the list of argument sets in the
108             // [RunnersFactory.allParametersField].
109             val testArguments = argumentsProvider(testClass, additionalArguments)
110             allParameters = testArguments.argumentSets
111 
112             // Get the [FrameworkMethod] for the [FakeTestClass.fakeParameters] method, extract its
113             // [FrameworkMethod.method], wrap that and the [TestArguments.pattern] in an
114             // [InjectedFrameworkMethod] that will intercept a request for [Parameters] annotation
115             // and return one containing the pattern supplied.
116             val fakeParametersMethod = parametersMethod
117             val injectedParametersMethod =
118                 InjectedFrameworkMethod(fakeParametersMethod.method, testArguments.pattern)
119             parametersMethod = injectedParametersMethod
120 
121             // Make sure that the [RunnersFactory.parameterCount] field is set correctly to the
122             // number of parameters.
123             parameterCount =
124                 if (allParameters.isEmpty()) 0
125                 else {
126                     val first = allParameters.first()
127                     (first as? Array<*>)?.size ?: 1
128                 }
129         }
130 
131     /** List containing [parameterized]. */
132     private val children: List<Runner> = mutableListOf(parameterized)
133 
134     companion object {
135         /**
136          * Get additional arguments, if any, provided by the [testClass] through use of a
137          * [Parameters] function.
138          *
139          * The returned values have been normalized so each entry is an `Array<Any>`.
140          */
getAdditionalArgumentsnull141         private fun getAdditionalArguments(testClass: TestClass): List<Array<Any>>? {
142             val parametersMethod =
143                 testClass.getAnnotatedMethods(Parameters::class.java).firstOrNull {
144                     it.isPublic && it.isStatic
145                 }
146                     ?: return null
147             return when (val parameters = parametersMethod.invokeExplosively(null)) {
148                     is List<*> -> parameters
149                     is Iterable<*> -> parameters.toList()
150                     is Array<*> -> parameters.toList()
151                     else ->
152                         error(
153                             "${testClass.name}.{${parametersMethod.name}() must return an Iterable of arrays."
154                         )
155                 }
156                 .filterNotNull()
157                 .map {
158                     if (
159                         it is Array<*> &&
160                             it.javaClass.isArray &&
161                             it.javaClass.componentType == Object::class.java
162                     ) {
163                         @Suppress("UNCHECKED_CAST")
164                         it as Array<Any>
165                     } else {
166                         arrayOf(it)
167                     }
168                 }
169         }
170     }
171 
172     /**
173      * A [TestClass] subclass that is injected into the [Parameterized.RunnersFactory] in order to
174      * intercept requests for information about the test class being run and supply information
175      * provide by this.
176      */
177     private class InjectedTestClass(
178         clazz: Class<*>,
179         /**
180          * The [ParametersRunnerFactory] class to use for creating runners for a specific set of
181          * test parameters.
182          */
183         private val runnerFactoryClass: KClass<out ParametersRunnerFactory>?,
184     ) : TestClass(clazz) {
getAnnotationnull185         override fun <T : Annotation> getAnnotation(annotationType: Class<T>): T? {
186             if (
187                 runnerFactoryClass != null &&
188                     annotationType == UseParametersRunnerFactory::class.java
189             ) {
190                 @Suppress("UNCHECKED_CAST")
191                 return UseParametersRunnerFactory(runnerFactoryClass) as T
192             }
193             return super.getAnnotation(annotationType)
194         }
195     }
196 
197     /**
198      * An extension of [FrameworkMethod] that exists to provide the custom [TestArguments.pattern]
199      * to [Parameterized.RunnersFactory] by intercepting a request for the [Parameters] annotation
200      * and returning one with the supplied [pattern].
201      */
202     private class InjectedFrameworkMethod(method: Method, val pattern: String) :
203         FrameworkMethod(method) {
getAnnotationnull204         override fun <T : Annotation> getAnnotation(annotationType: Class<T>): T? {
205             if (annotationType == Parameters::class.java) {
206                 @Suppress("UNCHECKED_CAST") return Parameters(name = pattern) as T
207             }
208             return super.getAnnotation(annotationType)
209         }
210     }
211 
getDescriptionnull212     override fun getDescription(): Description {
213         // Return the wrapped [parameterized]'s [Description] otherwise the description ends up
214         // looking something like this:
215         //     <class>
216         //         <class>
217         //             ...<method>...
218         //
219         // Which can cause issues with gradle test runner's handling of @Ignore.
220         return parameterized.description
221     }
222 
getChildrennull223     override fun getChildren() = children
224 
225     override fun describeChild(child: Runner): Description = child.description
226 
227     override fun runChild(child: Runner, notifier: RunNotifier) {
228         child.run(notifier)
229     }
230 
231     /**
232      * The main functionality of [Parameterized] is provided by the private class
233      * [Parameterized.RunnersFactory]. This class provides an abstract that allows instances of that
234      * to be constructed through reflection which is then used to construct a [Parameterized]
235      * instance.
236      */
237     private class ParameterizedBuilder {
238 
239         private val runnersFactory =
240             runnersFactoryConstructor.newInstance(FakeTestClass::class.java)
241 
242         // The following delegate to the corresponding field in [runnersFactory]
243         var testClass: TestClass by testClassField
244         var parametersMethod: FrameworkMethod by parametersMethodField
245         var allParameters: List<Any> by allParametersField
246         var parameterCount: Int by parameterCountField
247         private var runnerOverride: Runner? by runnerOverrideField
248 
249         init {
250             // The [FakeTestClass.fakeParameters] method throws an error so `runnerOverride` was set
251             // to a special runner that will report an error when the tests are run. Set the field
252             // to `null` to avoid that as actual arguments will be provided below.
253             runnerOverride = null
254         }
255 
256         /** Get this field from [runnersFactory]. */
getValuenull257         operator fun <T, V> Field.getValue(thisRef: T, property: KProperty<*>): V {
258             @Suppress("UNCHECKED_CAST") return get(runnersFactory) as V
259         }
260 
261         /** Set this field on [runnersFactory]. */
setValuenull262         operator fun <T, V> Field.setValue(thisRef: T, property: KProperty<*>, value: V) {
263             set(runnersFactory, value)
264         }
265 
266         /**
267          * Fake test class that is passed to the [Parameterized.RunnersFactory] to ensure that its
268          * constructor will complete successfully. Afterwards the fields in
269          * [Parameterized.RunnersFactory] will be updated to match the actual test class.
270          */
271         class FakeTestClass {
272             companion object {
273                 @JvmStatic
274                 @Parameters
fakeArgumentsnull275                 fun fakeArguments(): List<Any> = throw AssumptionViolatedException("fake arguments")
276             }
277         }
278 
279         companion object {
280             fun build(clazz: Class<*>, block: ParameterizedBuilder.() -> Unit): Parameterized {
281                 val builder = ParameterizedBuilder()
282                 builder.block()
283                 // Create a new `Parameterized` object.
284                 return parameterizedConstructor.newInstance(clazz, builder.runnersFactory)
285             }
286             /** [Parameterized] class. */
287             private val parameterizedClass = Parameterized::class.java
288 
289             /** The private [Parameterized.RunnersFactory] class. */
290             private val runnersFactoryClass =
291                 parameterizedClass.declaredClasses.first { it.simpleName == "RunnersFactory" }
292 
293             // Get the private `Parameterized(Class, RunnersFactory)` constructor.
294             private val parameterizedConstructor: Constructor<Parameterized> =
295                 parameterizedClass
296                     .getDeclaredConstructor(Class::class.java, runnersFactoryClass)
297                     .apply { isAccessible = true }
298 
299             // Create a new [Parameterized.RunnersFactory]. Uses [FakeTestClass] not the real test
300             // class. The correct information will be injected into it below.
301             private val runnersFactoryConstructor: Constructor<out Any> =
302                 runnersFactoryClass.getDeclaredConstructor(Class::class.java).apply {
303                     isAccessible = true
304                 }
305 
306             /** [Field.modifiers] field. */
307             private val modifiersField =
308                 getModifiersField().apply {
309                     try {
310                         // Modify the `modifiers` field for the field to remove `final`.
311                         // This requires "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED".
312                         isAccessible = true
313                     } catch (e: InaccessibleObjectException) {
314                         throw IllegalStateException(
315                             "Add --add-opens=java.base/java.lang.reflect=ALL-UNNAMED to jvm options",
316                             e
317                         )
318                     }
319                 }
320 
321             // Get various [Parameterized.RunnersFactory] fields and make them accessible and
322             // settable.
323             val testClassField = getSettableField("testClass")
324             val parametersMethodField = getSettableField("parametersMethod")
325             val allParametersField = getSettableField("allParameters")
326             val parameterCountField = getSettableField("parameterCount")
327             val runnerOverrideField = getSettableField("runnerOverride")
328 
329             /**
330              * Get an accessible and settable (i.e. not `final`) declared field called [name] in
331              * [runnersFactoryClass].
332              */
333             private fun getSettableField(name: String): Field {
334                 val field = runnersFactoryClass.getDeclaredField(name)
335                 field.isAccessible = true
336                 modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
337                 return field
338             }
339 
340             /**
341              * Need to use reflection to invoke `getDeclaredFields0` to get the hidden fields of the
342              * [Field] class, then select the one called `modifiers`.
343              */
344             private fun getModifiersField(): Field {
345                 val getDeclaredFields0 =
346                     Class::class.java.getDeclaredMethod("getDeclaredFields0", Boolean::class.java)
347                 getDeclaredFields0.isAccessible = true
348                 @Suppress("UNCHECKED_CAST")
349                 val fields = getDeclaredFields0.invoke(Field::class.java, false) as Array<Field>
350                 return fields.first { it.name == "modifiers" }
351             }
352         }
353     }
354 }
355