1 /* <lambda>null2 * Copyright (C) 2023 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.testsuite 18 19 import com.android.tools.lint.checks.infrastructure.TestFile 20 import com.android.tools.lint.checks.infrastructure.TestFiles 21 import com.android.tools.metalava.model.AnnotationManager 22 import com.android.tools.metalava.model.Assertions 23 import com.android.tools.metalava.model.Codebase 24 import com.android.tools.metalava.model.PackageFilter 25 import com.android.tools.metalava.model.annotation.DefaultAnnotationManager 26 import com.android.tools.metalava.model.api.surface.ApiSurfaces 27 import com.android.tools.metalava.model.provider.InputFormat 28 import com.android.tools.metalava.model.testing.CodebaseCreatorConfig 29 import com.android.tools.metalava.model.testing.CodebaseCreatorConfigAware 30 import com.android.tools.metalava.testing.TemporaryFolderOwner 31 import java.io.File 32 import org.junit.Rule 33 import org.junit.rules.TemporaryFolder 34 import org.junit.runner.RunWith 35 import org.junit.runners.Parameterized 36 import org.junit.runners.Parameterized.Parameter 37 38 /** 39 * Base class for tests that verify the behavior of model implementations. 40 * 41 * This is parameterized by [CodebaseCreatorConfig] as even though the tests are run in different 42 * projects the test results are collated and reported together. Having the parameters in the test 43 * name makes it easier to differentiate them. 44 * 45 * Note: In the top-level test report produced by Gradle it appears to just display whichever test 46 * ran last. However, the test reports in the model implementation projects do list each run 47 * separately. If this is an issue then the [ModelSuiteRunner] implementations could all be moved 48 * into the same project and run tests against them all at the same time. 49 */ 50 @RunWith(ModelTestSuiteRunner::class) 51 abstract class BaseModelTest() : 52 CodebaseCreatorConfigAware<ModelSuiteRunner>, TemporaryFolderOwner, Assertions { 53 54 /** 55 * Set by injection by [Parameterized] after class initializers are called. 56 * 57 * Anything that accesses this, either directly or indirectly must do it after initialization, 58 * e.g. from lazy fields or in methods called from test methods. 59 * 60 * The basic process is that each test class gets given a list of parameters. There are two ways 61 * to do that, through field injection or via constructor. If any fields in the test class 62 * hierarchy are annotated with the [Parameter] annotation then field injection is used, 63 * otherwise they are passed via constructor. 64 * 65 * The [Parameter] specifies the index within the list of parameters of the parameter that 66 * should be inserted into the field. The number of [Parameter] annotated fields must be the 67 * same as the number of parameters in the list and each index within the list must be specified 68 * by exactly one [Parameter]. 69 * 70 * The life-cycle of a parameterized test class is as follows: 71 * 1. The test class instance is created. 72 * 2. The parameters are injected into the [Parameter] annotated fields. 73 * 3. Follows the normal test class life-cycle. 74 */ 75 final override lateinit var codebaseCreatorConfig: CodebaseCreatorConfig<ModelSuiteRunner> 76 77 /** The [ModelSuiteRunner] that this test must use. */ 78 private val runner 79 get() = codebaseCreatorConfig.creator 80 81 /** 82 * The [InputFormat] of the test files that should be processed by this test. It must ignore all 83 * other [InputFormat]s. 84 */ 85 protected val inputFormat 86 get() = codebaseCreatorConfig.inputFormat 87 88 @get:Rule override val temporaryFolder = TemporaryFolder() 89 90 /** 91 * Set of inputs for a test. 92 * 93 * Currently, this is limited to one file but in future it may be more. 94 */ 95 data class InputSet( 96 /** The [InputFormat] of the [testFiles]. */ 97 val inputFormat: InputFormat, 98 99 /** The [TestFile]s to explicitly pass to code being tested. */ 100 val testFiles: List<TestFile>, 101 102 /** The optional [TestFile]s to pass on source path. */ 103 val additionalTestFiles: List<TestFile>?, 104 ) 105 106 /** Create an [InputSet] from a list of [TestFile]s. */ 107 fun inputSet(testFiles: List<TestFile>): InputSet = inputSet(*testFiles.toTypedArray()) 108 109 /** 110 * Create an [InputSet]. 111 * 112 * It is an error if [testFiles] is empty or if [testFiles] have a mixture of source 113 * ([InputFormat.JAVA] or [InputFormat.KOTLIN]) and signature ([InputFormat.SIGNATURE]). If it 114 * contains both [InputFormat.JAVA] and [InputFormat.KOTLIN] then the latter will be used. 115 */ 116 fun inputSet(vararg testFiles: TestFile, sourcePathFiles: List<TestFile>? = null): InputSet { 117 if (testFiles.isEmpty()) { 118 throw IllegalStateException("Must provide at least one source file") 119 } 120 121 val inputFormat = 122 testFiles 123 .asSequence() 124 // Map to path. 125 .map { it.targetRelativePath } 126 // Ignore HTML files. 127 .filter { !it.endsWith(".html") } 128 // Map to InputFormat. 129 .map { InputFormat.fromFilename(it) } 130 // Combine InputFormats to produce a single one, may throw an exception if they 131 // are incompatible. 132 .reduce { if1, if2 -> if1.combineWith(if2) } 133 134 return InputSet(inputFormat, testFiles.toList(), sourcePathFiles) 135 } 136 137 /** 138 * Context within which the main body of tests that check the state of the [Codebase] will run. 139 */ 140 interface CodebaseContext { 141 /** The newly created [Codebase]. */ 142 val codebase: Codebase 143 144 /** Replace any test run specific directories in [string] with a placeholder string. */ 145 fun removeTestSpecificDirectories(string: String): String 146 } 147 148 inner class DefaultCodebaseContext( 149 override val codebase: Codebase, 150 private val mainSourceDir: File, 151 ) : CodebaseContext { 152 override fun removeTestSpecificDirectories(string: String): String { 153 return cleanupString(string, mainSourceDir) 154 } 155 } 156 157 /** Additional properties that affect the behavior of the test. */ 158 data class TestFixture( 159 /** The [AnnotationManager] to use when creating a [Codebase]. */ 160 val annotationManager: AnnotationManager = DefaultAnnotationManager(), 161 162 /** 163 * The optional [PackageFilter] that defines which packages can contribute to the API. If 164 * this is unspecified then all packages can contribute to the API. 165 */ 166 val apiPackages: PackageFilter? = null, 167 168 /** The set of [ApiSurfaces] used in the test. */ 169 val apiSurfaces: ApiSurfaces = ApiSurfaces.DEFAULT 170 ) { 171 /** The [Codebase.Config] to use when creating a [Codebase] to test. */ 172 val codebaseConfig = 173 Codebase.Config( 174 annotationManager = annotationManager, 175 apiSurfaces = apiSurfaces, 176 ) 177 } 178 179 /** 180 * Create a [Codebase] from one of the supplied [inputSets] and then run a test on that 181 * [Codebase]. 182 * 183 * The [InputSet] that is selected is the one whose [InputSet.inputFormat] is the same as the 184 * current [inputFormat]. There can be at most one of those. 185 */ 186 private fun createCodebaseFromInputSetAndRun( 187 inputSets: Array<out InputSet>, 188 commonSourcesByInputFormat: Map<InputFormat, InputSet>, 189 testFixture: TestFixture, 190 test: CodebaseContext.() -> Unit, 191 ) { 192 // Run the input set that matches the current inputFormat, if there is one. 193 inputSets 194 .singleOrNull { it.inputFormat == inputFormat } 195 ?.let { inputSet -> 196 val mainSourceDir = sourceDir(inputSet) 197 198 val additionalSourceDir = inputSet.additionalTestFiles?.let { sourceDir(it) } 199 200 val commonSourceDir = 201 commonSourcesByInputFormat[inputFormat]?.let { commonInputSet -> 202 sourceDir(commonInputSet) 203 } 204 205 val inputs = 206 ModelSuiteRunner.TestInputs( 207 inputFormat = inputSet.inputFormat, 208 modelOptions = codebaseCreatorConfig.modelOptions, 209 mainSourceDir = mainSourceDir, 210 additionalMainSourceDir = additionalSourceDir, 211 commonSourceDir = commonSourceDir, 212 testFixture = testFixture, 213 ) 214 runner.createCodebaseAndRun(inputs) { codebase -> 215 val context = DefaultCodebaseContext(codebase, mainSourceDir.dir) 216 context.test() 217 } 218 } 219 } 220 221 private fun sourceDir(inputSet: InputSet): ModelSuiteRunner.SourceDir { 222 return sourceDir(inputSet.testFiles) 223 } 224 225 private fun sourceDir(testFiles: List<TestFile>): ModelSuiteRunner.SourceDir { 226 val tempDir = temporaryFolder.newFolder() 227 return ModelSuiteRunner.SourceDir(dir = tempDir, contents = testFiles) 228 } 229 230 private fun testFilesToInputSets(testFiles: Array<out TestFile>): Array<InputSet> { 231 return testFiles.map { inputSet(it) }.toTypedArray() 232 } 233 234 /** 235 * Create a [Codebase] from one of the supplied [sources] and then run the [test] on that 236 * [Codebase]. 237 * 238 * The [sources] array should have at most one [TestFile] whose extension matches an 239 * [InputFormat.extension]. 240 */ 241 fun runCodebaseTest( 242 vararg sources: TestFile, 243 commonSources: Array<TestFile> = emptyArray(), 244 testFixture: TestFixture = TestFixture(), 245 test: CodebaseContext.() -> Unit, 246 ) { 247 runCodebaseTest( 248 sources = testFilesToInputSets(sources), 249 commonSources = testFilesToInputSets(commonSources), 250 testFixture = testFixture, 251 test = test, 252 ) 253 } 254 255 /** 256 * Create a [Codebase] from one of the supplied [sources] [InputSet] and then run the [test] on 257 * that [Codebase]. 258 * 259 * The [sources] array should have at most one [InputSet] of each [InputFormat]. 260 */ 261 fun runCodebaseTest( 262 vararg sources: InputSet, 263 commonSources: Array<InputSet> = emptyArray(), 264 testFixture: TestFixture = TestFixture(), 265 test: CodebaseContext.() -> Unit, 266 ) { 267 runCodebaseTest( 268 sources = sources, 269 commonSourcesByInputFormat = commonSources.associateBy { it.inputFormat }, 270 testFixture = testFixture, 271 test = test, 272 ) 273 } 274 275 /** 276 * Create a [Codebase] from one of the supplied [sources] [InputSet] and then run the [test] on 277 * that [Codebase]. 278 * 279 * The [sources] array should have at most one [InputSet] of each [InputFormat]. 280 */ 281 private fun runCodebaseTest( 282 vararg sources: InputSet, 283 commonSourcesByInputFormat: Map<InputFormat, InputSet> = emptyMap(), 284 testFixture: TestFixture, 285 test: CodebaseContext.() -> Unit, 286 ) { 287 createCodebaseFromInputSetAndRun( 288 inputSets = sources, 289 commonSourcesByInputFormat = commonSourcesByInputFormat, 290 testFixture = testFixture, 291 test = test, 292 ) 293 } 294 295 /** 296 * Create a [Codebase] from one of the supplied [sources] and then run the [test] on that 297 * [Codebase]. 298 * 299 * The [sources] array should have at most one [TestFile] whose extension matches an 300 * [InputFormat.extension]. 301 */ 302 fun runSourceCodebaseTest( 303 vararg sources: TestFile, 304 commonSources: Array<TestFile> = emptyArray(), 305 testFixture: TestFixture = TestFixture(), 306 test: CodebaseContext.() -> Unit, 307 ) { 308 runSourceCodebaseTest( 309 sources = testFilesToInputSets(sources), 310 commonSourcesByInputFormat = 311 testFilesToInputSets(commonSources).associateBy { it.inputFormat }, 312 testFixture = testFixture, 313 test = test, 314 ) 315 } 316 317 /** 318 * Create a [Codebase] from one of the supplied [sources] [InputSet]s and then run the [test] on 319 * that [Codebase]. 320 * 321 * The [sources] array should have at most one [InputSet] of each [InputFormat]. 322 */ 323 fun runSourceCodebaseTest( 324 vararg sources: InputSet, 325 commonSources: Array<InputSet> = emptyArray(), 326 testFixture: TestFixture = TestFixture(), 327 test: CodebaseContext.() -> Unit, 328 ) { 329 runSourceCodebaseTest( 330 sources = sources, 331 commonSourcesByInputFormat = commonSources.associateBy { it.inputFormat }, 332 testFixture = testFixture, 333 test = test, 334 ) 335 } 336 337 /** 338 * Create a [Codebase] from one of the supplied [sources] [InputSet]s and then run the [test] on 339 * that [Codebase]. 340 * 341 * The [sources] array should have at most one [InputSet] of each [InputFormat]. 342 */ 343 private fun runSourceCodebaseTest( 344 vararg sources: InputSet, 345 commonSourcesByInputFormat: Map<InputFormat, InputSet>, 346 testFixture: TestFixture, 347 test: CodebaseContext.() -> Unit, 348 ) { 349 createCodebaseFromInputSetAndRun( 350 inputSets = sources, 351 commonSourcesByInputFormat = commonSourcesByInputFormat, 352 testFixture = testFixture, 353 test = test, 354 ) 355 } 356 357 /** 358 * Create a signature [TestFile] with the supplied [contents] in a file with a path of 359 * `api.txt`. 360 */ 361 fun signature(contents: String): TestFile = signature("api.txt", contents) 362 363 /** Create a signature [TestFile] with the supplied [contents] in a file with a path of [to]. */ 364 fun signature(to: String, contents: String): TestFile = 365 TestFiles.source(to, contents.trimIndent()) 366 } 367