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