• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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