blob: 46fed81ca1786655553084169f9f58923303fcbc [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.build.gradle.internal.tasks
import com.android.SdkConstants
import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.dsl.Device
import com.android.build.api.instrumentation.manageddevice.DeviceTestRunConfigureAction
import com.android.build.api.instrumentation.manageddevice.DeviceTestRunInput
import com.android.build.api.instrumentation.manageddevice.DeviceTestRunParameters
import com.android.build.api.instrumentation.manageddevice.DeviceTestRunTaskAction
import com.android.build.api.variant.ScopedArtifacts
import com.android.build.gradle.internal.component.DeviceTestCreationConfig
import com.android.build.gradle.internal.component.InstrumentedTestCreationConfig
import com.android.build.gradle.internal.profile.ProfileAwareWorkAction
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.tasks.DeviceProviderInstrumentTestTask.checkForNonApks
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.internal.test.AbstractTestDataImpl
import com.android.build.gradle.internal.test.gatherTestLibraries
import com.android.build.gradle.internal.test.recordCrashedInstrumentedTestRun
import com.android.build.gradle.internal.test.recordOkInstrumentedTestRun
import com.android.build.gradle.internal.test.report.ReportType
import com.android.build.gradle.internal.test.report.TestReport
import com.android.build.gradle.internal.testing.StaticTestData
import com.android.build.gradle.internal.testing.TestData
import com.android.build.gradle.internal.testing.TestRunData
import com.android.build.gradle.internal.utils.setDisallowChanges
import com.android.build.gradle.options.BooleanOption
import com.android.buildanalyzer.common.TaskCategory
import com.android.builder.model.TestOptions
import com.android.utils.FileUtils
import com.google.common.annotations.VisibleForTesting
import com.google.wireless.android.sdk.stats.TestLibraries
import org.gradle.api.GradleException
import org.gradle.api.InvalidUserDataException
import org.gradle.api.artifacts.ArtifactCollection
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.api.model.ObjectFactory
import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.gradle.internal.logging.ConsoleRenderer
import java.io.File
import javax.inject.Inject
@BuildAnalyzer(primaryTaskCategory = TaskCategory.TEST)
@CacheableTask
abstract class ManagedDeviceTestTask: NonIncrementalTask(), AndroidTestTask {
@get:Inject
abstract val objectFactory: ObjectFactory
@get:Input
abstract val testAction: Property<Class<out DeviceTestRunTaskAction<out DeviceTestRunInput>>>
@get:Nested
abstract val deviceInput: Property<DeviceTestRunInput>
@get:Nested
abstract val testData: Property<TestData>
@get:Input
abstract val deviceDslName: Property<String>
@get:InputFiles
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:Optional
abstract val setupResult: DirectoryProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val buddyApks: ConfigurableFileCollection
/** Whether failing tests should fail the build */
private var shouldIgnore: Boolean = false
// For analytics only
@get:Internal
@get:VisibleForTesting
@set:VisibleForTesting
lateinit var dependencies: ArtifactCollection
protected set
@get:Classpath
@get:Optional
abstract val classes: ConfigurableFileCollection
@get:Classpath
@get:Optional
abstract val buildConfigClasses: ConfigurableFileCollection
@get:Classpath
@get:Optional
abstract val rClasses: ConfigurableFileCollection
override fun getIgnoreFailures(): Boolean = shouldIgnore
override fun setIgnoreFailures(ignore: Boolean) {
shouldIgnore = ignore
}
@OutputDirectory
abstract override fun getResultsDir(): DirectoryProperty
@OutputDirectory
abstract fun getReportsDir(): DirectoryProperty
@OutputDirectory
abstract fun getCoverageDirectory(): DirectoryProperty
@Input
abstract fun getAdditionalTestOutputEnabled(): Property<Boolean>
@Optional
@OutputDirectory
abstract fun getAdditionalTestOutputDir(): DirectoryProperty
@get:Optional
@get:Input
abstract val installOptions: ListProperty<String>
@get:Input
abstract val executionEnum: Property<TestOptions.Execution>
public override fun doTaskAction() {
checkForNonApks(buddyApks.files) { message: String ->
throw InvalidUserDataException(message)
}
FileUtils.cleanOutputDir(resultsDir.get().asFile)
FileUtils.cleanOutputDir(getCoverageDirectory().get().asFile)
val additionalTestOutputDir = if (getAdditionalTestOutputEnabled().get()) {
getAdditionalTestOutputDir().get().also {
FileUtils.cleanOutputDir(it.asFile)
}
} else {
null
}
workerExecutor.noIsolation().submit(ManagedDeviceTestRunnable::class.java) { params ->
val testRunTaskAction = objectFactory.newInstance(testAction.get())
as DeviceTestRunTaskAction<DeviceTestRunInput>
params.initializeWith(projectPath, path, analyticsService)
params.testsFound.setDisallowChanges(testsFound())
params.deviceInput.setDisallowChanges(deviceInput)
params.setupResult.setDisallowChanges(setupResult)
params.testData.setDisallowChanges(testData.get().getAsStaticData())
params.testRunAction.setDisallowChanges(testRunTaskAction)
params.path.setDisallowChanges(path)
params.flavorName.setDisallowChanges(testData.get().flavorName)
params.deviceDslName.setDisallowChanges(deviceDslName)
params.resultsOutDir.setDisallowChanges(resultsDir)
params.codeCoverageOutDir.setDisallowChanges(getCoverageDirectory())
params.additionalTestOutputDir.setDisallowChanges(additionalTestOutputDir)
params.reportsDir.setDisallowChanges(getReportsDir())
params.installOptions.setDisallowChanges(installOptions)
params.buddyApks.setFrom(buddyApks)
params.testLibraries.setDisallowChanges(
gatherTestLibraries(dependencies)
)
params.ignoreFailures.setDisallowChanges(ignoreFailures)
params.executionEnum.setDisallowChanges(executionEnum)
}
}
/**
* Determines if there are any tests to run.
*
* @return true if there are some tests to run, false otherwise
*/
private fun testsFound(): Boolean {
return testData
.get()
.hasTests(classes, rClasses, buildConfigClasses)
.get()
}
abstract class ManagedDeviceTestRunnable :
ProfileAwareWorkAction<ManagedDeviceTestRunnableParameters>() {
val logger: Logger = Logging.getLogger("ManagedDeviceTestRunnable")
override fun run() {
val success = if (!parameters.testsFound.get()) {
logger.info("No tests found, nothing to do.")
true
} else {
try {
val testRunParams = object: DeviceTestRunParameters<DeviceTestRunInput> {
override val deviceInput = parameters.deviceInput.get()
override val setupResult = parameters.setupResult
override val testRunData = TestRunData(
parameters.flavorName.get(),
parameters.path.get(),
parameters.deviceDslName.get(),
parameters.resultsOutDir.get(),
parameters.codeCoverageOutDir.get(),
parameters.additionalTestOutputDir.get(),
parameters.installOptions.getOrElse(listOf()),
parameters.buddyApks.files,
parameters.projectPath.get(),
parameters.testData.get()
)
}
parameters.testRunAction.get().runTests(testRunParams)
} catch (e: Exception) {
recordCrashedInstrumentedTestRun(
parameters.testLibraries.get(),
parameters.executionEnum.get(),
false,
parameters.analyticsService.get())
throw e
}
}
val reportOutDir = parameters.reportsDir.get().asFile
FileUtils.cleanOutputDir(reportOutDir)
val report = TestReport(
ReportType.SINGLE_FLAVOR, parameters.resultsOutDir.get().asFile, reportOutDir)
val results = report.generateReport()
recordOkInstrumentedTestRun(
parameters.testLibraries.get(),
parameters.executionEnum.get(),
false,
results.testCount,
parameters.analyticsService.get())
if (!success) {
val reportUrl = ConsoleRenderer().asClickableFileUrl(
File(reportOutDir, "index.html"))
val message = """
There were failing tests for Device: ${parameters.deviceDslName.get()}.
See the report at: $reportUrl
""".trimIndent()
if (parameters.ignoreFailures.get()) {
logger.warn(message)
return
} else {
throw GradleException(message)
}
}
}
}
abstract class ManagedDeviceTestRunnableParameters : ProfileAwareWorkAction.Parameters() {
abstract val testsFound: Property<Boolean>
abstract val deviceInput: Property<DeviceTestRunInput>
abstract val setupResult: DirectoryProperty
abstract val testData: Property<StaticTestData>
abstract val testRunAction: Property<DeviceTestRunTaskAction<DeviceTestRunInput>>
abstract val path: Property<String>
abstract val flavorName: Property<String>
abstract val deviceDslName: Property<String>
abstract val resultsOutDir: DirectoryProperty
abstract val codeCoverageOutDir: DirectoryProperty
abstract val additionalTestOutputDir: DirectoryProperty
abstract val reportsDir: DirectoryProperty
abstract val installOptions: ListProperty<String>
abstract val buddyApks: ConfigurableFileCollection
abstract val testLibraries: Property<TestLibraries>
abstract val ignoreFailures: Property<Boolean>
abstract val executionEnum: Property<TestOptions.Execution>
}
class CreationAction<DeviceT: Device>(
creationConfig: InstrumentedTestCreationConfig,
private val device: DeviceT,
private val testRunConfigAction : Class<out DeviceTestRunConfigureAction<DeviceT, *>>,
private val testRunTaskAction: Class<out DeviceTestRunTaskAction<*>>,
private val testData: AbstractTestDataImpl,
private val testResultOutputDir: File,
private val testReportOutputDir: File,
private val additionalTestOutputDir: File,
private val coverageOutputDir: File,
private val setupResultDir: Provider<Directory>?,
nameSuffix: String = ""
): VariantTaskCreationAction<
ManagedDeviceTestTask,
InstrumentedTestCreationConfig
>(creationConfig) {
override val name = computeTaskName(device.name, nameSuffix)
override val type = ManagedDeviceTestTask::class.java
override fun handleProvider(taskProvider: TaskProvider<ManagedDeviceTestTask>) {
super.handleProvider(taskProvider)
creationConfig.artifacts
.setInitialProvider(
taskProvider,
ManagedDeviceTestTask::getCoverageDirectory
)
.atLocation(coverageOutputDir.absolutePath)
.on(InternalArtifactType.MANAGED_DEVICE_CODE_COVERAGE)
val isAdditionalAndroidTestOutputEnabled = creationConfig
.services
.projectOptions[BooleanOption.ENABLE_ADDITIONAL_ANDROID_TEST_OUTPUT]
if (isAdditionalAndroidTestOutputEnabled) {
creationConfig
.artifacts
.setInitialProvider(
taskProvider,
ManagedDeviceTestTask::getAdditionalTestOutputDir)
.atLocation(additionalTestOutputDir.absolutePath)
.on(InternalArtifactType.MANAGED_DEVICE_ANDROID_TEST_ADDITIONAL_OUTPUT)
}
}
override fun configure(task: ManagedDeviceTestTask) {
super.configure(task)
val projectOptions = creationConfig.services.projectOptions
val testedConfig = (creationConfig as? DeviceTestCreationConfig)?.mainVariant
val variantName = testedConfig?.name ?: creationConfig.name
task.description = "Installs and runs the test for $variantName " +
" on the managed device ${device.name}"
task.group = JavaBasePlugin.VERIFICATION_GROUP
task.testAction.setDisallowChanges(testRunTaskAction)
task.deviceInput.setDisallowChanges(
task.objectFactory.newInstance(testRunConfigAction).configureTaskInput(device))
task.testData.setDisallowChanges(testData)
task.deviceDslName.setDisallowChanges(device.name)
if (setupResultDir != null) {
task.setupResult.setDisallowChanges(setupResultDir)
}
task.dependencies =
creationConfig
.variantDependencies
.getArtifactCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.EXTERNAL,
AndroidArtifacts.ArtifactType.CLASSES_JAR)
task.getAdditionalTestOutputEnabled()
.set(projectOptions[BooleanOption.ENABLE_ADDITIONAL_ANDROID_TEST_OUTPUT])
task.resultsDir.set(testResultOutputDir)
task.resultsDir.disallowChanges()
task.getReportsDir().set(testReportOutputDir)
task.getReportsDir().disallowChanges()
task.project.configurations
.findByName(SdkConstants.GRADLE_ANDROID_TEST_UTIL_CONFIGURATION)?.let {
task.buddyApks.from(it)
}
task.classes.from(
creationConfig.artifacts
.forScope(ScopedArtifacts.Scope.PROJECT)
.getFinalArtifacts(ScopedArtifact.CLASSES)
)
task.classes.disallowChanges()
creationConfig.buildConfigCreationConfig?.let {
task.buildConfigClasses.from(it.compiledBuildConfig)
}
task.buildConfigClasses.disallowChanges()
creationConfig.androidResourcesCreationConfig?.let {
task.rClasses.from(
it.getCompiledRClasses(AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH)
)
}
task.rClasses.disallowChanges()
task.executionEnum.setDisallowChanges(creationConfig.global.testOptionExecutionEnum)
}
}
}