1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cts.tradefed.testtype; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.IShellEnabledDevice; 21 import com.android.ddmlib.Log; 22 import com.android.ddmlib.ShellCommandUnresponsiveException; 23 import com.android.ddmlib.TimeoutException; 24 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 25 import com.android.ddmlib.testrunner.ITestRunListener; 26 import com.android.ddmlib.testrunner.InstrumentationResultParser; 27 28 import java.io.IOException; 29 import java.util.Arrays; 30 import java.util.Collection; 31 import java.util.Hashtable; 32 import java.util.Map; 33 import java.util.Map.Entry; 34 import java.util.concurrent.TimeUnit; 35 36 public class PrintTestRemoteTestRunner implements IRemoteAndroidTestRunner { 37 38 private final String mPackageName; 39 private final String mRunnerName; 40 private IShellEnabledDevice mRemoteDevice; 41 // default to no timeout 42 private long mMaxTimeToOutputResponse = 0; 43 private TimeUnit mMaxTimeUnits = TimeUnit.MILLISECONDS; 44 private String mRunName = null; 45 46 /** map of name-value instrumentation argument pairs */ 47 private Map<String, String> mArgMap; 48 private InstrumentationResultParser mParser; 49 50 private static final String LOG_TAG = "RemoteAndroidTest"; 51 private static final String DEFAULT_RUNNER_NAME = "android.test.InstrumentationTestRunner"; 52 53 private static final char CLASS_SEPARATOR = ','; 54 private static final char METHOD_SEPARATOR = '#'; 55 private static final char RUNNER_SEPARATOR = '/'; 56 57 // defined instrumentation argument names 58 private static final String CLASS_ARG_NAME = "class"; 59 private static final String LOG_ARG_NAME = "log"; 60 private static final String DEBUG_ARG_NAME = "debug"; 61 private static final String COVERAGE_ARG_NAME = "coverage"; 62 private static final String PACKAGE_ARG_NAME = "package"; 63 private static final String SIZE_ARG_NAME = "size"; 64 65 // This command starts a shell Java program (installed by this class) 66 // in the folder owned by the shell user. This app creates a proxy 67 // which does privileged operations such as wiping a package's user 68 // data and then starts the tests passing the proxy. This enables 69 // the tests to clear the print spooler data. 70 private static final String INSTRUMENTATION_COMMAND = 71 "chmod 755 /data/local/tmp/print-instrument && " 72 + "/data/local/tmp/print-instrument instrument -w -r %1$s %2$s"; 73 74 /** 75 * Creates a remote Android test runner. 76 * 77 * @param packageName the Android application package that contains the 78 * tests to run 79 * @param runnerName the instrumentation test runner to execute. If null, 80 * will use default runner 81 * @param remoteDevice the Android device to execute tests on 82 */ 83 public PrintTestRemoteTestRunner(String packageName, String runnerName, 84 IShellEnabledDevice remoteDevice) { 85 86 mPackageName = packageName; 87 mRunnerName = runnerName; 88 mRemoteDevice = remoteDevice; 89 mArgMap = new Hashtable<String, String>(); 90 } 91 92 /** 93 * Alternate constructor. Uses default instrumentation runner. 94 * 95 * @param packageName the Android application package that contains the 96 * tests to run 97 * @param remoteDevice the Android device to execute tests on 98 */ 99 public PrintTestRemoteTestRunner(String packageName, IShellEnabledDevice remoteDevice) { 100 this(packageName, null, remoteDevice); 101 } 102 103 @Override 104 public String getPackageName() { 105 return mPackageName; 106 } 107 108 @Override 109 public String getRunnerName() { 110 if (mRunnerName == null) { 111 return DEFAULT_RUNNER_NAME; 112 } 113 return mRunnerName; 114 } 115 116 /** 117 * Returns the complete instrumentation component path. 118 */ 119 private String getRunnerPath() { 120 return getPackageName() + RUNNER_SEPARATOR + getRunnerName(); 121 } 122 123 @Override 124 public void setClassName(String className) { 125 addInstrumentationArg(CLASS_ARG_NAME, className); 126 } 127 128 @Override 129 public void setClassNames(String[] classNames) { 130 StringBuilder classArgBuilder = new StringBuilder(); 131 132 for (int i = 0; i < classNames.length; i++) { 133 if (i != 0) { 134 classArgBuilder.append(CLASS_SEPARATOR); 135 } 136 classArgBuilder.append(classNames[i]); 137 } 138 setClassName(classArgBuilder.toString()); 139 } 140 141 @Override 142 public void setMethodName(String className, String testName) { 143 setClassName(className + METHOD_SEPARATOR + testName); 144 } 145 146 @Override 147 public void setTestPackageName(String packageName) { 148 addInstrumentationArg(PACKAGE_ARG_NAME, packageName); 149 } 150 151 @Override 152 public void addInstrumentationArg(String name, String value) { 153 if (name == null || value == null) { 154 throw new IllegalArgumentException("name or value arguments cannot be null"); 155 } 156 mArgMap.put(name, value); 157 } 158 159 @Override 160 public void removeInstrumentationArg(String name) { 161 if (name == null) { 162 throw new IllegalArgumentException("name argument cannot be null"); 163 } 164 mArgMap.remove(name); 165 } 166 167 @Override 168 public void addBooleanArg(String name, boolean value) { 169 addInstrumentationArg(name, Boolean.toString(value)); 170 } 171 172 @Override 173 public void setLogOnly(boolean logOnly) { 174 addBooleanArg(LOG_ARG_NAME, logOnly); 175 } 176 177 @Override 178 public void setDebug(boolean debug) { 179 addBooleanArg(DEBUG_ARG_NAME, debug); 180 } 181 182 @Override 183 public void setCoverage(boolean coverage) { 184 addBooleanArg(COVERAGE_ARG_NAME, coverage); 185 } 186 187 @Override 188 public void setTestSize(TestSize size) { 189 addInstrumentationArg(SIZE_ARG_NAME, ""/*size.getRunnerValue()*/); 190 } 191 192 @Override 193 public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) { 194 setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS); 195 } 196 197 @Override 198 public void setMaxTimeToOutputResponse(long maxTimeToOutputResponse, TimeUnit maxTimeUnits) { 199 mMaxTimeToOutputResponse = maxTimeToOutputResponse; 200 mMaxTimeUnits = maxTimeUnits; 201 } 202 203 @Override 204 public void setRunName(String runName) { 205 mRunName = runName; 206 } 207 208 @Override 209 public void run(ITestRunListener... listeners) throws TimeoutException, 210 AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { 211 run(Arrays.asList(listeners)); 212 } 213 214 @Override 215 public void run(Collection<ITestRunListener> listeners) throws TimeoutException, 216 AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { 217 final String runCaseCommandStr = String.format(INSTRUMENTATION_COMMAND, 218 getArgsCommand(), getRunnerPath()); 219 Log.i(LOG_TAG, 220 String.format("Running %1$s on %2$s", runCaseCommandStr, mRemoteDevice.getName())); 221 String runName = mRunName == null ? mPackageName : mRunName; 222 mParser = new InstrumentationResultParser(runName, listeners); 223 224 try { 225 mRemoteDevice.executeShellCommand(runCaseCommandStr, mParser, mMaxTimeToOutputResponse, 226 mMaxTimeUnits); 227 } catch (IOException e) { 228 Log.w(LOG_TAG, String.format("IOException %1$s when running tests %2$s on %3$s", 229 e.toString(), getPackageName(), mRemoteDevice.getName())); 230 // rely on parser to communicate results to listeners 231 mParser.handleTestRunFailed(e.toString()); 232 throw e; 233 } catch (ShellCommandUnresponsiveException e) { 234 Log.w(LOG_TAG, String.format( 235 "ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s", 236 e.toString(), getPackageName(), mRemoteDevice.getName())); 237 mParser.handleTestRunFailed(String 238 .format("Failed to receive adb shell test output within %1$d ms. " 239 + "Test may have timed out, or adb connection to device became" 240 + "unresponsive", mMaxTimeToOutputResponse)); 241 throw e; 242 } catch (TimeoutException e) { 243 Log.w(LOG_TAG, String.format("TimeoutException when running tests %1$s on %2$s", 244 getPackageName(), mRemoteDevice.getName())); 245 mParser.handleTestRunFailed(e.toString()); 246 throw e; 247 } catch (AdbCommandRejectedException e) { 248 Log.w(LOG_TAG, String.format( 249 "AdbCommandRejectedException %1$s when running tests %2$s on %3$s", 250 e.toString(), getPackageName(), mRemoteDevice.getName())); 251 mParser.handleTestRunFailed(e.toString()); 252 throw e; 253 } 254 } 255 256 @Override 257 public void cancel() { 258 if (mParser != null) { 259 mParser.cancel(); 260 } 261 } 262 263 /** 264 * Returns the full instrumentation command line syntax for the provided 265 * instrumentation arguments. Returns an empty string if no arguments were 266 * specified. 267 */ 268 private String getArgsCommand() { 269 StringBuilder commandBuilder = new StringBuilder(); 270 for (Entry<String, String> argPair : mArgMap.entrySet()) { 271 final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(), 272 argPair.getValue()); 273 commandBuilder.append(argCmd); 274 } 275 return commandBuilder.toString(); 276 } 277 } 278