1 /* 2 * Copyright (C) 2012 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.tradefed.testtype; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.IDevice; 21 import com.android.ddmlib.ShellCommandUnresponsiveException; 22 import com.android.ddmlib.TimeoutException; 23 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 24 import com.android.ddmlib.testrunner.ITestRunListener; 25 import com.android.ddmlib.testrunner.InstrumentationResultParser; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.util.ArrayUtil; 28 29 import java.io.IOException; 30 import java.util.Arrays; 31 import java.util.Collection; 32 import java.util.LinkedHashMap; 33 import java.util.Map; 34 import java.util.Map.Entry; 35 import java.util.concurrent.TimeUnit; 36 37 /** 38 * Runs UI Automator test on device and reports results. 39 * 40 * UI Automator test is a dedicated test runner for running UI automation tests that 41 * utilizes UI Automator framework. The test runner on device emulates instrumentation 42 * test output format so that existing parsing code in ddmlib and TF can be reused. 43 * 44 * Essentially, this is a wrapper around this command: 45 * adb shell uiautomator runtest (jar files) -e class (test classes) ... 46 * 47 */ 48 public class UiAutomatorRunner implements IRemoteAndroidTestRunner { 49 50 private static final String CLASS_ARG_NAME = "class"; 51 private static final String DEBUG_ARG_NAME = "debug"; 52 private static final String RUNNER_ARG_NAME = "runner"; 53 private static final char METHOD_SEPARATOR = '#'; 54 private static final char CLASS_SEPARATOR = ','; 55 private static final String DEFAULT_RUNNER_NAME = 56 "com.android.uiautomator.testrunner.UiAutomatorTestRunner"; 57 private static final String UIAUTOMATOR_RUNNER_PATH = "/system/bin/uiautomator"; 58 59 private Map<String, String> mArgsMap; 60 private String[] mJarPaths; 61 private String mPackageName; 62 // default to no timeout 63 private long mMaxTimeToOutputResponse = 0; 64 private IDevice mRemoteDevice; 65 private String mRunName; 66 private InstrumentationResultParser mParser; 67 private String mRunnerPath = UIAUTOMATOR_RUNNER_PATH; 68 private String mRunnerName = DEFAULT_RUNNER_NAME; 69 private boolean mIgnoreSighup = false; 70 71 /** 72 * Create a UiAutomatorRunner for running UI automation tests 73 * 74 * @param remoteDevice the remote device to interact with: run test, collect results etc 75 * @param jarPaths the paths to jar files where UI Automator test cases are; the paths must be 76 * absolute or relative to /data/local/tmp/ on device 77 * @param runnerPath alternative uiautomator runner to use, may be <code>null</code> and default 78 * will be used in this case 79 */ 80 public UiAutomatorRunner(IDevice remoteDevice, String[] jarPaths, String runnerPath) { 81 mRemoteDevice = remoteDevice; 82 mJarPaths = jarPaths; 83 mArgsMap = new LinkedHashMap<String, String>(); // ensure the order that the args are added 84 if (runnerPath != null) { 85 mRunnerPath = runnerPath; 86 } 87 } 88 89 /** 90 * Returns the package name of last Java class added 91 */ 92 @Override 93 public String getPackageName() { 94 return mPackageName; 95 } 96 97 /** 98 * Returns default UiAutomatorTestRunner class name 99 */ 100 @Override 101 public String getRunnerName() { 102 return mRunnerName; 103 } 104 105 protected String getRunnerPath() { 106 return mRunnerPath; 107 } 108 109 /** 110 * Sets the option in the uiautomator to ignore SIGHUP. 111 * @param value ignore the signal if set to true 112 */ 113 public void setIgnoreSighup(boolean value) { 114 mIgnoreSighup = value; 115 } 116 117 protected String getRunCommand() { 118 String jarArg = ArrayUtil.join(" ", (Object[])mJarPaths); 119 String command = String.format("%s runtest %s %s", 120 getRunnerPath(), jarArg, getArgsCommand()); 121 if (mIgnoreSighup) { 122 return command + " --nohup"; 123 } else { 124 return command; 125 } 126 } 127 128 /** 129 * Returns the full instrumentation command line syntax for the provided instrumentation 130 * arguments. 131 * Returns an empty string if no arguments were specified. 132 */ 133 private String getArgsCommand() { 134 StringBuilder commandBuilder = new StringBuilder(); 135 for (Entry<String, String> argPair : mArgsMap.entrySet()) { 136 final String argCmd = String.format(" -e %1$s %2$s", argPair.getKey(), 137 argPair.getValue()); 138 commandBuilder.append(argCmd); 139 } 140 return commandBuilder.toString(); 141 } 142 143 /** 144 * {@inheritDoc} 145 */ 146 @Override 147 public void setClassName(String className) { 148 int pos = className.lastIndexOf('.'); 149 // get package name segment 150 if (pos == -1) { 151 mPackageName = "(default)"; 152 } else { 153 mPackageName = className.substring(0, pos); 154 } 155 addInstrumentationArg(CLASS_ARG_NAME, className); 156 } 157 158 /** 159 * {@inheritDoc} 160 */ 161 @Override 162 public void setClassNames(String[] classNames) { 163 StringBuilder classArgBuilder = new StringBuilder(); 164 165 for (int i = 0; i < classNames.length; i++) { 166 if (i != 0) { 167 classArgBuilder.append(CLASS_SEPARATOR); 168 } 169 classArgBuilder.append(classNames[i]); 170 } 171 setClassName(classArgBuilder.toString()); 172 } 173 174 /** 175 * {@inheritDoc} 176 */ 177 @Override 178 public void setMethodName(String className, String testName) { 179 setClassName(className + METHOD_SEPARATOR + testName); 180 } 181 182 /** 183 * {@inheritDoc} 184 */ 185 @Override 186 public void setTestPackageName(String packageName) { 187 throw new UnsupportedOperationException("specifying package name is not supported"); 188 } 189 190 /** 191 * {@inheritDoc} 192 */ 193 @Override 194 public void setTestSize(TestSize size) { 195 throw new UnsupportedOperationException("specifying test size is not supported"); 196 } 197 198 /** 199 * {@inheritDoc} 200 */ 201 @Override 202 public void addInstrumentationArg(String name, String value) { 203 if (name == null) { 204 throw new IllegalArgumentException("name cannot be null"); 205 } 206 if (RUNNER_ARG_NAME.equals(name)) { 207 mRunnerName = name; 208 } 209 mArgsMap.put(name, value); 210 } 211 212 /** 213 * {@inheritDoc} 214 */ 215 @Override 216 public void removeInstrumentationArg(String name) { 217 if (name == null) { 218 throw new IllegalArgumentException("name cannot be null"); 219 } 220 mArgsMap.remove(name); 221 } 222 223 /** 224 * {@inheritDoc} 225 */ 226 @Override 227 public void addBooleanArg(String name, boolean value) { 228 addInstrumentationArg(name, Boolean.toString(value)); 229 } 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override 235 public void setLogOnly(boolean logOnly) { 236 //TODO: we need to support log only for Eclipse and re-run support 237 throw new UnsupportedOperationException("log only mode is not supported"); 238 } 239 240 /** 241 * {@inheritDoc} 242 */ 243 @Override 244 public void setDebug(boolean debug) { 245 addBooleanArg(DEBUG_ARG_NAME, debug); 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override 252 public void setCoverage(boolean coverage) { 253 // TODO potentially it's possible to run with coverage, need more investigation 254 throw new UnsupportedOperationException("coverage mode is not supported"); 255 } 256 257 @Override 258 public void setTestCollection(boolean b) { 259 throw new UnsupportedOperationException("Test Collection mode is not supported"); 260 } 261 262 /** 263 * {@inheritDoc} 264 * @deprecated use {@link #setMaxTimeToOutputResponse(long, TimeUnit)} instead. 265 */ 266 @Deprecated 267 @Override 268 public void setMaxtimeToOutputResponse(int maxTimeToOutputResponse) { 269 setMaxTimeToOutputResponse(maxTimeToOutputResponse, TimeUnit.MILLISECONDS); 270 } 271 272 /** 273 * {@inheritDoc} 274 */ 275 @Override 276 public void setMaxTimeToOutputResponse(long timeout, TimeUnit unit) { 277 mMaxTimeToOutputResponse = unit.toMillis(timeout); 278 } 279 280 /** 281 * {@inheritDoc} 282 */ 283 @Override 284 public void setRunName(String runName) { 285 mRunName = runName; 286 } 287 288 /** 289 * {@inheritDoc} 290 */ 291 @Override 292 public void run(ITestRunListener... listeners) throws TimeoutException, 293 AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { 294 run(Arrays.asList(listeners)); 295 } 296 297 /** 298 * {@inheritDoc} 299 */ 300 @Override 301 public void run(Collection<ITestRunListener> listeners) throws TimeoutException, 302 AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { 303 String cmdLine = getRunCommand(); 304 CLog.i("Running %s on %s", cmdLine, mRemoteDevice.getSerialNumber()); 305 String runName = mRunName == null ? mPackageName : mRunName; 306 mParser = new InstrumentationResultParser(runName, listeners); 307 308 try { 309 mRemoteDevice.executeShellCommand(cmdLine, 310 mParser, mMaxTimeToOutputResponse, TimeUnit.MILLISECONDS); 311 } catch (IOException e) { 312 CLog.w(String.format("IOException %1$s when running tests %2$s on %3$s", 313 e.toString(), getPackageName(), mRemoteDevice.getSerialNumber())); 314 // rely on parser to communicate results to listeners 315 mParser.handleTestRunFailed(e.toString()); 316 throw e; 317 } catch (ShellCommandUnresponsiveException e) { 318 CLog.w("ShellCommandUnresponsiveException %1$s when running tests %2$s on %3$s", 319 e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()); 320 mParser.handleTestRunFailed(String.format( 321 "Failed to receive adb shell test output within %1$d ms. " + 322 "Test may have timed out, or adb connection to device became unresponsive", 323 mMaxTimeToOutputResponse)); 324 throw e; 325 } catch (TimeoutException e) { 326 CLog.w("TimeoutException when running tests %1$s on %2$s", getPackageName(), 327 mRemoteDevice.getSerialNumber()); 328 mParser.handleTestRunFailed(e.toString()); 329 throw e; 330 } catch (AdbCommandRejectedException e) { 331 CLog.w("AdbCommandRejectedException %1$s when running tests %2$s on %3$s", 332 e.toString(), getPackageName(), mRemoteDevice.getSerialNumber()); 333 mParser.handleTestRunFailed(e.toString()); 334 throw e; 335 } 336 } 337 338 /** 339 * {@inheritDoc} 340 */ 341 @Override 342 public void cancel() { 343 if (mParser != null) { 344 mParser.cancel(); 345 } 346 } 347 348 @Override 349 public void setEnforceTimeStamp(boolean arg0) { 350 // ignore, UiAutomator runner does not need this. 351 } 352 } 353