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