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.FileListingService; 20 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 21 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 22 import com.android.tradefed.config.Option; 23 import com.android.tradefed.device.DeviceNotAvailableException; 24 import com.android.tradefed.device.IFileEntry; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 28 import com.android.tradefed.result.FileInputStreamSource; 29 import com.android.tradefed.result.ITestInvocationListener; 30 import com.android.tradefed.result.ITestLifeCycleReceiver; 31 import com.android.tradefed.result.InputStreamSource; 32 import com.android.tradefed.result.LogDataType; 33 import com.android.tradefed.result.TestDescription; 34 import com.android.tradefed.util.FileUtil; 35 import com.android.tradefed.util.IRunUtil; 36 import com.android.tradefed.util.RunUtil; 37 import com.android.tradefed.util.StreamUtil; 38 import com.android.tradefed.util.ZipUtil; 39 40 import org.junit.Assert; 41 42 import java.io.File; 43 import java.io.IOException; 44 import java.util.ArrayList; 45 import java.util.Collection; 46 import java.util.HashMap; 47 import java.util.LinkedHashMap; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.Set; 51 import java.util.concurrent.TimeUnit; 52 53 public class UiAutomatorTest implements IRemoteTest, IDeviceTest, ITestFilterReceiver { 54 55 public enum LoggingOption { 56 AFTER_TEST, 57 AFTER_FAILURE, 58 OFF, 59 } 60 61 public enum TestFailureAction { 62 BUGREPORT, 63 SCREENSHOT, 64 BUGREPORT_AND_SCREENSHOT, 65 } 66 67 private static final String SHELL_EXE_BASE = "/data/local/tmp/"; 68 private static final String TRACE_ITERATIONS = "traceIterations"; 69 private static final String TRACE_DEST_DIRECTORY = "destDirectory"; 70 71 private ITestDevice mDevice = null; 72 private IRemoteAndroidTestRunner mRunner = null; 73 protected Collection<ITestLifeCycleReceiver> mListeners = new ArrayList<>(); 74 75 @Option(name = "jar-path", description = "path to jars containing UI Automator test cases and" 76 + " dependencies; May be repeated. " + 77 "If unspecified will use all jars found in /data/local/tmp/") 78 private List<String> mJarPaths = new ArrayList<String>(); 79 80 @Option(name = "class", 81 description = "test class to run, may be repeated; multiple classess will be run" 82 + " in the same order as provided in command line") 83 private List<String> mClasses = new ArrayList<String>(); 84 85 @Option(name = "sync-time", description = "time to allow for initial sync, in ms") 86 private long mSyncTime = 0; 87 88 @Option(name = "run-arg", 89 description = "Additional test specific arguments to provide.") 90 private Map<String, String> mArgMap = new LinkedHashMap<String, String>(); 91 92 @Option(name = "timeout", 93 description = "Aborts the test run if any test takes longer than the specified " 94 + "timeout. For no timeout, set to 0.", isTimeVal = true) 95 private long mTestTimeout = 30 * 60 * 1000; // default to 30 minutes 96 97 @Option(name = "capture-logs", description = 98 "capture bugreport and screenshot as specified.") 99 private LoggingOption mLoggingOption = LoggingOption.AFTER_FAILURE; 100 101 @Option(name = "runner-path", description = "path to uiautomator runner; may be null and " 102 + "default will be used in this case") 103 private String mRunnerPath = null; 104 105 @Option(name = "on-test-failure", 106 description = "sets the action to perform if a test fails") 107 private TestFailureAction mFailureAction = TestFailureAction.BUGREPORT_AND_SCREENSHOT; 108 109 @Option(name = "ignore-sighup", 110 description = "allows uiautomator test to ignore SIGHUP signal") 111 private boolean mIgnoreSighup = false; 112 113 @Option(name = "run-name", 114 description = "the run name to use when reporting test results.") 115 private String mRunName = "uiautomator"; 116 117 @Option(name = "instrumentation", 118 description = "the specified test should be driven with instrumentation." 119 + "jar-path, runner-path, ignore-sighup are ignored when this is set.") 120 private boolean mInstrumentation = false; 121 122 @Option(name = "package", 123 description = "The manifest package name of the UI test package." 124 + "Only applies when 'instrumentation' option is set.") 125 private String mPackage = null; 126 127 @Option(name = "runner", 128 description="The instrumentation based test runner class name to use." 129 + "Only applies when 'instrumentation' option is set.") 130 private String mRunnerName = 131 "android.support.test.uiautomator.UiAutomatorInstrumentationTestRunner"; 132 133 134 /** 135 * {@inheritDoc} 136 */ 137 @Override setDevice(ITestDevice device)138 public void setDevice(ITestDevice device) { 139 mDevice = device; 140 } 141 142 /** 143 * {@inheritDoc} 144 */ 145 @Override getDevice()146 public ITestDevice getDevice() { 147 return mDevice; 148 } 149 setLoggingOption(LoggingOption loggingOption)150 public void setLoggingOption(LoggingOption loggingOption) { 151 mLoggingOption = loggingOption; 152 } 153 154 /** 155 * @deprecated use {@link #setLoggingOption(LoggingOption)} instead. 156 * <p/> 157 * Retained for compatibility with cts-tradefed 158 */ 159 @Deprecated setCaptureLogs(boolean captureLogs)160 public void setCaptureLogs(boolean captureLogs) { 161 if (captureLogs) { 162 setLoggingOption(LoggingOption.AFTER_FAILURE); 163 } else { 164 setLoggingOption(LoggingOption.OFF); 165 } 166 } 167 setRunName(String runName)168 public void setRunName(String runName) { 169 mRunName = runName; 170 } 171 172 /** 173 * {@inheritDoc} 174 */ 175 @Override run(ITestInvocationListener listener)176 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 177 mListeners.add(listener); 178 if (!isInstrumentationTest()) { 179 buildJarPaths(); 180 } 181 mRunner = createTestRunner(); 182 if (!mClasses.isEmpty()) { 183 getTestRunner().setClassNames(mClasses.toArray(new String[]{})); 184 } 185 getTestRunner().setRunName(mRunName); 186 preTestSetup(); 187 getRunUtil().sleep(getSyncTime()); 188 getTestRunner().setMaxTimeToOutputResponse(mTestTimeout, TimeUnit.MILLISECONDS); 189 for (Map.Entry<String, String> entry : getTestRunArgMap().entrySet()) { 190 getTestRunner().addInstrumentationArg(entry.getKey(), entry.getValue()); 191 } 192 if (!isInstrumentationTest()) { 193 ((UiAutomatorRunner)getTestRunner()).setIgnoreSighup(mIgnoreSighup); 194 } 195 if (mLoggingOption != LoggingOption.OFF) { 196 mListeners.add(new LoggingWrapper(listener)); 197 getDevice().runInstrumentationTests(getTestRunner(), mListeners); 198 } else { 199 getDevice().runInstrumentationTests(getTestRunner(), mListeners); 200 } 201 202 if (getTestRunArgMap().containsKey(TRACE_ITERATIONS) && 203 getTestRunArgMap().containsKey(TRACE_DEST_DIRECTORY)) { 204 try { 205 logTraceFiles(listener, getTestRunArgMap().get(TRACE_DEST_DIRECTORY)); 206 } catch (IOException e) { 207 CLog.e(e); 208 } 209 } 210 211 } 212 createTestRunner()213 protected IRemoteAndroidTestRunner createTestRunner() { 214 if (isInstrumentationTest()) { 215 if (mPackage == null) { 216 throw new IllegalArgumentException("package name has not been set"); 217 } 218 IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mPackage, mRunnerName, 219 getDevice().getIDevice()); 220 return runner; 221 } else { 222 return new UiAutomatorRunner(getDevice().getIDevice(), 223 getTestJarPaths().toArray(new String[]{}), mRunnerPath); 224 } 225 } 226 buildJarPaths()227 private void buildJarPaths() throws DeviceNotAvailableException { 228 if (mJarPaths.isEmpty()) { 229 String rawFileString = 230 getDevice().executeShellCommand(String.format("ls %s", SHELL_EXE_BASE)); 231 String[] rawFiles = rawFileString.split("\r?\n"); 232 for (String rawFile : rawFiles) { 233 if (rawFile.endsWith(".jar")) { 234 mJarPaths.add(rawFile); 235 } 236 } 237 Assert.assertFalse(String.format("could not find jars in %s", SHELL_EXE_BASE), 238 mJarPaths.isEmpty()); 239 CLog.d("built jar paths %s", mJarPaths); 240 } 241 } 242 243 /** 244 * Add an argument to provide when running the UI Automator tests 245 * 246 * @param key the argument name 247 * @param value the argument value 248 */ addRunArg(String key, String value)249 public void addRunArg(String key, String value) { 250 getTestRunArgMap().put(key, value); 251 } 252 253 /** 254 * Checks if the UI Automator components are present on device 255 * 256 * @throws DeviceNotAvailableException 257 */ preTestSetup()258 protected void preTestSetup() throws DeviceNotAvailableException { 259 if (!isInstrumentationTest()) { 260 String runnerPath = ((UiAutomatorRunner)getTestRunner()).getRunnerPath(); 261 if (!getDevice().doesFileExist(runnerPath)) { 262 throw new RuntimeException("Missing UI Automator runner: " + runnerPath); 263 } 264 for (String jarPath : getTestJarPaths()) { 265 if (!jarPath.startsWith(FileListingService.FILE_SEPARATOR)) { 266 jarPath = SHELL_EXE_BASE + jarPath; 267 } 268 if (!getDevice().doesFileExist(jarPath)) { 269 throw new RuntimeException("Missing UI Automator test jar on device: " 270 + jarPath); 271 } 272 } 273 } 274 } 275 onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, String prefix)276 protected void onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, 277 String prefix) { 278 onScreenshotAndBugreport(device, listener, prefix, null); 279 } 280 onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, String prefix, TestFailureAction overrideAction)281 protected void onScreenshotAndBugreport(ITestDevice device, ITestInvocationListener listener, 282 String prefix, TestFailureAction overrideAction) { 283 if (overrideAction == null) { 284 overrideAction = mFailureAction; 285 } 286 // get screen shot 287 if (overrideAction == TestFailureAction.SCREENSHOT || 288 overrideAction == TestFailureAction.BUGREPORT_AND_SCREENSHOT) { 289 InputStreamSource screenshot = null; 290 try { 291 screenshot = device.getScreenshot(); 292 listener.testLog(prefix + "_screenshot", LogDataType.PNG, screenshot); 293 } catch (DeviceNotAvailableException e) { 294 CLog.e(e); 295 } finally { 296 StreamUtil.cancel(screenshot); 297 } 298 } 299 // get bugreport 300 if (overrideAction == TestFailureAction.BUGREPORT || 301 overrideAction == TestFailureAction.BUGREPORT_AND_SCREENSHOT) { 302 InputStreamSource data = null; 303 data = device.getBugreport(); 304 listener.testLog(prefix + "_bugreport", LogDataType.BUGREPORT, data); 305 StreamUtil.cancel(data); 306 } 307 } 308 309 310 /** 311 * Pull the atrace files if they exist under traceSrcDirectory and log it 312 * @param listener test result listener 313 * @param traceSrcDirectory source directory in the device where the trace files 314 * are copied to the local tmp directory 315 * @throws DeviceNotAvailableException 316 * @throws IOException 317 */ logTraceFiles(ITestInvocationListener listener, String traceSrcDirectory)318 private void logTraceFiles(ITestInvocationListener listener, String traceSrcDirectory) 319 throws DeviceNotAvailableException, IOException { 320 File tmpDestDir = null; 321 try { 322 tmpDestDir = FileUtil.createTempDir("atrace"); 323 IFileEntry traceSrcDir = mDevice.getFileEntry(traceSrcDirectory); 324 // Trace files are retrieved from traceSrcDirectory/testDirectory in device 325 if (traceSrcDir != null) { 326 for (IFileEntry testDirectory : traceSrcDir.getChildren(false)) { 327 File testTmpDirectory = new File(tmpDestDir, testDirectory.getName()); 328 if (!testTmpDirectory.mkdir()) { 329 throw new IOException("Not able to create the atrace test directory"); 330 } 331 for (IFileEntry traceFile : testDirectory.getChildren(false)) { 332 File pulledFile = new File(testTmpDirectory, traceFile.getName()); 333 if (!mDevice.pullFile(traceFile.getFullPath(), pulledFile)) { 334 throw new IOException( 335 "Not able to pull the trace file from test device"); 336 } 337 } 338 File atraceZip = ZipUtil.createZip(testTmpDirectory); 339 try (FileInputStreamSource streamSource = 340 new FileInputStreamSource(atraceZip)) { 341 listener.testLog(String.format("atrace_%s", testTmpDirectory.getName()), 342 LogDataType.ZIP, streamSource); 343 } finally { 344 if (atraceZip != null) { 345 atraceZip.delete(); 346 } 347 } 348 } 349 } 350 } finally { 351 if (tmpDestDir != null) { 352 FileUtil.recursiveDelete(tmpDestDir); 353 } 354 } 355 } 356 357 /** 358 * Wraps an existing listener, capture some data in case of test failure 359 */ 360 // TODO replace this once we have a generic event triggered reporter like 361 // BugReportCollector 362 private class LoggingWrapper implements ITestInvocationListener { 363 364 ITestInvocationListener mListener; 365 private boolean mLoggedTestFailure = false; 366 private boolean mLoggedTestRunFailure = false; 367 LoggingWrapper(ITestInvocationListener listener)368 public LoggingWrapper(ITestInvocationListener listener) { 369 mListener = listener; 370 } 371 372 @Override testFailed(TestDescription test, String trace)373 public void testFailed(TestDescription test, String trace) { 374 captureFailureLog(test); 375 } 376 377 @Override testAssumptionFailure(TestDescription test, String trace)378 public void testAssumptionFailure(TestDescription test, String trace) { 379 captureFailureLog(test); 380 } 381 captureFailureLog(TestDescription test)382 private void captureFailureLog(TestDescription test) { 383 if (mLoggingOption == LoggingOption.AFTER_FAILURE) { 384 onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_failure", 385 test.getClassName(), test.getTestName())); 386 // set the flag so that we don't log again when test finishes 387 mLoggedTestFailure = true; 388 } 389 } 390 391 @Override testRunFailed(String errorMessage)392 public void testRunFailed(String errorMessage) { 393 if (mLoggingOption == LoggingOption.AFTER_FAILURE) { 394 onScreenshotAndBugreport(getDevice(), mListener, "test_run_failure"); 395 // set the flag so that we don't log again when test run finishes 396 mLoggedTestRunFailure = true; 397 } 398 } 399 400 @Override testEnded(TestDescription test, HashMap<String, Metric> testMetrics)401 public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) { 402 if (!mLoggedTestFailure && mLoggingOption == LoggingOption.AFTER_TEST) { 403 onScreenshotAndBugreport(getDevice(), mListener, String.format("%s_%s_final", 404 test.getClassName(), test.getTestName())); 405 } 406 } 407 408 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)409 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 410 if (!mLoggedTestRunFailure && mLoggingOption == LoggingOption.AFTER_TEST) { 411 onScreenshotAndBugreport(getDevice(), mListener, "test_run_final"); 412 } 413 } 414 } 415 getRunUtil()416 protected IRunUtil getRunUtil() { 417 return RunUtil.getDefault(); 418 } 419 420 /** 421 * @return the time allocated for the tests to sync. 422 */ getSyncTime()423 public long getSyncTime() { 424 return mSyncTime; 425 } 426 427 /** 428 * @param syncTime the time for the tests files to sync. 429 */ setSyncTime(long syncTime)430 public void setSyncTime(long syncTime) { 431 mSyncTime = syncTime; 432 } 433 434 /** 435 * @return the test runner. 436 */ getTestRunner()437 public IRemoteAndroidTestRunner getTestRunner() { 438 return mRunner; 439 } 440 441 /** 442 * @return the test jar path. 443 */ getTestJarPaths()444 public List<String> getTestJarPaths() { 445 return mJarPaths; 446 } 447 448 /** 449 * @param jarPaths the locations of the test jars. 450 */ setTestJarPaths(List<String> jarPaths)451 public void setTestJarPaths(List<String> jarPaths) { 452 mJarPaths = jarPaths; 453 } 454 455 /** 456 * @return the arguments map to pass to the UiAutomatorRunner. 457 */ getTestRunArgMap()458 public Map<String, String> getTestRunArgMap() { 459 return mArgMap; 460 } 461 462 /** 463 * @param runArgMap the arguments to pass to the UiAutomatorRunner. 464 */ setTestRunArgMap(Map<String, String> runArgMap)465 public void setTestRunArgMap(Map<String, String> runArgMap) { 466 mArgMap = runArgMap; 467 } 468 469 /** 470 * Add a test class name to run. 471 */ addClassName(String className)472 public void addClassName(String className) { 473 mClasses.add(className); 474 } 475 476 /** 477 * Add a test class name collection to run. 478 */ addClassNames(Collection<String> classNames)479 public void addClassNames(Collection<String> classNames) { 480 mClasses.addAll(classNames); 481 } 482 isInstrumentationTest()483 public boolean isInstrumentationTest() { 484 return mInstrumentation; 485 } 486 setRunnerName(String runnerName)487 public void setRunnerName(String runnerName) { 488 mRunnerName = runnerName; 489 } 490 491 /** 492 * Gets the list of test class names that the harness is configured to run 493 * @return list of test class names 494 */ getClassNames()495 public List<String> getClassNames() { 496 return mClasses; 497 } 498 499 @Override addIncludeFilter(String filter)500 public void addIncludeFilter(String filter) { 501 mClasses.add(filter); 502 } 503 504 @Override addAllIncludeFilters(Set<String> filters)505 public void addAllIncludeFilters(Set<String> filters) { 506 mClasses.addAll(filters); 507 508 } 509 510 @Override addExcludeFilter(String filter)511 public void addExcludeFilter(String filter) { 512 throw new UnsupportedOperationException("Exclude filter is not supported."); 513 } 514 515 @Override addAllExcludeFilters(Set<String> filters)516 public void addAllExcludeFilters(Set<String> filters) { 517 throw new UnsupportedOperationException("Exclude filters is not supported."); 518 } 519 } 520