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