blob: 571515ed9b4d95a18830f9e77ba1b1a25e8aa49e [file] [log] [blame]
/*
* Copyright (C) 2008 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.ddmlib.testrunner;
import com.android.annotations.NonNull;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellEnabledDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
/**
* Runs a Android test command remotely and reports results.
*/
public class RemoteAndroidTestRunner implements IRemoteAndroidTestRunner {
/** Represents a status reporter mode in am instrument command options. */
public enum StatusReporterMode {
/**
* Use raw text message to receive status from am instrument command.
*
* @deprecated Use {@link #PROTO_STD} for API level 26 and above.
*/
@Deprecated
RAW_TEXT("-r", 0, InstrumentationResultParser::new),
/**
* Use instrumentationData protobuf status reporter to receive status from am instrument
* command.
*/
PROTO_STD("-m", 26, InstrumentationProtoResultParser::new);
StatusReporterMode(
String amInstrumentCommandArg,
int minApiLevel,
BiFunction<String, Collection<ITestRunListener>, IInstrumentationResultParser>
parserFactory) {
this.amInstrumentCommandArg = amInstrumentCommandArg;
this.minApiLevel = minApiLevel;
this.parserFactory = parserFactory;
}
private final String amInstrumentCommandArg;
private final int minApiLevel;
private final BiFunction<String, Collection<ITestRunListener>, IInstrumentationResultParser>
parserFactory;
/**
* Returns a command line arg for am instrument command to specify this status reporter
* mode.
*/
public String getAmInstrumentCommandArg() {
return amInstrumentCommandArg;
}
/**
* Returns the minimum Android API level which supports this instrumentation status report
* type.
*/
public int getMinimumApiLevel() {
return minApiLevel;
}
/**
* Create the {@link InstrumentationResultParser} that can be used to parse the
* instrumentation output.
*
* @param runName The name of the run to use.
* @param listeners The listeners where to report the results.
* @return An instance of {@link InstrumentationResultParser}.
*/
public IInstrumentationResultParser createInstrumentationResultParser(
@NonNull String runName, @NonNull Collection<ITestRunListener> listeners) {
return parserFactory.apply(runName, listeners);
}
}
private final StatusReporterMode mStatusReporterMode;
private final String mPackageName;
private final String mRunnerName;
private IShellEnabledDevice mRemoteDevice;
// default to no timeout
private long mMaxTimeoutMs = 0L;
private long mMaxTimeToOutputResponseMs = 0L;
private String mRunName = null;
/** map of name-value instrumentation argument pairs */
private Map<String, String> mArgMap;
private IInstrumentationResultParser mParser;
private static final String LOG_TAG = "RemoteAndroidTest";
private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner";
private static final char CLASS_SEPARATOR = ',';
private static final char METHOD_SEPARATOR = '#';
private static final char RUNNER_SEPARATOR = '/';
// defined instrumentation argument names
private static final String CLASS_ARG_NAME = "class";
private static final String LOG_ARG_NAME = "log";
private static final String DEBUG_ARG_NAME = "debug";
private static final String COVERAGE_ARG_NAME = "coverage";
private static final String PACKAGE_ARG_NAME = "package";
private static final String SIZE_ARG_NAME = "size";
private static final String DELAY_MSEC_ARG_NAME = "delay_msec";
private String mRunOptions = "";
private static final int TEST_COLLECTION_TIMEOUT = 2 * 60 * 1000; //2 min
/**
* Creates a remote Android test runner.
*
* @param packageName the Android application package that contains the tests to run
* @param runnerName the instrumentation test runner to execute. If null, will use default
* runner
* @param remoteDevice the Android device to execute tests on
* @param statusReporterMode the status reporter mode to be used for am instrument command
*/
public RemoteAndroidTestRunner(
String packageName,
String runnerName,
IShellEnabledDevice remoteDevice,
StatusReporterMode statusReporterMode) {
mPackageName = packageName;
mRunnerName = runnerName;
mRemoteDevice = remoteDevice;
mStatusReporterMode = statusReporterMode;
mArgMap = new Hashtable<String, String>();
}
/**
* Alternate constructor. Uses default {@code statusReporterMode}.
*
* @param packageName the Android application package that contains the tests to run
* @param runnerName the instrumentation test runner to execute. If null, will use default
* runner
* @param remoteDevice the Android device to execute tests on
*/
public RemoteAndroidTestRunner(
String packageName, String runnerName, IShellEnabledDevice remoteDevice) {
this(packageName, runnerName, remoteDevice, StatusReporterMode.RAW_TEXT);
}
/**
* Alternate constructor. Uses default instrumentation runner.
*
* @param packageName the Android application package that contains the tests to run
* @param remoteDevice the Android device to execute tests on
*/
public RemoteAndroidTestRunner(String packageName,
IShellEnabledDevice remoteDevice) {
this(packageName, null, remoteDevice);
}
@Override
public String getPackageName() {
return mPackageName;
}
@Override
public String getRunnerName() {
if (mRunnerName == null) {
return DEFAULT_RUNNER_NAME;
}
return mRunnerName;
}
/** Returns the complete instrumentation component path. */
protected String getRunnerPath() {
return getPackageName() + RUNNER_SEPARATOR + getRunnerName();
}
@Override
public void setClassName(String className) {
// The class name may contain the dollar sign, so needs to be quoted.
addInstrumentationArg(CLASS_ARG_NAME, "'" + className + "'");
}
@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());
}
@Override
public void setMethodName(String className, String testName) {
setClassName(className + METHOD_SEPARATOR + testName);
}
@Override
public void setTestPackageName(String packageName) {
addInstrumentationArg(PACKAGE_ARG_NAME, packageName);
}
@Override
public void addInstrumentationArg(String name, String value) {
if (name == null || value == null) {
throw new IllegalArgumentException("name or value arguments cannot be null");
}
mArgMap.put(name, value);
}
@Override
public void removeInstrumentationArg(String name) {
if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
}
mArgMap.remove(name);
}
@Override
public void addBooleanArg(String name, boolean value) {
addInstrumentationArg(name, Boolean.toString(value));
}
@Override
public void setLogOnly(boolean logOnly) {
addBooleanArg(LOG_ARG_NAME, logOnly);
}
@Override
public void setDebug(boolean debug) {
addBooleanArg(DEBUG_ARG_NAME, debug);
}
@Override
public void setAdditionalTestOutputLocation(String additionalTestDataPath) {
addInstrumentationArg("additionalTestOutputDir", additionalTestDataPath);
}
@Override
public void setCoverage(boolean coverage) {
addBooleanArg(COVERAGE_ARG_NAME, coverage);
}
@Override
public void setCoverageReportLocation(String reportPath) {
addInstrumentationArg("coverageFile", reportPath);
}
@Override
public CoverageOutput getCoverageOutputType() {
return CoverageOutput.FILE;
}
@Override
public void setTestSize(TestSize size) {
addInstrumentationArg(SIZE_ARG_NAME, size.getRunnerValue());
}
@Override
public void setTestCollection(boolean collect) {
if (collect) {
// skip test execution
setLogOnly(true);
// force a timeout for test collection
setMaxTimeToOutputResponse(TEST_COLLECTION_TIMEOUT, TimeUnit.MILLISECONDS);
if (getApiLevel() < 16 ) {
// On older platforms, collecting tests can fail for large volume of tests.
// Insert a small delay between each test to prevent this
addInstrumentationArg(DELAY_MSEC_ARG_NAME, "15" /* ms */);
}
} else {
setLogOnly(false);
// restore timeout to its original set value
setMaxTimeToOutputResponse(mMaxTimeToOutputResponseMs, TimeUnit.MILLISECONDS);
if (getApiLevel() < 16 ) {
// remove delay
removeInstrumentationArg(DELAY_MSEC_ARG_NAME);
}
}
}
/**
* Attempts to retrieve the Api level of the Android device
* @return the api level or -1 if the communication with the device wasn't successful
*/
private int getApiLevel() {
try {
return Integer.parseInt(mRemoteDevice.getSystemProperty(
IDevice.PROP_BUILD_API_LEVEL).get());
} catch (Exception e) {
return -1;
}
}
@Deprecated
@Override
public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) {
setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS);
}
@Override
public void setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits) {
mMaxTimeToOutputResponseMs = maxTimeUnits.toMillis(maxTimeToOutputResponse);
}
@Override
public void setMaxTimeout(long maxTimeout, TimeUnit maxTimeUnits) {
mMaxTimeoutMs = maxTimeUnits.toMillis(maxTimeout);
}
@Override
public void setRunName(String runName) {
mRunName = runName;
}
@Override
public void run(ITestRunListener... listeners)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
run(Arrays.asList(listeners));
}
@Override
public void run(Collection<ITestRunListener> listeners)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
final String runCaseCommandStr = getAmInstrumentCommand();
Log.i(LOG_TAG, String.format("Running %1$s on %2$s", runCaseCommandStr,
mRemoteDevice.getName()));
String runName = mRunName == null ? mPackageName : mRunName;
mParser = createParser(runName, listeners);
try {
mRemoteDevice.executeShellCommand(
runCaseCommandStr,
mParser,
mMaxTimeoutMs,
mMaxTimeToOutputResponseMs,
TimeUnit.MILLISECONDS);
} catch (IOException e) {
Log.w(LOG_TAG, String.format("IOException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getName()));
// rely on parser to communicate results to listeners
mParser.handleTestRunFailed(e.toString());
throw e;
} catch (ShellCommandUnresponsiveException e) {
Log.w(LOG_TAG, String.format(
"ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getName()));
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",
mMaxTimeToOutputResponseMs));
throw e;
} catch (TimeoutException e) {
Log.w(LOG_TAG, String.format(
"TimeoutException when running tests %1$s on %2$s", getPackageName(),
mRemoteDevice.getName()));
mParser.handleTestRunFailed(e.toString());
throw e;
} catch (AdbCommandRejectedException e) {
Log.w(LOG_TAG, String.format(
"AdbCommandRejectedException %1$s when running tests %2$s on %3$s",
e.toString(), getPackageName(), mRemoteDevice.getName()));
mParser.handleTestRunFailed(e.toString());
throw e;
}
}
/**
* Create the {@link InstrumentationResultParser} that will be used to parse the instrumentation
* output.
*
* @param runName The name of the run to use.
* @param listeners The listeners where to report the results.
* @return An instance of {@link InstrumentationResultParser}.
*/
@NonNull
public IInstrumentationResultParser createParser(
@NonNull String runName, @NonNull Collection<ITestRunListener> listeners) {
return mStatusReporterMode.createInstrumentationResultParser(runName, listeners);
}
@NonNull
public String getAmInstrumentCommand() {
return String.format(
"am instrument -w %1$s %2$s %3$s %4$s",
mStatusReporterMode.getAmInstrumentCommandArg(),
getRunOptions(),
getArgsCommand(),
getRunnerPath());
}
/**
* Returns options for the am instrument command.
*/
@NonNull public String getRunOptions() {
return mRunOptions;
}
/**
* Sets options for the am instrument command.
* See com/android/commands/am/Am.java for full list of options.
*/
public void setRunOptions(@NonNull String options) {
mRunOptions = options;
}
@Override
public void cancel() {
if (mParser != null) {
mParser.cancel();
}
}
/**
* Returns the full instrumentation command line syntax for the provided instrumentation
* arguments. Returns an empty string if no arguments were specified.
*/
protected String getArgsCommand() {
StringBuilder commandBuilder = new StringBuilder();
for (Entry<String, String> argPair : mArgMap.entrySet()) {
final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(),
argPair.getValue());
commandBuilder.append(argCmd);
}
return commandBuilder.toString();
}
}