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