/*
* 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);
}
}
}