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