1 /* 2 * Copyright (C) 2021 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.csuite.core; 18 19 import com.android.csuite.core.ApkInstaller.ApkInstallerException; 20 import com.android.csuite.core.DeviceUtils.DeviceTimestamp; 21 import com.android.csuite.core.DeviceUtils.DropboxEntry; 22 import com.android.csuite.core.TestUtils.RoboscriptSignal; 23 import com.android.csuite.core.TestUtils.TestUtilsException; 24 import com.android.tradefed.config.IConfiguration; 25 import com.android.tradefed.device.DeviceNotAvailableException; 26 import com.android.tradefed.invoker.TestInformation; 27 import com.android.tradefed.log.LogUtil.CLog; 28 import com.android.tradefed.result.LogDataType; 29 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; 30 import com.android.tradefed.util.CommandResult; 31 import com.android.tradefed.util.CommandStatus; 32 import com.android.tradefed.util.IRunUtil; 33 import com.android.tradefed.util.RunUtil; 34 import com.android.tradefed.util.ZipUtil; 35 36 import com.google.common.annotations.VisibleForTesting; 37 import com.google.common.base.Preconditions; 38 import com.google.common.io.MoreFiles; 39 40 import org.junit.Assert; 41 42 import java.io.File; 43 import java.io.IOException; 44 import java.nio.charset.Charset; 45 import java.nio.file.FileSystem; 46 import java.nio.file.FileSystems; 47 import java.nio.file.Files; 48 import java.nio.file.Path; 49 import java.util.ArrayList; 50 import java.util.Arrays; 51 import java.util.List; 52 import java.util.Optional; 53 import java.util.concurrent.atomic.AtomicReference; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 import java.util.stream.Collectors; 57 import java.util.stream.Stream; 58 59 60 /** A tester that interact with an app crawler during testing. */ 61 public final class AppCrawlTester { 62 @VisibleForTesting Path mOutput; 63 private final RunUtilProvider mRunUtilProvider; 64 private final TestUtils mTestUtils; 65 private final String mPackageName; 66 private FileSystem mFileSystem; 67 private DeviceTimestamp mScreenRecordStartTime; 68 private IConfiguration mConfiguration; 69 private ApkInstaller mApkInstaller; 70 private ExecutionStage mExecutionStage = new ExecutionStage(); 71 72 /** 73 * Creates an {@link AppCrawlTester} instance. 74 * 75 * @param packageName The package name of the apk files. 76 * @param testInformation The TradeFed test information. 77 * @param testLogData The TradeFed test output receiver. 78 * @return an {@link AppCrawlTester} instance. 79 */ newInstance( String packageName, TestInformation testInformation, TestLogData testLogData, IConfiguration configuration)80 public static AppCrawlTester newInstance( 81 String packageName, 82 TestInformation testInformation, 83 TestLogData testLogData, 84 IConfiguration configuration) { 85 return new AppCrawlTester( 86 packageName, 87 TestUtils.getInstance(testInformation, testLogData), 88 () -> new RunUtil(), 89 FileSystems.getDefault(), 90 configuration); 91 } 92 93 @VisibleForTesting AppCrawlTester( String packageName, TestUtils testUtils, RunUtilProvider runUtilProvider, FileSystem fileSystem, IConfiguration configuration)94 AppCrawlTester( 95 String packageName, 96 TestUtils testUtils, 97 RunUtilProvider runUtilProvider, 98 FileSystem fileSystem, 99 IConfiguration configuration) { 100 mRunUtilProvider = runUtilProvider; 101 mPackageName = packageName; 102 mTestUtils = testUtils; 103 mFileSystem = fileSystem; 104 mConfiguration = configuration; 105 } 106 107 /** Returns the options object for the app crawl tester */ getOptions()108 public AppCrawlTesterOptions getOptions() { 109 List<?> configurations = 110 mConfiguration.getConfigurationObjectList(AppCrawlTesterOptions.OBJECT_TYPE); 111 Preconditions.checkNotNull( 112 configurations, 113 "Expecting a " 114 + ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE 115 + " in the module configuration."); 116 Preconditions.checkArgument( 117 configurations.size() == 1, 118 "Expecting exactly 1 instance of " 119 + ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE 120 + " in the module configuration."); 121 return (AppCrawlTesterOptions) configurations.get(0); 122 } 123 124 /** An exception class representing crawler test failures. */ 125 public static final class CrawlerException extends Exception { 126 /** 127 * Constructs a new {@link CrawlerException} with a meaningful error message. 128 * 129 * @param message A error message describing the cause of the error. 130 */ CrawlerException(String message)131 private CrawlerException(String message) { 132 super(message); 133 } 134 135 /** 136 * Constructs a new {@link CrawlerException} with a meaningful error message, and a cause. 137 * 138 * @param message A detailed error message. 139 * @param cause A {@link Throwable} capturing the original cause of the CrawlerException. 140 */ CrawlerException(String message, Throwable cause)141 private CrawlerException(String message, Throwable cause) { 142 super(message, cause); 143 } 144 145 /** 146 * Constructs a new {@link CrawlerException} with a cause. 147 * 148 * @param cause A {@link Throwable} capturing the original cause of the CrawlerException. 149 */ CrawlerException(Throwable cause)150 private CrawlerException(Throwable cause) { 151 super(cause); 152 } 153 } 154 155 /** 156 * Runs the setup, test, and teardown steps together. 157 * 158 * <p>Test won't run if setup failed, and teardown will always run. 159 * 160 * @throws DeviceNotAvailableException when the device is lost. 161 * @throws CrawlerException when unexpected happened. 162 * @throws IOException 163 * @throws ApkInstallerException 164 */ run()165 public void run() 166 throws DeviceNotAvailableException, 167 CrawlerException, 168 ApkInstallerException, 169 IOException { 170 try { 171 runSetup(); 172 runTest(); 173 } finally { 174 runTearDown(); 175 } 176 } 177 178 /** 179 * Runs only the setup step of the crawl test. 180 * 181 * @throws DeviceNotAvailableException when the device is lost. 182 * @throws IOException when IO operations fail. 183 * @throws ApkInstallerException when APK installation fails. 184 */ runSetup()185 public void runSetup() throws DeviceNotAvailableException, ApkInstallerException, IOException { 186 // For Espresso mode, checks that a path with the location of the apk to repackage was 187 // provided 188 if (!getOptions().isUiAutomatorMode()) { 189 Preconditions.checkNotNull( 190 getOptions().getRepackApk(), 191 "Apk file path is required when not running in UIAutomator mode"); 192 } 193 194 mApkInstaller = ApkInstaller.getInstance(mTestUtils.getDeviceUtils().getITestDevice()); 195 mApkInstaller.install( 196 getOptions().getInstallApkPaths().stream() 197 .map(File::toPath) 198 .collect(Collectors.toList()), 199 getOptions().getInstallArgs()); 200 201 // Grant external storage permission 202 if (getOptions().isGrantExternalStoragePermission()) { 203 mTestUtils.getDeviceUtils().grantExternalStoragePermissions(mPackageName); 204 } 205 mExecutionStage.setSetupComplete(true); 206 } 207 208 /** Runs only the teardown step of the crawl test. */ runTearDown()209 public void runTearDown() { 210 mTestUtils.saveApks( 211 getOptions().getSaveApkWhen(), 212 mExecutionStage.isTestPassed(), 213 mPackageName, 214 getOptions().getInstallApkPaths()); 215 if (getOptions().getRepackApk() != null) { 216 mTestUtils.saveApks( 217 getOptions().getSaveApkWhen(), 218 mExecutionStage.isTestPassed(), 219 mPackageName, 220 Arrays.asList(getOptions().getRepackApk())); 221 } 222 223 try { 224 mApkInstaller.uninstallAllInstalledPackages(); 225 } catch (ApkInstallerException e) { 226 CLog.e("Uninstallation of installed apps failed during teardown: %s", e.getMessage()); 227 } 228 if (!getOptions().isUiAutomatorMode()) { 229 try { 230 mTestUtils.getDeviceUtils().getITestDevice().uninstallPackage(mPackageName); 231 } catch (DeviceNotAvailableException e) { 232 CLog.e( 233 "Uninstallation of installed apps failed during teardown: %s", 234 e.getMessage()); 235 } 236 } 237 238 cleanUpOutputDir(); 239 } 240 241 /** 242 * Starts crawling the app and throw AssertionError if app crash is detected. 243 * 244 * @throws DeviceNotAvailableException when the device because unavailable. 245 * @throws CrawlerException when unexpected happened during the execution. 246 */ runTest()247 public void runTest() throws DeviceNotAvailableException, CrawlerException { 248 if (!mExecutionStage.isSetupComplete()) { 249 throw new CrawlerException("Crawler setup has not run."); 250 } 251 if (mExecutionStage.isTestExecuted()) { 252 throw new CrawlerException( 253 "The crawler has already run. Multiple runs in the same " 254 + AppCrawlTester.class.getName() 255 + " instance are not supported."); 256 } 257 mExecutionStage.setTestExecuted(true); 258 259 DeviceTimestamp startTime = mTestUtils.getDeviceUtils().currentTimeMillis(); 260 261 CrawlerException crawlerException = null; 262 try { 263 startCrawl(); 264 } catch (CrawlerException e) { 265 crawlerException = e; 266 } 267 DeviceTimestamp endTime = mTestUtils.getDeviceUtils().currentTimeMillis(); 268 269 ArrayList<String> failureMessages = new ArrayList<>(); 270 271 try { 272 273 List<DropboxEntry> crashEntries = 274 mTestUtils 275 .getDeviceUtils() 276 .getDropboxEntries( 277 DeviceUtils.DROPBOX_APP_CRASH_TAGS, 278 mPackageName, 279 startTime, 280 endTime); 281 String dropboxCrashLog = 282 mTestUtils.compileTestFailureMessage( 283 mPackageName, crashEntries, true, mScreenRecordStartTime); 284 285 if (dropboxCrashLog != null) { 286 // Put dropbox crash log on the top of the failure messages. 287 failureMessages.add(dropboxCrashLog); 288 } 289 } catch (IOException e) { 290 failureMessages.add("Error while getting dropbox crash log: " + e.getMessage()); 291 } 292 293 if (crawlerException != null) { 294 failureMessages.add(crawlerException.getMessage()); 295 } 296 297 if (!failureMessages.isEmpty()) { 298 Assert.fail( 299 String.join( 300 "\n============\n", 301 failureMessages.toArray(new String[failureMessages.size()]))); 302 } 303 304 mExecutionStage.setTestPassed(true); 305 } 306 307 /** 308 * Starts a crawler run on the configured app. 309 * 310 * @throws CrawlerException When the crawler was not set up correctly or the crawler run command 311 * failed. 312 * @throws DeviceNotAvailableException When device because unavailable. 313 */ 314 @VisibleForTesting startCrawl()315 void startCrawl() throws CrawlerException, DeviceNotAvailableException { 316 if (!AppCrawlTesterHostPreparer.isReady(mTestUtils.getTestInformation())) { 317 throw new CrawlerException( 318 "The " 319 + AppCrawlTesterHostPreparer.class.getName() 320 + " is not ready. Please check whether " 321 + AppCrawlTesterHostPreparer.class.getName() 322 + " was included in the test plan and completed successfully."); 323 } 324 325 try { 326 mOutput = Files.createTempDirectory("crawler"); 327 } catch (IOException e) { 328 throw new CrawlerException("Failed to create temp directory for output.", e); 329 } 330 331 IRunUtil runUtil = mRunUtilProvider.get(); 332 AtomicReference<String[]> command = new AtomicReference<>(); 333 AtomicReference<CommandResult> commandResult = new AtomicReference<>(); 334 335 CLog.d("Start to crawl package: %s.", mPackageName); 336 337 Path bin = 338 mFileSystem.getPath( 339 AppCrawlTesterHostPreparer.getCrawlerBinPath( 340 mTestUtils.getTestInformation())); 341 boolean isUtpClient = false; 342 if (Files.exists(bin.resolve("utp-cli-android_deploy.jar"))) { 343 command.set(createUtpCrawlerRunCommand(mTestUtils.getTestInformation())); 344 runUtil.setEnvVariable( 345 "ANDROID_SDK", 346 AppCrawlTesterHostPreparer.getSdkPath(mTestUtils.getTestInformation()) 347 .toString()); 348 isUtpClient = true; 349 } else if (Files.exists(bin.resolve("crawl_launcher_deploy.jar"))) { 350 command.set(createCrawlerRunCommand(mTestUtils.getTestInformation())); 351 runUtil.setEnvVariable( 352 "GOOGLE_APPLICATION_CREDENTIALS", 353 AppCrawlTesterHostPreparer.getCredentialPath(mTestUtils.getTestInformation()) 354 .toString()); 355 } else { 356 throw new CrawlerException( 357 "Crawler executable binaries not found in " + bin.toString()); 358 } 359 360 if (getOptions().isCollectGmsVersion()) { 361 mTestUtils.collectGmsVersion(mPackageName); 362 } 363 364 // Minimum timeout 3 minutes plus crawl test timeout. 365 long commandTimeout = 3L * 60 * 1000 + getOptions().getTimeoutSec() * 1000; 366 367 CLog.i( 368 "Starting to crawl the package %s with command %s", 369 mPackageName, String.join(" ", command.get())); 370 // TODO(yuexima): When the obb_file option is supported in espresso mode, the timeout need 371 // to be extended. 372 if (getOptions().isRecordScreen()) { 373 mTestUtils.collectScreenRecord( 374 () -> { 375 commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get())); 376 }, 377 mPackageName, 378 deviceTime -> mScreenRecordStartTime = deviceTime); 379 } else { 380 commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get())); 381 } 382 383 // Must be done after the crawler run because the app is installed by the crawler. 384 if (getOptions().isCollectAppVersion()) { 385 mTestUtils.collectAppVersion(mPackageName); 386 } 387 388 collectOutputZip(); 389 collectCrawlStepScreenshots(isUtpClient); 390 createCrawlerRoboscriptSignal(isUtpClient); 391 392 if (!commandResult.get().getStatus().equals(CommandStatus.SUCCESS) 393 || commandResult.get().getStdout().contains("Unknown options:")) { 394 throw new CrawlerException("Crawler command failed: " + commandResult.get()); 395 } 396 397 CLog.i("Completed crawling the package %s. Outputs: %s", mPackageName, commandResult.get()); 398 } 399 400 /** Copys the step screenshots into test outputs for easier access. */ collectCrawlStepScreenshots(boolean isUtpClient)401 private void collectCrawlStepScreenshots(boolean isUtpClient) { 402 if (mOutput == null) { 403 CLog.e("Output directory is not created yet. Skipping collecting step screenshots."); 404 return; 405 } 406 407 Path subDir = getClientCrawlerOutputSubDir(isUtpClient); 408 if (!Files.exists(subDir)) { 409 CLog.e( 410 "The crawler output directory is not complete, skipping collecting step" 411 + " screenshots."); 412 return; 413 } 414 415 try (Stream<Path> files = Files.list(subDir)) { 416 files.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".png")) 417 .forEach( 418 path -> { 419 mTestUtils 420 .getTestArtifactReceiver() 421 .addTestArtifact( 422 mPackageName 423 + "-crawl_step_screenshot_" 424 + path.getFileName(), 425 LogDataType.PNG, 426 path.toFile()); 427 }); 428 } catch (IOException e) { 429 CLog.e(e); 430 } 431 } 432 433 /** 434 * Reads the crawler output and creates an artifact with the success signal for a Roboscript 435 * that has been executed by the crawler. 436 */ createCrawlerRoboscriptSignal(boolean isUtpClient)437 private void createCrawlerRoboscriptSignal(boolean isUtpClient) { 438 if (mOutput == null) { 439 CLog.e("Output directory is not created yet. Skipping collecting crawler signal."); 440 return; 441 } 442 443 Path subDir = getClientCrawlerOutputSubDir(isUtpClient); 444 if (!Files.exists(subDir)) { 445 CLog.e( 446 "The crawler output directory is not complete, skipping collecting crawler" 447 + " signal."); 448 return; 449 } 450 451 try (Stream<Path> files = Files.list(subDir)) { 452 Optional<Path> roboOutputFile = 453 files.filter( 454 path -> 455 path.getFileName() 456 .toString() 457 .toLowerCase() 458 .endsWith("crawl_outputs.txt")) 459 .findFirst(); 460 if (roboOutputFile.isPresent()) { 461 generateRoboscriptSignalFile(roboOutputFile.get(), mPackageName); 462 } 463 } catch (IOException e) { 464 CLog.e(e); 465 } 466 } 467 468 /** 469 * Generates an artifact text file with a name indicating whether the Roboscript was successful. 470 * 471 * @param roboOutputFile - the file containing the Robo crawler output. 472 * @param packageName - the android package name of the app for which the signal file is being 473 * generated. 474 */ generateRoboscriptSignalFile(Path roboOutputFile, String packageName)475 private void generateRoboscriptSignalFile(Path roboOutputFile, String packageName) { 476 try { 477 File signalFile = 478 Files.createTempFile( 479 packageName 480 + "_roboscript_" 481 + getRoboscriptSignal(Optional.of(roboOutputFile)) 482 .toString() 483 .toLowerCase(), 484 ".txt") 485 .toFile(); 486 mTestUtils 487 .getTestArtifactReceiver() 488 .addTestArtifact(signalFile.getName(), LogDataType.HOST_LOG, signalFile); 489 } catch (IOException e) { 490 CLog.e(e); 491 } 492 } 493 494 /** 495 * Computes whether the Robosript was successful based on the output file, and returns the 496 * success signal. 497 * 498 * @param roboOutput 499 * @return Roboscript success signal 500 */ getRoboscriptSignal(Optional<Path> roboOutput)501 public RoboscriptSignal getRoboscriptSignal(Optional<Path> roboOutput) { 502 if (!roboOutput.isPresent()) { 503 return RoboscriptSignal.UNKNOWN; 504 } 505 Pattern totalActionsPattern = 506 Pattern.compile("(?:robo_script_execution(?:.|\\n)*)total_actions.\\s(\\d*)"); 507 Pattern successfulActionsPattern = 508 Pattern.compile("(?:robo_script_execution(?:.|\\n)*)successful_actions.\\s(\\d*)"); 509 final String outputFile; 510 try { 511 outputFile = 512 String.join("", Files.readAllLines(roboOutput.get(), Charset.defaultCharset())); 513 } catch (IOException e) { 514 CLog.e(e); 515 return RoboscriptSignal.UNKNOWN; 516 } 517 int totalActions = 0; 518 int successfulActions = 0; 519 Matcher mTotal = totalActionsPattern.matcher(outputFile); 520 Matcher mSuccessful = successfulActionsPattern.matcher(outputFile); 521 if (mTotal.find() && mSuccessful.find()) { 522 totalActions = Integer.parseInt(mTotal.group(1)); 523 successfulActions = Integer.parseInt(mSuccessful.group(1)); 524 if (totalActions == 0) { 525 return RoboscriptSignal.FAIL; 526 } 527 return successfulActions / totalActions < 1 528 ? RoboscriptSignal.FAIL 529 : RoboscriptSignal.SUCCESS; 530 } 531 return RoboscriptSignal.UNKNOWN; 532 } 533 534 /** Based on the type of Robo client, resolves the Path for its output directory. */ getClientCrawlerOutputSubDir(boolean isUtpClient)535 private Path getClientCrawlerOutputSubDir(boolean isUtpClient) { 536 return isUtpClient 537 ? mOutput.resolve("output").resolve("artifacts") 538 : mOutput.resolve("app_firebase_test_lab"); 539 } 540 541 /** Puts the zipped crawler output files into test output. */ collectOutputZip()542 private void collectOutputZip() { 543 if (mOutput == null) { 544 CLog.e("Output directory is not created yet. Skipping collecting output."); 545 return; 546 } 547 548 // Compress the crawler output directory and add it to test outputs. 549 try { 550 File outputZip = ZipUtil.createZip(mOutput.toFile()); 551 mTestUtils 552 .getTestArtifactReceiver() 553 .addTestArtifact(mPackageName + "-crawler_output", LogDataType.ZIP, outputZip); 554 } catch (IOException e) { 555 CLog.e("Failed to zip the output directory: " + e); 556 } 557 } 558 559 @VisibleForTesting createUtpCrawlerRunCommand(TestInformation testInfo)560 String[] createUtpCrawlerRunCommand(TestInformation testInfo) throws CrawlerException { 561 562 Path bin = 563 mFileSystem.getPath( 564 AppCrawlTesterHostPreparer.getCrawlerBinPath( 565 mTestUtils.getTestInformation())); 566 ArrayList<String> cmd = new ArrayList<>(); 567 cmd.addAll( 568 Arrays.asList( 569 "java", 570 "-jar", 571 bin.resolve("utp-cli-android_deploy.jar").toString(), 572 "android", 573 "robo", 574 "--device-id", 575 testInfo.getDevice().getSerialNumber(), 576 "--app-id", 577 mPackageName, 578 "--controller-endpoint", 579 "PROD", 580 "--utp-binaries-dir", 581 bin.toString(), 582 "--key-file", 583 AppCrawlTesterHostPreparer.getCredentialPath( 584 mTestUtils.getTestInformation()) 585 .toString(), 586 "--base-crawler-apk", 587 bin.resolve("crawler_app.apk").toString(), 588 "--stub-crawler-apk", 589 bin.resolve("crawler_stubapp_androidx.apk").toString(), 590 "--tmp-dir", 591 mOutput.toString())); 592 593 if (getOptions().getTimeoutSec() > 0) { 594 cmd.add("--crawler-flag"); 595 cmd.add("crawlDurationSec=" + Integer.toString(getOptions().getTimeoutSec())); 596 } 597 598 if (getOptions().isUiAutomatorMode()) { 599 cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-installed-on-device")); 600 } else { 601 Preconditions.checkNotNull( 602 getOptions().getRepackApk(), 603 "Apk file path is required when not running in UIAutomator mode"); 604 605 try { 606 TestUtils.listApks(mFileSystem.getPath(getOptions().getRepackApk().toString())) 607 .forEach( 608 path -> { 609 String nameLowercase = 610 path.getFileName().toString().toLowerCase(); 611 if (nameLowercase.endsWith(".apk")) { 612 cmd.add("--apks-to-crawl"); 613 cmd.add(path.toString()); 614 } else if (nameLowercase.endsWith(".obb")) { 615 cmd.add("--files-to-push"); 616 cmd.add( 617 String.format( 618 "%s=/sdcard/Android/obb/%s/%s", 619 path.toString(), 620 mPackageName, 621 path.getFileName().toString())); 622 } else { 623 CLog.d("Skipping unrecognized file %s", path.toString()); 624 } 625 }); 626 } catch (TestUtilsException e) { 627 throw new CrawlerException(e); 628 } 629 } 630 631 if (getOptions().getRoboscriptFile() != null) { 632 Assert.assertTrue( 633 "Please provide a valid roboscript file.", 634 Files.isRegularFile( 635 mFileSystem.getPath(getOptions().getRoboscriptFile().toString()))); 636 cmd.add("--crawler-asset"); 637 cmd.add("robo.script=" + getOptions().getRoboscriptFile().toString()); 638 } 639 640 if (getOptions().getCrawlGuidanceProtoFile() != null) { 641 Assert.assertTrue( 642 "Please provide a valid CrawlGuidance file.", 643 Files.isRegularFile( 644 mFileSystem.getPath( 645 getOptions().getCrawlGuidanceProtoFile().toString()))); 646 cmd.add("--crawl-guidance-proto-path"); 647 cmd.add(getOptions().getCrawlGuidanceProtoFile().toString()); 648 } 649 650 if (getOptions().getLoginConfigDir() != null) { 651 RoboLoginConfigProvider configProvider = 652 new RoboLoginConfigProvider( 653 mFileSystem.getPath(getOptions().getLoginConfigDir().toString())); 654 cmd.addAll(configProvider.findConfigFor(mPackageName, true).getLoginArgs()); 655 } 656 657 return cmd.toArray(new String[cmd.size()]); 658 } 659 660 @VisibleForTesting createCrawlerRunCommand(TestInformation testInfo)661 String[] createCrawlerRunCommand(TestInformation testInfo) throws CrawlerException { 662 663 Path bin = 664 mFileSystem.getPath( 665 AppCrawlTesterHostPreparer.getCrawlerBinPath( 666 mTestUtils.getTestInformation())); 667 ArrayList<String> cmd = new ArrayList<>(); 668 cmd.addAll( 669 Arrays.asList( 670 "java", 671 "-jar", 672 bin.resolve("crawl_launcher_deploy.jar").toString(), 673 "--android-sdk-path", 674 AppCrawlTesterHostPreparer.getSdkPath(testInfo).toString(), 675 "--device-serial-code", 676 testInfo.getDevice().getSerialNumber(), 677 "--output-dir", 678 mOutput.toString(), 679 "--key-store-file", 680 // Using the publicly known default file name of the debug keystore. 681 bin.resolve("debug.keystore").toString(), 682 "--key-store-password", 683 // Using the publicly known default password of the debug keystore. 684 "android")); 685 686 if (getOptions().getCrawlControllerEndpoint() != null 687 && getOptions().getCrawlControllerEndpoint().length() > 0) { 688 cmd.addAll(Arrays.asList("--endpoint", getOptions().getCrawlControllerEndpoint())); 689 } 690 691 if (getOptions().isUiAutomatorMode()) { 692 cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-package-name", mPackageName)); 693 } else { 694 Preconditions.checkNotNull( 695 getOptions().getRepackApk(), 696 "Apk file path is required when not running in UIAutomator mode"); 697 698 List<Path> apks; 699 try { 700 apks = 701 TestUtils.listApks( 702 mFileSystem.getPath(getOptions().getRepackApk().toString())) 703 .stream() 704 .filter( 705 path -> 706 path.getFileName() 707 .toString() 708 .toLowerCase() 709 .endsWith(".apk")) 710 .collect(Collectors.toList()); 711 } catch (TestUtilsException e) { 712 throw new CrawlerException(e); 713 } 714 715 cmd.add("--apk-file"); 716 cmd.add(apks.get(0).toString()); 717 718 for (int i = 1; i < apks.size(); i++) { 719 cmd.add("--split-apk-files"); 720 cmd.add(apks.get(i).toString()); 721 } 722 } 723 724 if (getOptions().getTimeoutSec() > 0) { 725 cmd.add("--timeout-sec"); 726 cmd.add(Integer.toString(getOptions().getTimeoutSec())); 727 } 728 729 if (getOptions().getRoboscriptFile() != null) { 730 Assert.assertTrue( 731 "Please provide a valid roboscript file.", 732 Files.isRegularFile( 733 mFileSystem.getPath(getOptions().getRoboscriptFile().toString()))); 734 cmd.addAll( 735 Arrays.asList( 736 "--robo-script-file", getOptions().getRoboscriptFile().toString())); 737 } 738 739 if (getOptions().getCrawlGuidanceProtoFile() != null) { 740 Assert.assertTrue( 741 "Please provide a valid CrawlGuidance file.", 742 Files.isRegularFile( 743 mFileSystem.getPath( 744 getOptions().getCrawlGuidanceProtoFile().toString()))); 745 cmd.addAll( 746 Arrays.asList( 747 "--text-guide-file", 748 getOptions().getCrawlGuidanceProtoFile().toString())); 749 } 750 751 if (getOptions().getLoginConfigDir() != null) { 752 RoboLoginConfigProvider configProvider = 753 new RoboLoginConfigProvider( 754 mFileSystem.getPath(getOptions().getLoginConfigDir().toString())); 755 cmd.addAll(configProvider.findConfigFor(mPackageName, false).getLoginArgs()); 756 } 757 758 return cmd.toArray(new String[cmd.size()]); 759 } 760 761 private class ExecutionStage { 762 private boolean mIsSetupComplete = false; 763 private boolean mIsTestExecuted = false; 764 private boolean mIsTestPassed = false; 765 isSetupComplete()766 private boolean isSetupComplete() { 767 return mIsSetupComplete; 768 } 769 setSetupComplete(boolean isSetupComplete)770 private void setSetupComplete(boolean isSetupComplete) { 771 mIsSetupComplete = isSetupComplete; 772 } 773 isTestExecuted()774 private boolean isTestExecuted() { 775 return mIsTestExecuted; 776 } 777 setTestExecuted(boolean misTestExecuted)778 private void setTestExecuted(boolean misTestExecuted) { 779 mIsTestExecuted = misTestExecuted; 780 } 781 isTestPassed()782 private boolean isTestPassed() { 783 return mIsTestPassed; 784 } 785 setTestPassed(boolean isTestPassed)786 private void setTestPassed(boolean isTestPassed) { 787 mIsTestPassed = isTestPassed; 788 } 789 } 790 791 /** Cleans up the crawler output directory. */ 792 @VisibleForTesting cleanUpOutputDir()793 void cleanUpOutputDir() { 794 if (mOutput == null) { 795 return; 796 } 797 798 try { 799 MoreFiles.deleteRecursively(mOutput); 800 } catch (IOException e) { 801 CLog.e("Failed to clean up the crawler output directory: " + e); 802 } 803 } 804 805 @VisibleForTesting 806 interface RunUtilProvider { get()807 IRunUtil get(); 808 } 809 } 810