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