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