自动化工具 谷歌官方发布应用遍历工具 App Crawler, crawl_launcher 浅析

花开 · 2019年10月04日 · 最后由 陈子昂 回复于 2019年10月07日 · 2586 次阅读

写这个文章的主要目的是因为我没有跑起来,然后先看看里面具体如何实现的,然后明确我的问题是啥?如果不明白这个是啥可以看这里:
谷歌官方发布应用遍历工具 App Crawler

我画了一个简单的类图,方便查看

程序入口类是这个 androidx.test.tools.crawler.launcher.CrawlLauncher, 差不多主要方法都在里面了 CrawlSetup ExternalCrawlSetup 是工具的配置信息,基本所有的配置都在里面。

这里简单说一下逻辑:

  • launchCrawl

    • 获取包名信息,如果不在参数里面就是用 aapt dump, 具体可以查看util.AppPackageNameExtractor
    • 是否需要唤醒,具体可以查看util.AdbExecutor
    • 情况输出目录
    • 这里比较奇怪,如果不是--ui-automator-mode--instant-apps-mode 会对 crawler_app.apk 重新签名, 然后比较坑的是不会重新签名 sub 文件,好像会造成启动不起来
    • 和上面一样,如果指定了apk文件,一样会对 apk 重新签名
  • installCrawlerAndApp(这里面会对是否有 dump 权限检查,在 23 以上)

    • cleanupDevice
    • 安装 sub
    • 安装遍历 app
  • cleanupDevice

    • 停止遍历的 app
    • 清空如下目录 /sdcard/app_firebase_test_lab /sdcard/robo_tmp_files
    • 卸载 crawler_app.apk
    • 卸载遍历的 app

最后就是开始遍历

public CrawlLauncher(String[] crawlParameters) {
        this.crawlSetup.processCrawlParameters(crawlParameters);
        this.appPackageNameExtractor = new AppPackageNameExtractor(this.crawlSetup);
        this.apkSigner = new ApkSigner(this.crawlSetup);
        this.crawlerRepacker = new CrawlerRepacker(this.crawlSetup, this.apkSigner);
        this.adbExecutor = new AdbExecutor(this.crawlSetup);
        this.deviceApiLevel = this.adbExecutor.getDeviceApiLevel();
    }

    public static void main(String[] args) throws CrawlerRepackingException, ApkSigningException {
        new CrawlLauncher(args).launchCrawl(false);
    }

    public CrawlSetup getCrawlSetup() {
        return this.crawlSetup;
    }

    public AdbExecutor getAdbExecutor() {
        return this.adbExecutor;
    }

    public void launchCrawl(boolean isHostGuidedCrawl) throws ApkSigningException, CrawlerRepackingException {
        Path signedRepackedCrawlerApkFilePath;
        String appPackageName = this.crawlSetup.getAppPackageName();
        if (appPackageName.isEmpty()) {
            appPackageName = this.appPackageNameExtractor.extractAppPackageName();
        }
        Logger.atInfo().log("Preparing to crawl %s", appPackageName);
        if (this.crawlSetup.isWakeDevice()) {
            this.adbExecutor.wakeDevice();
        }
        if (!isHostGuidedCrawl) {
            FileUtil.cleanupDirectory(this.crawlSetup.getOutputDirectoryPath());
        }
        this.crawlSetup.buildCrawler();
        if (this.crawlSetup.isUiAutomatorMode() || this.crawlSetup.isInstantAppsMode()) {
            signedRepackedCrawlerApkFilePath = this.crawlSetup.getCrawlerAppApkPath();
        } else {
            signedRepackedCrawlerApkFilePath = this.crawlerRepacker.repackAndSignCrawlerApp(appPackageName);
        }
        Optional<Path> appApkFilePath = Optional.empty();
        if (!this.crawlSetup.getApkFilePath().isEmpty()) {
            appApkFilePath = Optional.of(Paths.get(this.crawlSetup.getApkFilePath(), new String[0]));
            if (!(this.crawlSetup.isUiAutomatorMode() || this.crawlSetup.isInstantAppsMode())) {
                appApkFilePath = Optional.of(this.apkSigner.sign((Path) appApkFilePath.get()));
            }
        }
        installCrawlerAndApp(appPackageName, signedRepackedCrawlerApkFilePath, appApkFilePath);
        LogcatRecorder logcatRecorder = null;
        if (!isHostGuidedCrawl) {
            if (this.crawlSetup.isPauseBeforeCrawl()) {
                Logger.atInfo().log("Press Enter to start the crawl", new Object[0]);
                try {
                    System.in.read();
                } catch (Exception e) {
                }
            }
            logcatRecorder = new LogcatRecorder(this.adbExecutor, this.crawlSetup.getOutputDirectoryPath(), appPackageName);
            logcatRecorder.start();
            new VideocatRecorder(this.adbExecutor, this.crawlSetup, appPackageName, this.deviceApiLevel).start();
        }
        startCrawler(appPackageName);
        this.adbExecutor.stopReadingInput();
        if (!isHostGuidedCrawl) {
            processLogcat(logcatRecorder);
            processCrawlOutput();
        }
    }

    private static /* synthetic */ boolean lambda$processLogcat$0(LogcatFinding finding) {
        return finding.type() == LogcatFindingType.NON_SDK_API_USED;
    }

    private static /* synthetic */ boolean lambda$processLogcat$1(LogcatFinding f) {
        return f.type() == LogcatFindingType.FATAL_EXCEPTION || f.type() == LogcatFindingType.NATIVE_CRASH;
    }

    private void installCrawlerAndApp(String appPackageName, Path crawlerApkFilePath, Optional<Path> appApkFilePath) {
        boolean grantPermissionsOnInstall;
        cleanupDevice(appPackageName);
        if (this.deviceApiLevel >= 23) {
            grantPermissionsOnInstall = true;
        } else {
            grantPermissionsOnInstall = false;
        }
        this.adbExecutor.installApp(grantPermissionsOnInstall, crawlerApkFilePath.toAbsolutePath().toString());
        this.adbExecutor.execute("shell", "pm", "grant", ConfigConstants.CRAWLER_PACKAGE_ID, "android.permission.DUMP");
        this.adbExecutor.installApp(grantPermissionsOnInstall, "-r", this.crawlSetup.getCrawlerStubappApk().getAbsolutePath());
        if (appApkFilePath.isPresent()) {
            this.adbExecutor.installApp(grantPermissionsOnInstall, ((Path) appApkFilePath.get()).toAbsolutePath().toString());
        }
    }

    private void cleanupDevice(String appPackageName) {
        this.adbExecutor.execute("shell", "am", "force-stop", appPackageName);
        this.adbExecutor.execute("shell", "rm", "-rf", CrawlSetup.DEVICE_OUTPUT);
        this.adbExecutor.execute("shell", "rm", "-rf", CrawlSetup.ROBO_TEMP_FILES);
        this.adbExecutor.uninstallApp(ConfigConstants.CRAWLER_PACKAGE_ID);
        if (!this.crawlSetup.getApkFilePath().isEmpty()) {
            this.adbExecutor.uninstallApp(appPackageName);
        }
    }

    private void startCrawler(String appPackageName) {
        List<String> executionOptions = getExecutionOptions(appPackageName);
        List startServiceCommand = new ArrayList(Arrays.asList(new String[]{"shell", "am", "startservice"}));
        startServiceCommand.addAll(executionOptions);
        startServiceCommand.add("-n");
        startServiceCommand.add("androidx.test.tools.crawler/androidx.test.tools.crawler.controller.CrawlDriver");
        this.adbExecutor.execute(startServiceCommand);
        List instrumentCommand = new ArrayList(Arrays.asList(new String[]{"shell", "am", "instrument", "--no-window-animation", "-w", "-r"}));
        instrumentCommand.addAll(executionOptions);
        addExecutionOption("class", "androidx.test.tools.crawler.CrawlPlatform", instrumentCommand);
        instrumentCommand.add("androidx.test.tools.crawler/androidx.test.runner.AndroidJUnitRunner");
        Logger.atInfo().log("Crawl started.", new Object[0]);
        this.adbExecutor.execute(instrumentCommand);
        this.adbExecutor.execute("shell", "am", "instrument", "-w", "-r", "androidx.test.tools.crawler/.CrawlMonitor");
        Logger.atInfo().log("Crawl finished.", new Object[0]);
    }

    private List<String> getExecutionOptions(String appPackageName) {
        List<String> options = new ArrayList();
        addExecutionOption(ConfigConstants.ROBO_V2_CRAWL_DURATION_FLAG, String.valueOf(this.crawlSetup.getCrawlTimeoutSeconds()), options);
        addExecutionOption(ConfigConstants.ROBO_V2_APP_PACKAGE_FLAG, appPackageName, options);
        addExecutionOption("appListener", "androidx.test.tools.crawler.SignaturePatchingCallback", options);
        addExecutionOption("disableAnalytics", "true", options);
        addExecutionOption(ConfigConstants.ROBO_V2_EXECUTION_ID_FLAG, UUID.randomUUID().toString(), options);
        addExecutionOption("dataDir", this.crawlSetup.getOutputDirectoryPath().toAbsolutePath().toString(), options);
        if (this.crawlSetup.isTestAccessibility()) {
            addExecutionOption(ConfigConstants.ROBO_V2_TEST_ACCESSIBILITY, "true", options);
        }
        if (this.crawlSetup.isInstantAppsMode()) {
            addExecutionOption(ConfigConstants.ROBO_V2_INSTANT_APPS_MODE, "true", options);
        }
        addExecutionOption(ConfigConstants.ROBO_V2_CRAWL_DRIVER_INITIALIZER_CLASS_FLAG, this.crawlSetup.getCrawlDriverInitializerClass(), options);
        Properties experiments = this.crawlSetup.getExperiments();
        for (String experimentName : experiments.stringPropertyNames()) {
            addExecutionOption(experimentName, experiments.getProperty(experimentName), options);
        }
        return options;
    }

    private static void addExecutionOption(String optionName, String optionValue, List<String> options) {
        options.add("-e");
        options.add(optionName);
        options.add(optionValue);
    }

    private void processCrawlOutput() {
        this.adbExecutor.execute("pull", CrawlSetup.DEVICE_OUTPUT, this.crawlSetup.getOutputDirectoryPath().toAbsolutePath().toString());
        Optional<File> crawlOutputsBinaryProtoFile = getCrawlOutputsBinaryProtoFile();
        if (crawlOutputsBinaryProtoFile.isPresent()) {
            Logger.atInfo().log("Converting output files", new Object[0]);
            Appendable fileWriter;
            try {
                MessageOrBuilder crawlOutputs = CrawlOutputs.parseFrom(new FileInputStream((File) crawlOutputsBinaryProtoFile.get()));
                fileWriter = Files.newBufferedWriter(Paths.get(this.crawlSetup.getCrawlOutputsTextProtoPath(), new String[0]), StandardCharsets.UTF_8, new OpenOption[0]);
                TextFormat.print(crawlOutputs, fileWriter);
                if (fileWriter != null) {
                    fileWriter.close();
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to convert output files", e);
            } catch (Throwable th) {
                th.addSuppressed(th);
            }
        }
        Logger.atInfo().log("The output directory is %s", this.crawlSetup.getOutputDirectoryPath().toAbsolutePath().toString());
    }

    private Optional<File> getCrawlOutputsBinaryProtoFile() {
        File crawlOutputsBinaryProtoFile = new File(this.crawlSetup.getCrawlOutputsBinaryProtoPath());
        int waitForCrawlOutputsAttempts = 30;
        while (!crawlOutputsBinaryProtoFile.exists() && waitForCrawlOutputsAttempts > 0) {
            Logger.atDebug().log("Waiting for crawl outputs proto file %s", crawlOutputsBinaryProtoFile);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Logger.atWarning().log("Interrupted while waiting for outputs proto file %s", crawlOutputsBinaryProtoFile);
            }
            waitForCrawlOutputsAttempts--;
        }
        if (crawlOutputsBinaryProtoFile.exists()) {
            return Optional.of(crawlOutputsBinaryProtoFile);
        }
        Logger.atWarning().log("Timed out waiting for crawl outputs proto file %s", crawlOutputsBinaryProtoFile);
        return Optional.empty();
    }

最后放上反编译的代码,虽然关系没有了,但是还可以阅读的
链接: https://pan.baidu.com/s/17ijQDr83gClopV7sAagFhQ 提取码: gyjy 复制这段内容后打开百度网盘手机 App,操作更方便哦

然后顺便求一个测试开发工程师的岗位。 目标地点杭州。多年安卓开发经验,对测试相关信息有一定的了解,希望在质量领域做点事。

共收到 1 条回复 时间 点赞

蛮不错的,谷歌的工具和思寒几年前的叫同一个名字,作用也差不多。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册