blob: f648eb97ab43a09865e069d7ab9b6298536af862 [file] [log] [blame]
/*
* 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.tradefed.testtype;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.InstrumentationResultParser;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ArrayUtil;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
/**
* Runs UI Automator test on device and reports results.
*
* UI Automator test is a dedicated test runner for running UI automation tests that
* utilizes UI Automator framework. The test runner on device emulates instrumentation
* test output format so that existing parsing code in ddmlib and TF can be reused.
*
* Essentially, this is a wrapper around this command:
* adb shell uiautomator runtest (jar files) -e class (test classes) ...
*
*/
public class UiAutomatorRunner implements IRemoteAndroidTestRunner {
private static final String CLASS_ARG_NAME = "class";
private static final String DEBUG_ARG_NAME = "debug";
private static final String RUNNER_ARG_NAME = "runner";
private static final char METHOD_SEPARATOR = '#';
private static final char CLASS_SEPARATOR = ',';
private static final String DEFAULT_RUNNER_NAME =
"com.android.uiautomator.testrunner.UiAutomatorTestRunner";
private static final String UIAUTOMATOR_RUNNER_PATH = "/system/bin/uiautomator";
private Map<String, String> mArgsMap;
private String[] mJarPaths;
private String mPackageName;
// default to no timeout
private long mMaxTimeToOutputResponse = 0;
private IDevice mRemoteDevice;
private String mRunName;
private InstrumentationResultParser mParser;
private String mRunnerPath = UIAUTOMATOR_RUNNER_PATH;
private String mRunnerName = DEFAULT_RUNNER_NAME;
private boolean mIgnoreSighup = false;
/**
* Create a UiAutomatorRunner for running UI automation tests
*
* @param remoteDevice the remote device to interact with: run test, collect results etc
* @param jarPaths the paths to jar files where UI Automator test cases are; the paths must be
* absolute or relative to /data/local/tmp/ on device
* @param runnerPath alternative uiautomator runner to use, may be <code>null</code> and default
* will be used in this case
*/
public UiAutomatorRunner(IDevice remoteDevice, String[] jarPaths, String runnerPath) {
mRemoteDevice = remoteDevice;
mJarPaths = jarPaths;
mArgsMap = new LinkedHashMap<String, String>(); // ensure the order that the args are added
if (runnerPath != null) {
mRunnerPath = runnerPath;
}
}
/**
* Returns the package name of last Java class added
*/
@Override
public String getPackageName() {
return mPackageName;
}
/**
* Returns default UiAutomatorTestRunner class name
*/
@Override
public String getRunnerName() {
return mRunnerName;
}
protected String getRunnerPath() {
return mRunnerPath;
}
/**
* Sets the option in the uiautomator to ignore SIGHUP.
* @param value ignore the signal if set to true
*/
public void setIgnoreSighup(boolean value) {
mIgnoreSighup = value;
}
protected String getRunCommand() {
String jarArg = ArrayUtil.join(" ", (Object[])mJarPaths);
String command = String.format("%s runtest %s %s",
getRunnerPath(), jarArg, getArgsCommand());
if (mIgnoreSighup) {
return command + " --nohup";
} else {
return command;
}
}
/**
* Returns the full instrumentation command line syntax for the provided instrumentation
* arguments.
* Returns an empty string if no arguments were specified.
*/
private String getArgsCommand() {
StringBuilder commandBuilder = new StringBuilder();
for (Entry<String, String> argPair : mArgsMap.entrySet()) {
final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(),
argPair.getValue());
commandBuilder.append(argCmd);
}
return commandBuilder.toString();
}
/**
* {@inheritDoc}
*/
@Override
public void setClassName(String className) {
int pos = className.lastIndexOf('.');
// get package name segment
if (pos == -1) {
mPackageName = "(default)";
} else {
mPackageName = className.substring(0, pos);
}
addInstrumentationArg(CLASS_ARG_NAME, className);
}
/**
* {@inheritDoc}
*/
@Override
public void setClassNames(String[] classNames) {
StringBuilder classArgBuilder = new StringBuilder();
for (int i = 0; i < classNames.length; i++) {
if (i != 0) {
classArgBuilder.append(CLASS_SEPARATOR);
}
classArgBuilder.append(classNames[i]);
}
setClassName(classArgBuilder.toString());
}
/**
* {@inheritDoc}
*/
@Override
public void setMethodName(String className, String testName) {
setClassName(className + METHOD_SEPARATOR + testName);
}
/**
* {@inheritDoc}
*/
@Override
public void setTestPackageName(String packageName) {
throw new UnsupportedOperationException("specifying package name is not supported");
}
/**
* {@inheritDoc}
*/
@Override
public void setTestSize(TestSize size) {
throw new UnsupportedOperationException("specifying test size is not supported");
}
/**
* {@inheritDoc}
*/
@Override
public void addInstrumentationArg(String name, String value) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
if (RUNNER_ARG_NAME.equals(name)) {
mRunnerName = name;
}
mArgsMap.put(name, value);
}
/**
* {@inheritDoc}
*/
@Override
public void removeInstrumentationArg(String name) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
mArgsMap.remove(name);
}
/**
* {@inheritDoc}
*/
@Override
public void addBooleanArg(String name, boolean value) {
addInstrumentationArg(name, Boolean.toString(value));
}
/**
* {@inheritDoc}
*/
@Override
public void setLogOnly(boolean logOnly) {
//TODO: we need to support log only for Eclipse and re-run support
throw new UnsupportedOperationException("log only mode is not supported");
}
/**
* {@inheritDoc}
*/
@Override
public void setDebug(boolean debug) {
addBooleanArg(DEBUG_ARG_NAME, debug);
}
/**
* {@inheritDoc}
*/
@Override
public void setCoverage(boolean coverage) {
// TODO potentially it's possible to run with coverage, need more investigation
throw new UnsupportedOperationException("coverage mode is not supported");
}
@Override
public void setTestCollection(boolean b) {
throw new UnsupportedOperationException("Test Collection mode is not supported");
}
/**
* {@inheritDoc}
*/
@Deprecated
@Override
public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) {
setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS);
}
/**
* {@inheritDoc}
*/
@Override
public void setMaxTimeToOutputResponse(long timeout, TimeUnit unit) {
mMaxTimeToOutputResponse = unit.toMillis(timeout);
}
/**
* {@inheritDoc}
*/
@Override
public void setRunName(String runName) {
mRunName = runName;
}
/**
* {@inheritDoc}
*/
@Override
public void run(ITestRunListener... listeners) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
run(Arrays.asList(listeners));
}
/**
* {@inheritDoc}
*/
@Override
public void run(Collection<ITestRunListener> listeners) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
String cmdLine = getRunCommand();
CLog.i("Running %s on %s", cmdLine, mRemoteDevice.getSerialNumber());
String runName = mRunName == null ? mPackageName : mRunName;
mParser = new InstrumentationResultParser(runName, listeners);
try {
mRemoteDevice.executeShellCommand(cmdLine,
mParser, mMaxTimeToOutputResponse, TimeUnit.MILLISECONDS);
} catch (IOException e) {
CLog.w(String.format("IOException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()));
// rely on parser to communicate results to listeners
mParser.handleTestRunFailed(e.toString());
throw e;
} catch (ShellCommandUnresponsiveException e) {
CLog.w("ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getSerialNumber());
mParser.handleTestRunFailed(String.format(
"Failed to receive adb shell test output within %1$d ms. " +
"Test may have timed out, or adb connection to device became unresponsive",
mMaxTimeToOutputResponse));
throw e;
} catch (TimeoutException e) {
CLog.w("TimeoutException when running tests %1$s on %2$s", getPackageName(),
mRemoteDevice.getSerialNumber());
mParser.handleTestRunFailed(e.toString());
throw e;
} catch (AdbCommandRejectedException e) {
CLog.w("AdbCommandRejectedException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getSerialNumber());
mParser.handleTestRunFailed(e.toString());
throw e;
}
}
/**
* {@inheritDoc}
*/
@Override
public void cancel() {
if (mParser != null) {
mParser.cancel();
}
}
}