1 /*
<lambda>null2  * Copyright 2020 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 androidx.build.testConfiguration
18 
19 import androidx.build.AndroidXExtension
20 import androidx.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK
21 import androidx.build.androidXExtension
22 import androidx.build.asFilenamePrefix
23 import androidx.build.dependencyTracker.AffectedModuleDetector
24 import androidx.build.getFileInTestConfigDirectory
25 import androidx.build.hasBenchmarkPlugin
26 import androidx.build.isMacrobenchmark
27 import androidx.build.isPresubmitBuild
28 import com.android.build.api.artifact.Artifacts
29 import com.android.build.api.artifact.SingleArtifact
30 import com.android.build.api.attributes.BuildTypeAttr
31 import com.android.build.api.dsl.ApplicationExtension
32 import com.android.build.api.dsl.CommonExtension
33 import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
34 import com.android.build.api.dsl.TestExtension
35 import com.android.build.api.variant.AndroidComponentsExtension
36 import com.android.build.api.variant.ApkOutputProviders
37 import com.android.build.api.variant.ApplicationAndroidComponentsExtension
38 import com.android.build.api.variant.HasDeviceTests
39 import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
40 import com.android.build.api.variant.LibraryAndroidComponentsExtension
41 import com.android.build.api.variant.TestAndroidComponentsExtension
42 import com.android.build.api.variant.Variant
43 import java.util.function.Consumer
44 import kotlin.math.max
45 import org.gradle.api.Project
46 import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
47 import org.gradle.api.attributes.Usage
48 import org.gradle.api.file.RegularFile
49 import org.gradle.api.provider.Provider
50 import org.gradle.api.tasks.TaskProvider
51 import org.gradle.kotlin.dsl.getByType
52 import org.gradle.kotlin.dsl.named
53 
54 /**
55  * Creates and configures the test config generation task for a project. Configuration includes
56  * populating the task with relevant data from the first 4 params, and setting whether the task is
57  * enabled.
58  */
59 private fun Project.createTestConfigurationGenerationTask(
60     variantName: String,
61     artifacts: Artifacts,
62     minSdk: Int,
63     testRunner: Provider<String>,
64     instrumentationRunnerArgs: Provider<Map<String, String>>,
65     variant: Variant?,
66     projectIsolationEnabled: Boolean,
67 ) {
68     val copyTestApksTask = registerCopyTestApksTask(variantName, artifacts, variant)
69 
70     if (isPrivacySandboxEnabled()) {
71         /*
72         Privacy Sandbox SDKs could be installed starting from PRIVACY_SANDBOX_MIN_API_LEVEL.
73         Separate compat config generated for lower api levels.
74         */
75         registerGenerateTestConfigurationTask(
76             "${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}$variantName",
77             TestConfigType.PRIVACY_SANDBOX_MAIN,
78             xmlName = "${path.asFilenamePrefix()}$variantName.xml",
79             jsonName = null, // Privacy sandbox not yet supported in JSON configs
80             copyTestApksTask.flatMap { it.outputApplicationId },
81             copyTestApksTask.flatMap { it.outputTestApk },
82             minSdk = max(minSdk, PRIVACY_SANDBOX_MIN_API_LEVEL),
83             testRunner,
84             instrumentationRunnerArgs,
85             variant,
86             projectIsolationEnabled,
87         )
88 
89         registerGenerateTestConfigurationTask(
90             "${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${variantName}",
91             TestConfigType.PRIVACY_SANDBOX_COMPAT,
92             xmlName = "${path.asFilenamePrefix()}${variantName}Compat.xml",
93             jsonName = null, // Privacy sandbox not yet supported in JSON configs
94             copyTestApksTask.flatMap { it.outputApplicationId },
95             copyTestApksTask.flatMap { it.outputTestApk },
96             minSdk,
97             testRunner,
98             instrumentationRunnerArgs,
99             variant,
100             projectIsolationEnabled,
101         )
102     } else {
103         registerGenerateTestConfigurationTask(
104             "${GENERATE_TEST_CONFIGURATION_TASK}$variantName",
105             TestConfigType.DEFAULT,
106             xmlName = "${path.asFilenamePrefix()}$variantName.xml",
107             jsonName = "_${path.asFilenamePrefix()}$variantName.json",
108             copyTestApksTask.flatMap { it.outputApplicationId },
109             copyTestApksTask.flatMap { it.outputTestApk },
110             minSdk,
111             testRunner,
112             instrumentationRunnerArgs,
113             variant,
114             projectIsolationEnabled,
115         )
116     }
117 }
118 
Projectnull119 private fun Project.registerCopyTestApksTask(
120     variantName: String,
121     artifacts: Artifacts,
122     variant: Variant?
123 ): TaskProvider<CopyTestApksTask> {
124     return tasks.register("${COPY_TEST_APKS_TASK}$variantName", CopyTestApksTask::class.java) { task
125         ->
126         task.testFolder.set(artifacts.get(SingleArtifact.APK))
127         task.testLoader.set(artifacts.getBuiltArtifactsLoader())
128 
129         task.outputApplicationId.set(layout.buildDirectory.file("$variantName-appId.txt"))
130         task.outputTestApk.set(
131             getFileInTestConfigDirectory("${path.asFilenamePrefix()}-$variantName.apk")
132         )
133 
134         // Skip task if getTestSourceSetsForAndroid is empty, even if
135         //  androidXExtension.deviceTests.enabled is set to true
136         task.androidTestSourceCode.from(getTestSourceSetsForAndroid(variant))
137         val androidXExtension = extensions.getByType<AndroidXExtension>()
138         task.enabled = androidXExtension.deviceTests.enabled
139         AffectedModuleDetector.configureTaskGuard(task)
140     }
141 }
142 
Projectnull143 private fun Project.registerGenerateTestConfigurationTask(
144     taskName: String,
145     configType: TestConfigType,
146     xmlName: String,
147     jsonName: String?,
148     applicationIdFile: Provider<RegularFile>,
149     testApk: Provider<RegularFile>,
150     minSdk: Int,
151     testRunner: Provider<String>,
152     instrumentationRunnerArgs: Provider<Map<String, String>>,
153     variant: Variant?,
154     projectIsolationEnabled: Boolean,
155 ) {
156     val generateTestConfigurationTask =
157         tasks.register(taskName, GenerateTestConfigurationTask::class.java) { task ->
158             task.testConfigType.set(configType)
159 
160             task.applicationId.set(project.providers.fileContents(applicationIdFile).asText)
161             task.testApk.set(testApk)
162 
163             val androidXExtension = extensions.getByType<AndroidXExtension>()
164             task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
165             task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
166             task.outputXml.set(getFileInTestConfigDirectory(xmlName))
167             jsonName?.let { task.outputJson.set(getFileInTestConfigDirectory(it)) }
168             task.presubmit.set(isPresubmitBuild())
169             task.instrumentationArgs.putAll(instrumentationRunnerArgs)
170             task.minSdk.set(minSdk)
171             task.hasBenchmarkPlugin.set(hasBenchmarkPlugin())
172             task.macrobenchmark.set(isMacrobenchmark())
173             task.testRunner.set(testRunner)
174             // Skip task if getTestSourceSetsForAndroid is empty, even if
175             //  androidXExtension.deviceTests.enabled is set to true
176             task.androidTestSourceCodeCollection.from(getTestSourceSetsForAndroid(variant))
177             task.enabled = androidXExtension.deviceTests.enabled
178             AffectedModuleDetector.configureTaskGuard(task)
179         }
180     if (!projectIsolationEnabled) {
181         rootProject.tasks
182             .findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!
183             .dependsOn(generateTestConfigurationTask)
184         addToModuleInfo(testName = xmlName, projectIsolationEnabled)
185     }
186     androidXExtension.testModuleNames.add(xmlName)
187 }
188 
189 /**
190  * Further configures the test config generation task for a project. This only gets called when
191  * there is a test app in addition to the instrumentation app, and the only thing it configures is
192  * the location of the testapp.
193  */
Projectnull194 fun Project.addAppApkToTestConfigGeneration(androidXExtension: AndroidXExtension) {
195 
196     fun outputAppApkFile(
197         variant: Variant,
198         appProjectPath: String,
199         instrumentationProjectPath: String?
200     ): Provider<RegularFile> {
201         var filename = appProjectPath.asFilenamePrefix()
202         if (instrumentationProjectPath != null) {
203             filename += "_for_${instrumentationProjectPath.asFilenamePrefix()}"
204         }
205         filename += "-${variant.name}.apk"
206         return getFileInTestConfigDirectory(filename)
207     }
208 
209     // For application modules, the instrumentation apk is generated in the module itself
210     extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
211         onVariants(selector().withBuildType("debug")) { variant ->
212             if (isPrivacySandboxEnabled()) {
213                 @Suppress("UnstableApiUsage") // variant.outputProviders b/397701480
214                 addAppApksToPrivacySandboxTestConfigsGeneration(
215                     testVariantName = "${variant.name}AndroidTest",
216                     variant,
217                     variant.outputProviders
218                 )
219             } else {
220                 // TODO(b/347956800): Migrate to ApkOutputProviders after testing on PrivacySandbox
221                 addAppApkFromArtifactsToTestConfigGeneration(
222                     testVariantName = "${variant.name}AndroidTest",
223                     variant,
224                     configureAction = { task ->
225                         task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
226 
227                         // The target project is the same being evaluated
228                         task.outputAppApk.set(outputAppApkFile(variant, path, null))
229                     }
230                 )
231             }
232         }
233     }
234 
235     // Migrate away when b/280680434 is fixed.
236     // For tests modules, the instrumentation apk is pulled from the <variant>TestedApks
237     // configuration. Note that also the associated test configuration task name is different
238     // from the application one.
239     extensions.findByType(TestAndroidComponentsExtension::class.java)?.apply {
240         onVariants(selector().all()) { variant ->
241             if (isPrivacySandboxEnabled()) {
242                 @Suppress("UnstableApiUsage") // variant.outputProviders b/397701480
243                 addAppApksToPrivacySandboxTestConfigsGeneration(
244                     testVariantName = variant.name,
245                     variant,
246                     variant.outputProviders
247                 )
248             } else {
249                 // TODO(b/347956800): Migrate to ApkOutputProviders after b/378675038
250                 addAppApkFromArtifactsToTestConfigGeneration(
251                     testVariantName = variant.name,
252                     variant,
253                     configureAction = { task ->
254                         // The target app path is defined in the targetProjectPath field in the
255                         // android extension of the test module
256                         val targetProjectPath =
257                             project.extensions
258                                 .getByType(TestExtension::class.java)
259                                 .targetProjectPath
260                                 ?: throw IllegalStateException(
261                                     """
262                                 Module `$path` does not have a targetProjectPath defined.
263                             """
264                                         .trimIndent()
265                                 )
266                         task.outputAppApk.set(outputAppApkFile(variant, targetProjectPath, path))
267 
268                         task.appFileCollection.from(
269                             configurations
270                                 .named("${variant.name}TestedApks")
271                                 .get()
272                                 .incoming
273                                 .artifactView {
274                                     it.attributes { container ->
275                                         container.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk")
276                                     }
277                                 }
278                                 .files
279                         )
280                     }
281                 )
282             }
283         }
284     }
285 
286     // For library modules we only look at the build type release. The target app project can be
287     // specified through the androidX extension, through: targetAppProjectForInstrumentationTest
288     // and targetAppProjectVariantForInstrumentationTest.
289     extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.apply {
290         onVariants(selector().withBuildType("release")) { variant ->
291             val targetAppProject =
292                 androidXExtension.deviceTests.targetAppProject ?: return@onVariants
293             val targetAppProjectVariant = androidXExtension.deviceTests.targetAppVariant
294 
295             // Recreate the same configuration existing for test modules to pull the artifact
296             // from the application module specified in the deviceTests extension.
297             @Suppress("UnstableApiUsage") // Incubating dependencyFactory APIs
298             val configuration =
299                 configurations.create("${variant.name}TestedApks") { config ->
300                     config.isCanBeResolved = true
301                     config.isCanBeConsumed = false
302                     config.attributes {
303                         it.attribute(
304                             BuildTypeAttr.ATTRIBUTE,
305                             objects.named(targetAppProjectVariant)
306                         )
307                         it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
308                     }
309                     config.dependencies.add(project.dependencyFactory.create(targetAppProject))
310                 }
311 
312             addAppApkFromArtifactsToTestConfigGeneration(
313                 testVariantName = "${variant.name}AndroidTest",
314                 variant,
315                 configureAction = { task ->
316                     // The target app path is defined in the androidx extension
317                     task.outputAppApk.set(outputAppApkFile(variant, targetAppProject.path, path))
318 
319                     task.appFileCollection.from(
320                         configuration.incoming
321                             .artifactView { view ->
322                                 view.attributes { it.attribute(ARTIFACT_TYPE_ATTRIBUTE, "apk") }
323                             }
324                             .files
325                     )
326                 }
327             )
328         }
329     }
330 }
331 
Projectnull332 private fun Project.addAppApkFromArtifactsToTestConfigGeneration(
333     testVariantName: String,
334     variant: Variant,
335     configureAction: Consumer<CopyApkFromArtifactsTask>
336 ) {
337     val copyApkTask = registerCopyAppApkFromArtifactsTask(variant, configureAction)
338     tasks.named(
339         "${GENERATE_TEST_CONFIGURATION_TASK}$testVariantName",
340         GenerateTestConfigurationTask::class.java
341     ) { t ->
342         t.appApksModel.set(copyApkTask.flatMap(CopyApkFromArtifactsTask::outputAppApksModel))
343     }
344 }
345 
346 // TODO(b/347956800): Call from createTestConfigurationGenerationTask() after b/378674313
347 // TODO(b/347956800): Use tasks providers directly instead of tasks.named after b/378674313
348 @Suppress("UnstableApiUsage") // ApkOutputProviders b/397701480
Projectnull349 private fun Project.addAppApksToPrivacySandboxTestConfigsGeneration(
350     testVariantName: String,
351     variant: Variant,
352     outputProviders: ApkOutputProviders,
353 ) {
354     val copyTestApksTask =
355         tasks.named("${COPY_TEST_APKS_TASK}${testVariantName}", CopyTestApksTask::class.java)
356     val excludeTestApk = copyTestApksTask.flatMap(CopyTestApksTask::outputTestApk)
357 
358     val copyMainApksTask =
359         registerCopyPrivacySandboxMainAppApksTask(variant, outputProviders, excludeTestApk)
360     tasks.named(
361         "${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}${testVariantName}",
362         GenerateTestConfigurationTask::class.java
363     ) { t ->
364         t.appApksModel.set(
365             copyMainApksTask.flatMap(CopyApksFromOutputProviderTask::outputAppApksModel)
366         )
367     }
368 
369     val copyCompatApksTask =
370         registerCopyPrivacySandboxCompatAppApksTask(variant, outputProviders, excludeTestApk)
371     tasks.named(
372         "${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${testVariantName}",
373         GenerateTestConfigurationTask::class.java
374     ) { t ->
375         t.appApksModel.set(
376             copyCompatApksTask.flatMap(CopyApksFromOutputProviderTask::outputAppApksModel)
377         )
378     }
379 }
380 
Projectnull381 fun Project.configureTestConfigGeneration(
382     commonExtension: CommonExtension<*, *, *, *, *, *>,
383     projectIsolationEnabled: Boolean,
384 ) {
385     extensions.getByType(AndroidComponentsExtension::class.java).apply {
386         onVariants { variant ->
387             when {
388                 variant is HasDeviceTests -> {
389                     variant.deviceTests.forEach { (_, deviceTest) ->
390                         createTestConfigurationGenerationTask(
391                             deviceTest.name,
392                             deviceTest.artifacts,
393                             // replace minSdk after b/328495232 is fixed
394                             commonExtension.defaultConfig.minSdk!!,
395                             deviceTest.instrumentationRunner,
396                             deviceTest.instrumentationRunnerArguments,
397                             variant,
398                             projectIsolationEnabled,
399                         )
400                     }
401                 }
402                 project.plugins.hasPlugin("com.android.test") -> {
403                     createTestConfigurationGenerationTask(
404                         variant.name,
405                         variant.artifacts,
406                         // replace minSdk after b/328495232 is fixed
407                         commonExtension.defaultConfig.minSdk!!,
408                         provider { commonExtension.defaultConfig.testInstrumentationRunner!! },
409                         provider {
410                             commonExtension.defaultConfig.testInstrumentationRunnerArguments
411                         },
412                         variant,
413                         projectIsolationEnabled,
414                     )
415                 }
416             }
417         }
418     }
419 }
420 
421 // KotlinMultiplatformAndroidComponentsExtension is @Incubating b/393137152
422 @Suppress("UnstableApiUsage")
Projectnull423 fun Project.configureTestConfigGeneration(
424     kotlinMultiplatformAndroidTarget: KotlinMultiplatformAndroidLibraryTarget,
425     componentsExtension: KotlinMultiplatformAndroidComponentsExtension,
426     projectIsolationEnabled: Boolean,
427 ) {
428     componentsExtension.onVariant { variant ->
429         variant.deviceTests.forEach { (_, deviceTest) ->
430             createTestConfigurationGenerationTask(
431                 deviceTest.name,
432                 deviceTest.artifacts,
433                 // replace minSdk after b/328495232 is fixed
434                 kotlinMultiplatformAndroidTarget.minSdk!!,
435                 deviceTest.instrumentationRunner,
436                 deviceTest.instrumentationRunnerArguments,
437                 null,
438                 projectIsolationEnabled,
439             )
440         }
441     }
442 }
443 
Projectnull444 private fun Project.isPrivacySandboxEnabled(): Boolean =
445     extensions.findByType(ApplicationExtension::class.java)?.privacySandbox?.enable
446         ?: extensions.findByType(TestExtension::class.java)?.privacySandbox?.enable
447         ?: false
448 
449 private const val COPY_TEST_APKS_TASK = "CopyTestApks"
450 private const val GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK =
451     "GeneratePrivacySandboxMainTestConfiguration"
452 private const val GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK =
453     "GeneratePrivacySandboxCompatTestConfiguration"
454 private const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
455