/* * Copyright (C) 2013 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.builder.internal.testing; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.testing.TestData; import com.android.builder.testing.api.DeviceConnector; import com.android.builder.testing.api.DeviceException; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; import com.android.ddmlib.testrunner.TestIdentifier; import com.android.ddmlib.testrunner.TestRunResult; import com.android.utils.ILogger; import com.google.common.base.Joiner; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; /** * Basic Callable to run tests on a given {@link DeviceConnector} using * {@link RemoteAndroidTestRunner}. * * The boolean return value is true if success. */ public class SimpleTestCallable implements Callable<Boolean> { public static final String FILE_COVERAGE_EC = "coverage.ec"; @NonNull private final String projectName; @NonNull private final DeviceConnector device; @NonNull private final String flavorName; @NonNull private final TestData testData; @NonNull private final File resultsDir; @NonNull private final File coverageDir; @NonNull private final File testApk; @NonNull private final List<File> testedApks; @NonNull private final File adbExec; private final int timeout; @NonNull private final ILogger logger; public SimpleTestCallable( @NonNull DeviceConnector device, @NonNull String projectName, @NonNull String flavorName, @NonNull File testApk, @NonNull List<File> testedApks, @NonNull File adbExec, @NonNull TestData testData, @NonNull File resultsDir, @NonNull File coverageDir, int timeout, @NonNull ILogger logger) { this.projectName = projectName; this.device = device; this.flavorName = flavorName; this.resultsDir = resultsDir; this.coverageDir = coverageDir; this.testApk = testApk; this.testedApks = testedApks; this.testData = testData; this.adbExec = adbExec; this.timeout = timeout; this.logger = logger; } @Override public Boolean call() throws Exception { String deviceName = device.getName(); boolean isInstalled = false; CustomTestRunListener runListener = new CustomTestRunListener( deviceName, projectName, flavorName, logger); runListener.setReportDir(resultsDir); long time = System.currentTimeMillis(); boolean success = false; String coverageFile = "/data/data/" + testData.getTestedApplicationId() + "/" + FILE_COVERAGE_EC; try { device.connect(timeout, logger); if (!testedApks.isEmpty()) { logger.verbose("DeviceConnector '%s': installing %s", deviceName, Joiner.on(',').join(testedApks)); if (testedApks.size() > 1) { List<String> args = new ArrayList<String>(); args.add(adbExec.getAbsolutePath()); args.add("install-multiple"); args.add("-r"); for (File testedApk : testedApks) { args.add(testedApk.getAbsolutePath()); } // for now, do a simple java exec adb ProcessBuilder processBuilder = new ProcessBuilder(args); Process process = processBuilder.start(); //Read out dir output InputStream is = process.getErrorStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String line; while ((line = br.readLine()) != null) { logger.verbose("adb output is :" + line); } //Wait to get exit value try { int exitValue = process.waitFor(); logger.verbose("\n\nExit Value is " + exitValue); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else { device.installPackage(testedApks.get(0), timeout, logger); } } logger.verbose("DeviceConnector '%s': installing %s", deviceName, testApk); device.installPackage(testApk, timeout, logger); isInstalled = true; RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner( testData.getApplicationId(), testData.getInstrumentationRunner(), device); if (testData.isTestCoverageEnabled()) { runner.addInstrumentationArg("coverage", "true"); runner.addInstrumentationArg("coverageFile", coverageFile); } runner.setRunName(deviceName); runner.setMaxtimeToOutputResponse(timeout); runner.run(runListener); TestRunResult testRunResult = runListener.getRunResult(); success = true; // for now throw an exception if no tests. // TODO return a status instead of allow merging of multi-variants/multi-device reports. if (testRunResult.getNumTests() == 0) { CustomTestRunListener fakeRunListener = new CustomTestRunListener( deviceName, projectName, flavorName, logger); fakeRunListener.setReportDir(resultsDir); // create a fake test output Map<String, String> emptyMetrics = Collections.emptyMap(); TestIdentifier fakeTest = new TestIdentifier(device.getClass().getName(), "hasTests"); fakeRunListener.testStarted(fakeTest); fakeRunListener.testFailed(fakeTest , "No tests found."); fakeRunListener.testEnded(fakeTest, emptyMetrics); // end the run to generate the XML file. fakeRunListener.testRunEnded(System.currentTimeMillis() - time, emptyMetrics); return false; } return !testRunResult.hasFailedTests(); } catch (Exception e) { Map<String, String> emptyMetrics = Collections.emptyMap(); // create a fake test output ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintWriter pw = new PrintWriter(baos, true); e.printStackTrace(pw); TestIdentifier fakeTest = new TestIdentifier(device.getClass().getName(), "runTests"); runListener.testStarted(fakeTest); runListener.testFailed(fakeTest , baos.toString()); runListener.testEnded(fakeTest, emptyMetrics); // end the run to generate the XML file. runListener.testRunEnded(System.currentTimeMillis() - time, emptyMetrics); // and throw throw e; } finally { if (isInstalled) { // Get the coverage if needed. if (success && testData.isTestCoverageEnabled()) { String temporaryCoverageCopy = "/data/local/tmp/" + testData.getTestedApplicationId() + "." + FILE_COVERAGE_EC; MultiLineReceiver outputReceiver = new MultiLineReceiver() { @Override public void processNewLines(String[] lines) { for (String line : lines) { logger.info(line); } } @Override public boolean isCancelled() { return false; } }; logger.verbose("DeviceConnector '%s': fetching coverage data from %s", deviceName, coverageFile); device.executeShellCommand("run-as " + testData.getTestedApplicationId() + " cat " + coverageFile + " > " + temporaryCoverageCopy, outputReceiver, 30, TimeUnit.SECONDS); device.pullFile( temporaryCoverageCopy, new File(coverageDir, FILE_COVERAGE_EC).getPath()); device.executeShellCommand("rm " + temporaryCoverageCopy, outputReceiver, 30, TimeUnit.SECONDS); } // uninstall the apps // This should really not be null, because if it was the build // would have broken before. uninstall(testApk, testData.getApplicationId(), deviceName); if (!testedApks.isEmpty()) { for (File testedApk : testedApks) { uninstall(testedApk, testData.getTestedApplicationId(), deviceName); } } } device.disconnect(timeout, logger); } } private void uninstall(@NonNull File apkFile, @Nullable String packageName, @NonNull String deviceName) throws DeviceException { if (packageName != null) { logger.verbose("DeviceConnector '%s': uninstalling %s", deviceName, packageName); device.uninstallPackage(packageName, timeout, logger); } else { logger.verbose("DeviceConnector '%s': unable to uninstall %s: unable to get package name", deviceName, apkFile); } } }