• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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