/* * Copyright (C) 2012 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 static com.android.builder.core.BuilderConstants.CONNECTED; import static com.android.builder.core.BuilderConstants.DEVICE; import static com.android.builder.core.BuilderConstants.FD_ANDROID_RESULTS; import static com.android.builder.core.BuilderConstants.FD_ANDROID_TESTS; import static com.android.builder.core.BuilderConstants.FD_FLAVORS; import static com.android.builder.core.BuilderConstants.FD_REPORTS; import static com.android.builder.model.AndroidProject.FD_OUTPUTS; import static com.android.sdklib.BuildToolInfo.PathId.SPLIT_SELECT; import com.android.build.gradle.internal.scope.ConventionMappingHelper; import com.android.build.gradle.internal.scope.TaskConfigAction; import com.android.build.gradle.internal.scope.VariantScope; import com.android.build.gradle.internal.test.report.ReportType; import com.android.build.gradle.internal.test.report.TestReport; import com.android.build.gradle.internal.variant.TestVariantData; import com.android.builder.internal.testing.SimpleTestCallable; import com.android.builder.sdk.SdkInfo; import com.android.builder.sdk.TargetInfo; import com.android.builder.testing.ConnectedDeviceProvider; import com.android.builder.testing.SimpleTestRunner; import com.android.builder.testing.TestData; import com.android.builder.testing.TestRunner; import com.android.builder.testing.api.DeviceException; import com.android.builder.testing.api.DeviceProvider; import com.android.builder.testing.api.TestException; import com.android.ide.common.process.ProcessExecutor; import com.android.utils.FileUtils; import com.android.utils.StringHelper; import com.google.common.collect.ImmutableList; import org.gradle.api.GradleException; import org.gradle.api.Nullable; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.tasks.TaskAction; import org.gradle.logging.ConsoleRenderer; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.concurrent.Callable; /** * Run instrumentation tests for a given variant */ public class DeviceProviderInstrumentTestTask extends BaseTask implements AndroidTestTask { private File reportsDir; private File resultsDir; private File coverageDir; private String flavorName; @Nullable private Collection<String> installOptions; private DeviceProvider deviceProvider; private TestData testData; private File adbExec; @Nullable private File splitSelectExec; private ProcessExecutor processExecutor; private boolean ignoreFailures; private boolean testFailed; @TaskAction protected void runTests() throws DeviceException, IOException, InterruptedException, TestRunner.NoAuthorizedDeviceFoundException, TestException { File resultsOutDir = getResultsDir(); FileUtils.emptyFolder(resultsOutDir); File coverageOutDir = getCoverageDir(); FileUtils.emptyFolder(coverageOutDir); boolean success = false; // If there are tests to run, and the test runner returns with no results, we fail (since // this is most likely a problem with the device setup). If no, the task will succeed. if (!testsFound()) { getLogger().info("No tests found, nothing to do."); // If we don't create the coverage file, createXxxCoverageReport task will fail. File emptyCoverageFile = new File(coverageOutDir, SimpleTestCallable.FILE_COVERAGE_EC); emptyCoverageFile.createNewFile(); success = true; } else { File testApk = testData.getTestApk(); String flavor = getFlavorName(); TestRunner testRunner = new SimpleTestRunner( getSplitSelectExec(), getProcessExecutor()); deviceProvider.init(); Collection<String> extraArgs = installOptions == null || installOptions.isEmpty() ? ImmutableList.<String>of() : installOptions; try { success = testRunner.runTests(getProject().getName(), flavor, testApk, testData, deviceProvider.getDevices(), deviceProvider.getMaxThreads(), deviceProvider.getTimeoutInMs(), extraArgs, resultsOutDir, coverageOutDir, getILogger()); } finally { deviceProvider.terminate(); } } // run the report from the results. File reportOutDir = getReportsDir(); FileUtils.emptyFolder(reportOutDir); TestReport report = new TestReport(ReportType.SINGLE_FLAVOR, resultsOutDir, reportOutDir); report.generateReport(); if (!success) { testFailed = true; String reportUrl = new ConsoleRenderer().asClickableFileUrl( new File(reportOutDir, "index.html")); String message = "There were failing tests. See the report at: " + reportUrl; if (getIgnoreFailures()) { getLogger().warn(message); return; } else { throw new GradleException(message); } } testFailed = false; } /** * Determines if there are any tests to run. * * @return true if there are some tests to run, false otherwise */ private boolean testsFound() { // For now we check if there are any test sources. We could inspect the test classes and // apply JUnit logic to see if there's something to run, but that would not catch the case // where user makes a typo in a test name or forgets to inherit from a JUnit class return !getProject().files(testData.getTestDirectories()).getAsFileTree().isEmpty(); } public File getReportsDir() { return reportsDir; } public void setReportsDir(File reportsDir) { this.reportsDir = reportsDir; } @Override public File getResultsDir() { return resultsDir; } public void setResultsDir(File resultsDir) { this.resultsDir = resultsDir; } public File getCoverageDir() { return coverageDir; } public void setCoverageDir(File coverageDir) { this.coverageDir = coverageDir; } public String getFlavorName() { return flavorName; } public void setFlavorName(String flavorName) { this.flavorName = flavorName; } public Collection<String> getInstallOptions() { return installOptions; } public void setInstallOptions(Collection<String> installOptions) { this.installOptions = installOptions; } public DeviceProvider getDeviceProvider() { return deviceProvider; } public void setDeviceProvider(DeviceProvider deviceProvider) { this.deviceProvider = deviceProvider; } public TestData getTestData() { return testData; } public void setTestData(TestData testData) { this.testData = testData; } public File getAdbExec() { return adbExec; } public void setAdbExec(File adbExec) { this.adbExec = adbExec; } public File getSplitSelectExec() { return splitSelectExec; } public void setSplitSelectExec(File splitSelectExec) { this.splitSelectExec = splitSelectExec; } public ProcessExecutor getProcessExecutor() { return processExecutor; } public void setProcessExecutor(ProcessExecutor processExecutor) { this.processExecutor = processExecutor; } @Override public boolean getIgnoreFailures() { return ignoreFailures; } @Override public void setIgnoreFailures(boolean ignoreFailures) { this.ignoreFailures = ignoreFailures; } @Override public boolean getTestFailed() { return testFailed; } public static class ConfigAction implements TaskConfigAction<DeviceProviderInstrumentTestTask> { private final VariantScope scope; private final DeviceProvider deviceProvider; private final TestData testData; public ConfigAction(VariantScope scope, DeviceProvider deviceProvider, TestData testData) { this.scope = scope; this.deviceProvider = deviceProvider; this.testData = testData; } @Override public String getName() { return scope.getTaskName(deviceProvider.getName()); } @Override public Class<DeviceProviderInstrumentTestTask> getType() { return DeviceProviderInstrumentTestTask.class; } @Override public void execute(DeviceProviderInstrumentTestTask task) { final boolean connected = deviceProvider instanceof ConnectedDeviceProvider; String variantName = scope.getTestedVariantData() != null ? scope.getTestedVariantData().getName() : scope.getVariantData().getName(); if (connected) { task.setDescription("Installs and runs the tests for " + variantName + " on connected devices."); } else { task.setDescription("Installs and runs the tests for " + variantName + " using provider: " + StringHelper.capitalize(deviceProvider.getName())); } task.setGroup(JavaBasePlugin.VERIFICATION_GROUP); task.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder()); task.setVariantName(variantName); task.setTestData(testData); task.setFlavorName(testData.getFlavorName()); task.setDeviceProvider(deviceProvider); task.setInstallOptions(scope.getGlobalScope().getExtension().getAdbOptions().getInstallOptions()); task.setProcessExecutor(scope.getGlobalScope().getAndroidBuilder().getProcessExecutor()); String flavorFolder = testData.getFlavorName(); if (!flavorFolder.isEmpty()) { flavorFolder = FD_FLAVORS + "/" + flavorFolder; } String providerFolder = connected ? CONNECTED : DEVICE + "/" + deviceProvider.getName(); final String subFolder = "/" + providerFolder + "/" + flavorFolder; ConventionMappingHelper.map(task, "adbExec", new Callable<File>() { @Override public File call() { final SdkInfo info = scope.getGlobalScope().getSdkHandler() .getSdkInfo(); return (info == null ? null : info.getAdb()); } }); ConventionMappingHelper.map(task, "splitSelectExec", new Callable<File>() { @Override public File call() throws Exception { final TargetInfo info = scope.getGlobalScope().getAndroidBuilder().getTargetInfo(); String path = info == null ? null : info.getBuildTools().getPath(SPLIT_SELECT); if (path != null) { File splitSelectExe = new File(path); return splitSelectExe.exists() ? splitSelectExe : null; } else { return null; } } }); ConventionMappingHelper.map(task, "resultsDir", new Callable<File>() { @Override public File call() { String rootLocation = scope.getGlobalScope().getExtension().getTestOptions().getResultsDir(); if (rootLocation == null) { rootLocation = scope.getGlobalScope().getBuildDir() + "/" + FD_OUTPUTS + "/" + FD_ANDROID_RESULTS; } return scope.getGlobalScope().getProject().file(rootLocation + subFolder); } }); ConventionMappingHelper.map(task, "reportsDir", new Callable<File>() { @Override public File call() { String rootLocation = scope.getGlobalScope().getExtension().getTestOptions().getReportDir(); if (rootLocation == null) { rootLocation = scope.getGlobalScope().getBuildDir() + "/" + FD_REPORTS + "/" + FD_ANDROID_TESTS; } return scope.getGlobalScope().getProject().file(rootLocation + subFolder); } }); String rootLocation = scope.getGlobalScope().getBuildDir() + "/" + FD_OUTPUTS + "/code-coverage"; task.setCoverageDir(scope.getGlobalScope().getProject().file(rootLocation + subFolder)); if (scope.getVariantData() instanceof TestVariantData) { TestVariantData testVariantData = (TestVariantData) scope.getVariantData(); if (connected) { testVariantData.connectedTestTask = task; } else { testVariantData.providerTestTaskList.add(task); } } task.setEnabled(deviceProvider.isConfigured()); } } }