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