diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:52:02 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:52:02 +0000 |
commit | c463c196705ca086645d312e5cd173637f1f34cd (patch) | |
tree | 033dc92ba1dfd9d3f438e4ee99f4897a4ac0644f | |
parent | 2675a424f87f605059355c2f11baffc9fc8b7dbe (diff) | |
parent | e43fe576896287dcdd51be7fa2699f3e4822a9ba (diff) | |
download | csuite-c463c196705ca086645d312e5cd173637f1f34cd.tar.gz |
Snap for 10453563 from e43fe576896287dcdd51be7fa2699f3e4822a9ba to mainline-art-releaseaml_art_341411300aml_art_341311100aml_art_341110110aml_art_341110060aml_art_341010050aml_art_340915060android14-mainline-art-release
Change-Id: I553feaf6484665138cd85428dbf32d6242f47daf
55 files changed, 3240 insertions, 533 deletions
@@ -15,15 +15,3 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } - -python_defaults { - name: "csuite_python_defaults", - version: { - py2: { - enabled: false, - }, - py3: { - enabled: true, - }, - }, -} diff --git a/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java b/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java index 82fddb8..a9bc8a1 100644 --- a/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java +++ b/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java @@ -154,7 +154,7 @@ public final class AppRemoteFileResolver implements IRemoteFileResolver { } catch (URISyntaxException e) { throw new IllegalStateException( String.format( - "URI template (%s) did not expand to a a valid URI (%s)", + "%s: URI template (%s) did not expand to a a valid URI (%s)", URI_TEMPLATE_OPTION, mUriTemplate, expanded), e); } diff --git a/harness/src/main/java/com/android/csuite/core/ApkInstaller.java b/harness/src/main/java/com/android/csuite/core/ApkInstaller.java index 2882164..54df8f4 100644 --- a/harness/src/main/java/com/android/csuite/core/ApkInstaller.java +++ b/harness/src/main/java/com/android/csuite/core/ApkInstaller.java @@ -20,6 +20,7 @@ import com.android.csuite.core.TestUtils.TestUtilsException; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.util.AaptParser; +import com.android.tradefed.util.AaptParser.AaptVersion; import com.android.tradefed.util.CommandResult; import com.android.tradefed.util.CommandStatus; import com.android.tradefed.util.IRunUtil; @@ -31,14 +32,16 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; /** A utility class to install APKs. */ public final class ApkInstaller { private static long sCommandTimeOut = TimeUnit.MINUTES.toMillis(4); + private static long sObbPushCommandTimeOut = TimeUnit.MINUTES.toMillis(12); private final String mDeviceSerial; - private final List<Path> mInstalledBaseApks = new ArrayList<>(); + private final List<String> mInstalledPackages = new ArrayList<>(); private final IRunUtil mRunUtil; private final PackageNameParser mPackageNameParser; @@ -66,7 +69,7 @@ public final class ApkInstaller { * @throws ApkInstallerException If the installation failed. * @throws IOException If an IO exception occurred. */ - public void install(Path apkPath, String... args) throws ApkInstallerException, IOException { + public void install(Path apkPath, List<String> args) throws ApkInstallerException, IOException { List<Path> apkFilePaths; try { apkFilePaths = TestUtils.listApks(apkPath); @@ -74,24 +77,77 @@ public final class ApkInstaller { throw new ApkInstallerException("Failed to list APK files from the path " + apkPath, e); } - CLog.d("Installing a package from " + apkPath); + String packageName; + try { + packageName = mPackageNameParser.parsePackageName(apkFilePaths.get(0)); + } catch (IOException e) { + throw new ApkInstallerException( + String.format("Failed to parse the package name from %s", apkPath), e); + } + CLog.d("Attempting to uninstall package %s before installation", packageName); + String[] uninstallCmd = createUninstallCommand(packageName, mDeviceSerial); + // TODO(yuexima): Add command result checks after we start to check whether. + // the package is installed on device before uninstalling it. + // At this point, command failure is expected if the package wasn't installed. + mRunUtil.runTimedCmd(sCommandTimeOut, uninstallCmd); - String[] cmd = createInstallCommand(apkFilePaths, mDeviceSerial, args); + CLog.d("Installing package %s from %s", packageName, apkPath); - CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd); - if (res.getStatus() != CommandStatus.SUCCESS) { + String[] installApkCmd = createApkInstallCommand(apkFilePaths, mDeviceSerial, args); + + CommandResult apkRes = mRunUtil.runTimedCmd(sCommandTimeOut, installApkCmd); + if (apkRes.getStatus() != CommandStatus.SUCCESS) { throw new ApkInstallerException( String.format( "Failed to install APKs from the path %s: %s", - apkPath, res.toString())); + apkPath, apkRes.toString())); } - mInstalledBaseApks.add(apkFilePaths.get(0)); + mInstalledPackages.add(packageName); + + List<String[]> installObbCmds = + createObbInstallCommands(apkFilePaths, mDeviceSerial, packageName); + for (String[] cmd : installObbCmds) { + CommandResult obbRes = mRunUtil.runTimedCmd(sObbPushCommandTimeOut, cmd); + if (obbRes.getStatus() != CommandStatus.SUCCESS) { + throw new ApkInstallerException( + String.format( + "Failed to install an OBB file from the path %s: %s", + apkPath, obbRes.toString())); + } + } CLog.i("Successfully installed " + apkPath); } /** + * Overload for install method to use when install args are empty + * + * @param apkPath + * @throws ApkInstallerException + * @throws IOException + */ + public void install(Path apkPath) throws ApkInstallerException, IOException { + install(apkPath, Collections.emptyList()); + } + + /** + * Installs apks from a list of paths. Can be used to install additional library apks or 3rd + * party apks. + * + * @param apkPaths List of paths to the apk files. + * @param args Install args for the 'adb install-multiple' command. + * @throws ApkInstallerException If the installation failed. + * @throws IOException If an IO exception occurred. + */ + public void install(List<Path> apkPaths, List<String> args) + throws ApkInstallerException, IOException { + for (Path apkPath : apkPaths) { + install(apkPath, args); + } + } + + /** * Attempts to uninstall all the installed packages. * * <p>When failed to uninstall one of the installed packages, this method will still attempt to @@ -100,29 +156,18 @@ public final class ApkInstaller { * @throws ApkInstallerException when failed to uninstall a package. */ public void uninstallAllInstalledPackages() throws ApkInstallerException { - StringBuilder errorMessage = new StringBuilder(); - mInstalledBaseApks.forEach( - baseApk -> { - String packageName; - try { - packageName = mPackageNameParser.parsePackageName(baseApk); - } catch (IOException e) { - errorMessage.append( - String.format( - "Failed to parse the package name from %s. Reason: %s.\n", - baseApk, e.getMessage())); - return; - } - - String[] cmd = - new String[] {"adb", "-s", mDeviceSerial, "uninstall", packageName}; + CLog.d("Uninstalling all installed packages."); + StringBuilder errorMessage = new StringBuilder(); + mInstalledPackages.forEach( + installedPackage -> { + String[] cmd = createUninstallCommand(installedPackage, mDeviceSerial); CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd); if (res.getStatus() != CommandStatus.SUCCESS) { errorMessage.append( String.format( - "Failed to uninstall package %s from %s. Reason: %s.\n", - packageName, baseApk, res.toString())); + "Failed to uninstall package %s. Reason: %s.\n", + installedPackage, res.toString())); } }); @@ -131,15 +176,59 @@ public final class ApkInstaller { } } - private String[] createInstallCommand( - List<Path> apkFilePaths, String deviceSerial, String[] args) { + private String[] createApkInstallCommand( + List<Path> apkFilePaths, String deviceSerial, List<String> args) { ArrayList<String> cmd = new ArrayList<>(); cmd.addAll(Arrays.asList("adb", "-s", deviceSerial, "install-multiple")); + cmd.addAll(args); - cmd.addAll(Arrays.asList(args)); + apkFilePaths.stream() + .map(Path::toString) + .filter(path -> path.toLowerCase().endsWith(".apk")) + .forEach(cmd::add); - apkFilePaths.stream().map(Path::toString).forEach(cmd::add); + return cmd.toArray(new String[cmd.size()]); + } + + private List<String[]> createObbInstallCommands( + List<Path> apkFilePaths, String deviceSerial, String packageName) { + ArrayList<String[]> cmds = new ArrayList<>(); + + apkFilePaths.stream() + .filter(path -> path.toString().toLowerCase().endsWith(".obb")) + .forEach( + path -> { + String dest = + "/sdcard/Android/obb/" + packageName + "/" + path.getFileName(); + cmds.add( + new String[] { + "adb", "-s", deviceSerial, "shell", "rm", "-f", dest + }); + cmds.add( + new String[] { + "adb", "-s", deviceSerial, "push", path.toString(), dest + }); + }); + + if (!cmds.isEmpty()) { + cmds.add( + 0, + new String[] { + "adb", + "-s", + deviceSerial, + "shell", + "mkdir", + "-p", + "/sdcard/Android/obb/" + packageName + }); + } + + return cmds; + } + private String[] createUninstallCommand(String packageName, String deviceSerial) { + List<String> cmd = Arrays.asList("adb", "-s", deviceSerial, "uninstall", packageName); return cmd.toArray(new String[cmd.size()]); } @@ -180,7 +269,8 @@ public final class ApkInstaller { private static final class AaptPackageNameParser implements PackageNameParser { @Override public String parsePackageName(Path apkFile) throws IOException { - String packageName = AaptParser.parse(apkFile.toFile()).getPackageName(); + String packageName = + AaptParser.parse(apkFile.toFile(), AaptVersion.AAPT2).getPackageName(); if (packageName == null) { throw new IOException( String.format("Failed to parse package name with AAPT for %s", apkFile)); diff --git a/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java b/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java index 96785ef..258b374 100644 --- a/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java +++ b/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java @@ -17,6 +17,7 @@ package com.android.csuite.core; import com.android.csuite.core.DeviceUtils.DeviceTimestamp; +import com.android.csuite.core.TestUtils.TestUtilsException; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.invoker.TestInformation; import com.android.tradefed.log.LogUtil.CLog; @@ -36,28 +37,36 @@ import org.junit.Assert; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; + /** A tester that interact with an app crawler during testing. */ public final class AppCrawlTester { @VisibleForTesting Path mOutput; private final RunUtilProvider mRunUtilProvider; private final TestUtils mTestUtils; private final String mPackageName; - private static final long COMMAND_TIMEOUT_MILLIS = 4 * 60 * 1000; private boolean mRecordScreen = false; private boolean mCollectGmsVersion = false; private boolean mCollectAppVersion = false; private boolean mUiAutomatorMode = false; + private int mTimeoutSec; + private String mCrawlControllerEndpoint; private Path mApkRoot; + private Path mRoboscriptFile; + private Path mCrawlGuidanceProtoFile; + private Path mLoginConfigDir; + private FileSystem mFileSystem; /** * Creates an {@link AppCrawlTester} instance. @@ -68,23 +77,24 @@ public final class AppCrawlTester { * @return an {@link AppCrawlTester} instance. */ public static AppCrawlTester newInstance( - String packageName, - TestInformation testInformation, - TestLogData testLogData) { + String packageName, TestInformation testInformation, TestLogData testLogData) { return new AppCrawlTester( packageName, TestUtils.getInstance(testInformation, testLogData), - () -> new RunUtil()); + () -> new RunUtil(), + FileSystems.getDefault()); } @VisibleForTesting AppCrawlTester( String packageName, TestUtils testUtils, - RunUtilProvider runUtilProvider) { + RunUtilProvider runUtilProvider, + FileSystem fileSystem) { mRunUtilProvider = runUtilProvider; mPackageName = packageName; mTestUtils = testUtils; + mFileSystem = fileSystem; } /** An exception class representing crawler test failures. */ @@ -187,30 +197,52 @@ public final class AppCrawlTester { throw new CrawlerException("Failed to create temp directory for output.", e); } - String[] command = createCrawlerRunCommand(mTestUtils.getTestInformation()); - - CLog.d("Launching package: %s.", mPackageName); - IRunUtil runUtil = mRunUtilProvider.get(); - + AtomicReference<String[]> command = new AtomicReference<>(); AtomicReference<CommandResult> commandResult = new AtomicReference<>(); - runUtil.setEnvVariable( - "GOOGLE_APPLICATION_CREDENTIALS", - AppCrawlTesterHostPreparer.getCredentialPath(mTestUtils.getTestInformation()) - .toString()); + + CLog.d("Start to crawl package: %s.", mPackageName); + + Path bin = + mFileSystem.getPath( + AppCrawlTesterHostPreparer.getCrawlerBinPath( + mTestUtils.getTestInformation())); + boolean isUtpClient = false; + if (Files.exists(bin.resolve("utp-cli-android_deploy.jar"))) { + command.set(createUtpCrawlerRunCommand(mTestUtils.getTestInformation())); + runUtil.setEnvVariable( + "ANDROID_SDK", + AppCrawlTesterHostPreparer.getSdkPath(mTestUtils.getTestInformation()) + .toString()); + isUtpClient = true; + } else if (Files.exists(bin.resolve("crawl_launcher_deploy.jar"))) { + command.set(createCrawlerRunCommand(mTestUtils.getTestInformation())); + runUtil.setEnvVariable( + "GOOGLE_APPLICATION_CREDENTIALS", + AppCrawlTesterHostPreparer.getCredentialPath(mTestUtils.getTestInformation()) + .toString()); + } else { + throw new CrawlerException( + "Crawler executable binaries not found in " + bin.toString()); + } if (mCollectGmsVersion) { mTestUtils.collectGmsVersion(mPackageName); } + // Minimum timeout 3 minutes plus crawl test timeout. + long commandTimeout = 3 * 60 * 1000 + mTimeoutSec * 1000; + + // TODO(yuexima): When the obb_file option is supported in espresso mode, the timeout need + // to be extended. if (mRecordScreen) { mTestUtils.collectScreenRecord( () -> { - commandResult.set(runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, command)); + commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get())); }, mPackageName); } else { - commandResult.set(runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, command)); + commandResult.set(runUtil.runTimedCmd(commandTimeout, command.get())); } // Must be done after the crawler run because the app is installed by the crawler. @@ -219,9 +251,10 @@ public final class AppCrawlTester { } collectOutputZip(); - collectCrawlStepScreenshots(); + collectCrawlStepScreenshots(isUtpClient); - if (!commandResult.get().getStatus().equals(CommandStatus.SUCCESS)) { + if (!commandResult.get().getStatus().equals(CommandStatus.SUCCESS) + || commandResult.get().getStdout().contains("Unknown options:")) { throw new CrawlerException("Crawler command failed: " + commandResult.get()); } @@ -229,13 +262,16 @@ public final class AppCrawlTester { } /** Copys the step screenshots into test outputs for easier access. */ - private void collectCrawlStepScreenshots() { + private void collectCrawlStepScreenshots(boolean isUtpClient) { if (mOutput == null) { CLog.e("Output directory is not created yet. Skipping collecting step screenshots."); return; } - Path subDir = mOutput.resolve("app_firebase_test_lab"); + Path subDir = + isUtpClient + ? mOutput.resolve("output").resolve("artifacts") + : mOutput.resolve("app_firebase_test_lab"); if (!Files.exists(subDir)) { CLog.e( "The crawler output directory is not complete, skipping collecting step" @@ -279,81 +315,107 @@ public final class AppCrawlTester { } } - /** - * Generates a list of APK paths where the base.apk of split apk files are always on the first - * index if exists. - * - * <p>If the apk path is a single apk, then the apk is returned. If the apk path is a directory - * containing only one non-split apk file, the apk file is returned. If the apk path is a - * directory containing split apk files for one package, then the list of apks are returned and - * the base.apk sits on the first index. If the apk path does not contain any apk files, or - * multiple apk files without base.apk, then an IOException is thrown. - * - * @return A list of APK paths. - * @throws CrawlerException If failed to read the apk path or unexpected number of apk files are - * found under the path. - */ - private static List<Path> getApks(Path root) throws CrawlerException { - // The apk path points to a non-split apk file. - if (Files.isRegularFile(root)) { - if (!root.toString().endsWith(".apk")) { - throw new CrawlerException( - "The file on the given apk path is not an apk file: " + root); - } - return List.of(root); - } + @VisibleForTesting + String[] createUtpCrawlerRunCommand(TestInformation testInfo) throws CrawlerException { - List<Path> apks; - CLog.d("APK path = " + root); - try (Stream<Path> fileTree = Files.walk(root)) { - apks = - fileTree.filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".apk")) - .collect(Collectors.toList()); - } catch (IOException e) { - throw new CrawlerException("Failed to list apk files.", e); + Path bin = + mFileSystem.getPath( + AppCrawlTesterHostPreparer.getCrawlerBinPath( + mTestUtils.getTestInformation())); + ArrayList<String> cmd = new ArrayList<>(); + cmd.addAll( + Arrays.asList( + "java", + "-jar", + bin.resolve("utp-cli-android_deploy.jar").toString(), + "android", + "robo", + "--device-id", + testInfo.getDevice().getSerialNumber(), + "--app-id", + mPackageName, + "--controller-endpoint", + "PROD", + "--utp-binaries-dir", + bin.toString(), + "--key-file", + AppCrawlTesterHostPreparer.getCredentialPath( + mTestUtils.getTestInformation()) + .toString(), + "--base-crawler-apk", + bin.resolve("crawler_app.apk").toString(), + "--stub-crawler-apk", + bin.resolve("crawler_stubapp_androidx.apk").toString(), + "--tmp-dir", + mOutput.toString())); + + if (mTimeoutSec > 0) { + cmd.add("--crawler-flag"); + cmd.add("crawlDurationSec=" + Integer.toString(mTimeoutSec)); } - if (apks.isEmpty()) { - throw new CrawlerException("The apk directory does not contain any apk files"); - } + if (mUiAutomatorMode) { + cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-installed-on-device")); + } else { + Preconditions.checkNotNull( + mApkRoot, "Apk file path is required when not running in UIAutomator mode"); + + List<Path> apks; + try { + apks = + TestUtils.listApks(mApkRoot).stream() + .filter( + path -> + path.getFileName() + .toString() + .toLowerCase() + .endsWith(".apk")) + .collect(Collectors.toList()); + } catch (TestUtilsException e) { + throw new CrawlerException(e); + } - // The apk path contains a single non-split apk or the base.apk of a split-apk. - if (apks.size() == 1) { - return apks; + cmd.add("--apks-to-crawl"); + cmd.add(apks.stream().map(Path::toString).collect(Collectors.joining(","))); } - if (apks.stream().map(path -> path.getParent().toString()).distinct().count() != 1) { - throw new CrawlerException( - "Apk files are not all in the same folder: " - + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); + if (mRoboscriptFile != null) { + Assert.assertTrue( + "Please provide a valid roboscript file.", + Files.isRegularFile(mRoboscriptFile)); + cmd.add("--crawler-asset"); + cmd.add("robo.script=" + mRoboscriptFile.toString()); } - if (apks.stream().filter(path -> path.getFileName().toString().equals("base.apk")).count() - == 0) { - throw new CrawlerException( - "Multiple non-split apk files detected: " - + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); + if (mCrawlGuidanceProtoFile != null) { + Assert.assertTrue( + "Please provide a valid CrawlGuidance file.", + Files.isRegularFile(mCrawlGuidanceProtoFile)); + cmd.add("--crawl-guidance-proto-path"); + cmd.add(mCrawlGuidanceProtoFile.toString()); } - Collections.sort( - apks, - (first, second) -> first.getFileName().toString().equals("base.apk") ? -1 : 0); + if (mLoginConfigDir != null) { + RoboLoginConfigProvider configProvider = new RoboLoginConfigProvider(mLoginConfigDir); + cmd.addAll(configProvider.findConfigFor(mPackageName, true).getLoginArgs()); + } - return apks; + return cmd.toArray(new String[cmd.size()]); } @VisibleForTesting String[] createCrawlerRunCommand(TestInformation testInfo) throws CrawlerException { + Path bin = + mFileSystem.getPath( + AppCrawlTesterHostPreparer.getCrawlerBinPath( + mTestUtils.getTestInformation())); ArrayList<String> cmd = new ArrayList<>(); cmd.addAll( Arrays.asList( "java", "-jar", - AppCrawlTesterHostPreparer.getCrawlerBinPath(testInfo) - .resolve("crawl_launcher_deploy.jar") - .toString(), + bin.resolve("crawl_launcher_deploy.jar").toString(), "--android-sdk-path", AppCrawlTesterHostPreparer.getSdkPath(testInfo).toString(), "--device-serial-code", @@ -362,20 +424,35 @@ public final class AppCrawlTester { mOutput.toString(), "--key-store-file", // Using the publicly known default file name of the debug keystore. - AppCrawlTesterHostPreparer.getCrawlerBinPath(testInfo) - .resolve("debug.keystore") - .toString(), + bin.resolve("debug.keystore").toString(), "--key-store-password", // Using the publicly known default password of the debug keystore. "android")); + if (mCrawlControllerEndpoint != null && mCrawlControllerEndpoint.length() > 0) { + cmd.addAll(Arrays.asList("--endpoint", mCrawlControllerEndpoint)); + } + if (mUiAutomatorMode) { cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-package-name", mPackageName)); } else { Preconditions.checkNotNull( mApkRoot, "Apk file path is required when not running in UIAutomator mode"); - List<Path> apks = getApks(mApkRoot); + List<Path> apks; + try { + apks = + TestUtils.listApks(mApkRoot).stream() + .filter( + path -> + path.getFileName() + .toString() + .toLowerCase() + .endsWith(".apk")) + .collect(Collectors.toList()); + } catch (TestUtilsException e) { + throw new CrawlerException(e); + } cmd.add("--apk-file"); cmd.add(apks.get(0).toString()); @@ -386,6 +463,30 @@ public final class AppCrawlTester { } } + if (mTimeoutSec > 0) { + cmd.add("--timeout-sec"); + cmd.add(Integer.toString(mTimeoutSec)); + } + + if (mRoboscriptFile != null) { + Assert.assertTrue( + "Please provide a valid roboscript file.", + Files.isRegularFile(mRoboscriptFile)); + cmd.addAll(Arrays.asList("--robo-script-file", mRoboscriptFile.toString())); + } + + if (mCrawlGuidanceProtoFile != null) { + Assert.assertTrue( + "Please provide a valid CrawlGuidance file.", + Files.isRegularFile(mCrawlGuidanceProtoFile)); + cmd.addAll(Arrays.asList("--text-guide-file", mCrawlGuidanceProtoFile.toString())); + } + + if (mLoginConfigDir != null) { + RoboLoginConfigProvider configProvider = new RoboLoginConfigProvider(mLoginConfigDir); + cmd.addAll(configProvider.findConfigFor(mPackageName, false).getLoginArgs()); + } + return cmd.toArray(new String[cmd.size()]); } @@ -422,6 +523,16 @@ public final class AppCrawlTester { mUiAutomatorMode = uiAutomatorMode; } + /** Sets the value of the "timeout-sec" param for the crawler launcher. */ + public void setTimeoutSec(int timeoutSec) { + mTimeoutSec = timeoutSec; + } + + /** Sets the robo crawler controller endpoint (optional). */ + public void setCrawlControllerEndpoint(String crawlControllerEndpoint) { + mCrawlControllerEndpoint = crawlControllerEndpoint; + } + /** * Sets the apk file path. Required when not running in UIAutomator mode. * @@ -431,6 +542,27 @@ public final class AppCrawlTester { mApkRoot = apkRoot; } + /** + * Sets the option of the Roboscript file to be used by the crawler. Null can be passed to + * remove the reference to the file. + */ + public void setRoboscriptFile(@Nullable Path roboscriptFile) { + mRoboscriptFile = roboscriptFile; + } + + /** + * Sets the option of the CrawlGuidance file to be used by the crawler. Null can be passed to + * remove the reference to the file. + */ + public void setCrawlGuidanceProtoFile(@Nullable Path crawlGuidanceProtoFile) { + mCrawlGuidanceProtoFile = crawlGuidanceProtoFile; + } + + /** Sets the option of the directory that contains configuration for login. */ + public void setLoginConfigDir(@Nullable Path loginFilesDir) { + mLoginConfigDir = loginFilesDir; + } + @VisibleForTesting interface RunUtilProvider { IRunUtil get(); diff --git a/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java b/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java index 4be78f7..abb3de3 100644 --- a/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java +++ b/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java @@ -31,6 +31,8 @@ import com.google.common.io.MoreFiles; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -44,6 +46,7 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { @VisibleForTesting static final String SDK_TAR_OPTION = "sdk-tar"; @VisibleForTesting static final String CRAWLER_BIN_OPTION = "crawler-bin"; @VisibleForTesting static final String CREDENTIAL_JSON_OPTION = "credential-json"; + private final FileSystem mFileSystem; @Option( name = SDK_TAR_OPTION, @@ -66,12 +69,13 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { private RunUtilProvider mRunUtilProvider; public AppCrawlTesterHostPreparer() { - this(() -> new RunUtil()); + this(() -> new RunUtil(), FileSystems.getDefault()); } @VisibleForTesting - AppCrawlTesterHostPreparer(RunUtilProvider runUtilProvider) { + AppCrawlTesterHostPreparer(RunUtilProvider runUtilProvider, FileSystem fileSystem) { mRunUtilProvider = runUtilProvider; + mFileSystem = fileSystem; } /** @@ -80,7 +84,7 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { * @param testInfo The test info where the path is stored in. * @return The path to Android SDK; Null if not set. */ - public static Path getSdkPath(TestInformation testInfo) { + public static String getSdkPath(TestInformation testInfo) { return getPathFromBuildInfo(testInfo, SDK_PATH_KEY); } @@ -90,7 +94,7 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { * @param testInfo The test info where the path is stored in. * @return The path to the crawler binaries folder; Null if not set. */ - public static Path getCrawlerBinPath(TestInformation testInfo) { + public static String getCrawlerBinPath(TestInformation testInfo) { return getPathFromBuildInfo(testInfo, CRAWLER_BIN_PATH_KEY); } @@ -100,7 +104,7 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { * @param testInfo The test info where the path is stored in. * @return The path to the crawler credential json file. */ - public static Path getCredentialPath(TestInformation testInfo) { + public static String getCredentialPath(TestInformation testInfo) { return getPathFromBuildInfo(testInfo, CREDENTIAL_PATH_KEY); } @@ -114,9 +118,8 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { return testInfo.getBuildInfo().getBuildAttributes().get(IS_READY_KEY) != null; } - private static Path getPathFromBuildInfo(TestInformation testInfo, String key) { - String path = testInfo.getBuildInfo().getBuildAttributes().get(key); - return path == null ? null : Path.of(path); + private static String getPathFromBuildInfo(TestInformation testInfo, String key) { + return testInfo.getBuildInfo().getBuildAttributes().get(key); } @VisibleForTesting @@ -154,9 +157,13 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { setSdkPath(testInfo, sdkPath); + Path jar = mCrawlerBin.toPath().resolve("crawl_launcher_deploy.jar"); + if (!Files.exists(jar)) { + jar = mCrawlerBin.toPath().resolve("utp-cli-android_deploy.jar"); + } + // Make the crawler binary executable. - String chmodCmd = - "chmod 555 " + mCrawlerBin.toPath().resolve("crawl_launcher_deploy.jar").toString(); + String chmodCmd = "chmod 555 " + jar.toString(); CommandResult chmodRes = runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, chmodCmd.split(" ")); if (!chmodRes.getStatus().equals(CommandStatus.SUCCESS)) { throw new TargetSetupError( @@ -173,7 +180,7 @@ public final class AppCrawlTesterHostPreparer implements ITargetPreparer { @Override public void tearDown(TestInformation testInfo, Throwable e) { try { - cleanUp(getSdkPath(testInfo)); + cleanUp(mFileSystem.getPath(getSdkPath(testInfo))); } catch (IOException ioException) { CLog.e(ioException); } diff --git a/harness/src/main/java/com/android/csuite/core/DeviceUtils.java b/harness/src/main/java/com/android/csuite/core/DeviceUtils.java index 997f41b..3528fe8 100644 --- a/harness/src/main/java/com/android/csuite/core/DeviceUtils.java +++ b/harness/src/main/java/com/android/csuite/core/DeviceUtils.java @@ -30,15 +30,25 @@ import com.android.tradefed.util.IRunUtil; import com.android.tradefed.util.RunUtil; import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.InvalidProtocolBufferException; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.ListIterator; import java.util.Random; import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** A utility class that contains common methods to interact with the test device. */ public class DeviceUtils { @@ -56,12 +66,11 @@ public class DeviceUtils { "data_app_native_crash", "data_app_crash"); + private static final String VIDEO_PATH_ON_DEVICE_TEMPLATE = "/sdcard/screenrecord_%s.mp4"; + @VisibleForTesting - static final String LAUNCH_PACKAGE_COMMAND_TEMPLATE = - "monkey -p %s -c android.intent.category.LAUNCHER 1"; + static final int WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS = 10 * 1000; - private static final String VIDEO_PATH_ON_DEVICE_TEMPLATE = "/sdcard/screenrecord_%s.mp4"; - @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS = 10 * 1000; @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS = 500; private final ITestDevice mDevice; @@ -154,7 +163,8 @@ public class DeviceUtils { .get() .runCmdInBackground( String.format( - "adb -s %s shell screenrecord %s", + "adb -s %s shell screenrecord --time-limit 600" + + " %s", mDevice.getSerialNumber(), videoPath) .split("\\s+")); } catch (IOException ioException) { @@ -182,10 +192,10 @@ public class DeviceUtils { } if (mClock.currentTimeMillis() - start - > WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS) { + > WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS) { CLog.e( "Screenrecord did not start within %s milliseconds.", - WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS); + WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS); break; } } @@ -193,7 +203,26 @@ public class DeviceUtils { action.run(); } finally { if (recordingProcess != null) { - recordingProcess.destroy(); + mRunUtilProvider + .get() + .runTimedCmd( + WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS, + "kill", + "-SIGINT", + Long.toString(recordingProcess.pid())); + try { + recordingProcess.waitFor( + WAIT_FOR_SCREEN_RECORDING_START_STOP_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + recordingProcess.destroyForcibly(); + } + } + + CommandResult result = mDevice.executeShellV2Command("ls -sh " + videoPath); + if (result != null && result.getStatus() == CommandStatus.SUCCESS) { + CLog.d("Completed screenrecord %s, video size: %s", videoPath, result.getStdout()); } // Try to pull, handle, and delete the video file from the device anyway. handler.handleScreenRecordFile(mDevice.pullFile(videoPath)); @@ -259,14 +288,167 @@ public class DeviceUtils { */ public void launchPackage(String packageName) throws DeviceUtilsException, DeviceNotAvailableException { - CommandResult result = + CommandResult monkeyResult = mDevice.executeShellV2Command( - String.format(LAUNCH_PACKAGE_COMMAND_TEMPLATE, packageName)); - if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) { + String.format( + "monkey -p %s -c android.intent.category.LAUNCHER 1", packageName)); + if (monkeyResult.getStatus() == CommandStatus.SUCCESS) { + return; + } + CLog.w( + "Continuing to attempt using am command to launch the package %s after the monkey" + + " command failed: %s", + packageName, monkeyResult); + + CommandResult pmResult = + mDevice.executeShellV2Command(String.format("pm dump %s", packageName)); + if (pmResult.getStatus() != CommandStatus.SUCCESS || pmResult.getExitCode() != 0) { + if (isPackageInstalled(packageName)) { + throw new DeviceUtilsException( + String.format( + "The command to dump package info for %s failed: %s", + packageName, pmResult)); + } else { + throw new DeviceUtilsException( + String.format("Package %s is not installed on the device.", packageName)); + } + } + + String activity = getLaunchActivity(pmResult.getStdout()); + + CommandResult amResult = + mDevice.executeShellV2Command(String.format("am start -n %s", activity)); + if (amResult.getStatus() != CommandStatus.SUCCESS + || amResult.getExitCode() != 0 + || amResult.getStdout().contains("Error")) { + throw new DeviceUtilsException( + String.format( + "The command to start the package %s with activity %s failed: %s", + packageName, activity, amResult)); + } + } + + /** + * Extracts the launch activity from a pm dump output. + * + * <p>This method parses the package manager dump, extracts the activities and filters them + * based on the categories and actions defined in the Android framework. The activities are + * sorted based on these attributes, and the first activity that is either the main action or a + * launcher category is returned. + * + * @param pmDump the pm dump output to parse. + * @return a activity that can be used to launch the package. + * @throws DeviceUtilsException if the launch activity cannot be found in the + * dump. @VisibleForTesting + */ + @VisibleForTesting + String getLaunchActivity(String pmDump) throws DeviceUtilsException { + class Activity { + String mName; + int mIndex; + List<String> mActions = new ArrayList<>(); + List<String> mCategories = new ArrayList<>(); + } + + Pattern activityNamePattern = + Pattern.compile( + "([a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+" + + "\\/([a-zA-Z][a-zA-Z0-9_]*)*(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); + Pattern actionPattern = + Pattern.compile( + "Action:([^a-zA-Z0-9_\\.]*)([a-zA-Z][a-zA-Z0-9_]*" + + "(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); + Pattern categoryPattern = + Pattern.compile( + "Category:([^a-zA-Z0-9_\\.]*)([a-zA-Z][a-zA-Z0-9_]*" + + "(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); + + Matcher activityNameMatcher = activityNamePattern.matcher(pmDump); + + List<Activity> activities = new ArrayList<>(); + while (activityNameMatcher.find()) { + Activity activity = new Activity(); + activity.mName = activityNameMatcher.group(0); + activity.mIndex = activityNameMatcher.start(); + activities.add(activity); + } + + int endIdx = pmDump.length(); + ListIterator<Activity> iterator = activities.listIterator(activities.size()); + while (iterator.hasPrevious()) { + Activity activity = iterator.previous(); + Matcher actionMatcher = + actionPattern.matcher(pmDump.substring(activity.mIndex, endIdx)); + while (actionMatcher.find()) { + activity.mActions.add(actionMatcher.group(2)); + } + Matcher categoryMatcher = + categoryPattern.matcher(pmDump.substring(activity.mIndex, endIdx)); + while (categoryMatcher.find()) { + activity.mCategories.add(categoryMatcher.group(2)); + } + endIdx = activity.mIndex; + } + + String categoryDefault = "android.intent.category.DEFAULT"; + String categoryLauncher = "android.intent.category.LAUNCHER"; + String actionMain = "android.intent.action.MAIN"; + + class AndroidActivityComparator implements Comparator<Activity> { + @Override + public int compare(Activity a1, Activity a2) { + if (a1.mCategories.contains(categoryLauncher) + && !a2.mCategories.contains(categoryLauncher)) { + return -1; + } + if (!a1.mCategories.contains(categoryLauncher) + && a2.mCategories.contains(categoryLauncher)) { + return 1; + } + if (a1.mActions.contains(actionMain) && !a2.mActions.contains(actionMain)) { + return -1; + } + if (!a1.mActions.contains(actionMain) && a2.mActions.contains(actionMain)) { + return 1; + } + if (a1.mCategories.contains(categoryDefault) + && !a2.mCategories.contains(categoryDefault)) { + return -1; + } + if (!a1.mCategories.contains(categoryDefault) + && a2.mCategories.contains(categoryDefault)) { + return 1; + } + return Integer.compare(a2.mCategories.size(), a1.mCategories.size()); + } + } + + Collections.sort(activities, new AndroidActivityComparator()); + List<Activity> filteredActivities = + activities.stream() + .filter( + activity -> + activity.mActions.contains(actionMain) + || activity.mCategories.contains(categoryLauncher)) + .collect(Collectors.toList()); + if (filteredActivities.isEmpty()) { throw new DeviceUtilsException( String.format( - "The command to launch package %s failed: %s", packageName, result)); + "Cannot find an activity to launch the package. Number of activities" + + " parsed: %s", + activities.size())); + } + + Activity res = filteredActivities.get(0); + + if (!res.mCategories.contains(categoryLauncher)) { + CLog.d("Activity %s is not specified with a LAUNCHER category.", res.mName); + } + if (!res.mActions.contains(actionMain)) { + CLog.d("Activity %s is not specified with a MAIN action.", res.mName); } + + return res.mName; } /** @@ -334,6 +516,43 @@ public class DeviceUtils { } /** + * Checks whether a package is installed on the device. + * + * @param packageName The name of the package to check + * @return True if the package is installed on the device; false otherwise. + * @throws DeviceUtilsException If the adb shell command failed. + * @throws DeviceNotAvailableException If the device was lost. + */ + public boolean isPackageInstalled(String packageName) + throws DeviceUtilsException, DeviceNotAvailableException { + CommandResult commandResult = + executeShellCommandOrThrow( + String.format("pm list packages %s", packageName), + "Failed to execute pm command"); + + if (commandResult.getStdout() == null) { + throw new DeviceUtilsException( + String.format( + "Failed to get pm command output: %s", commandResult.getStdout())); + } + + return Arrays.asList(commandResult.getStdout().split("\\r?\\n")) + .contains(String.format("package:%s", packageName)); + } + + private CommandResult executeShellCommandOrThrow(String command, String failureMessage) + throws DeviceUtilsException, DeviceNotAvailableException { + CommandResult commandResult = mDevice.executeShellV2Command(command); + + if (commandResult.getStatus() != CommandStatus.SUCCESS) { + throw new DeviceUtilsException( + String.format("%s; Command result: %s", failureMessage, commandResult)); + } + + return commandResult; + } + + /** * Gets dropbox entries from the device filtered by the provided tags. * * @param tags Dropbox tags to query. @@ -356,15 +575,23 @@ public class DeviceUtils { String.format( "adb -s %s shell dumpsys dropbox --proto %s > %s", mDevice.getSerialNumber(), tag, dumpFile)); + if (res.getStatus() != CommandStatus.SUCCESS) { throw new IOException("Dropbox dump command failed: " + res); } - DropBoxManagerServiceDumpProto p = - DropBoxManagerServiceDumpProto.parseFrom(Files.readAllBytes(dumpFile)); + DropBoxManagerServiceDumpProto proto; + try { + proto = DropBoxManagerServiceDumpProto.parseFrom(Files.readAllBytes(dumpFile)); + } catch (InvalidProtocolBufferException e) { + // If dumping proto format is not supported such as in Android 10, the command will + // still succeed with exit code 0 and output strings instead of protobuf bytes, + // causing parse error. In this case we fallback to dumping dropbox --print option. + return getDropboxEntriesFromStdout(tags); + } Files.delete(dumpFile); - for (Entry entry : p.getEntriesList()) { + for (Entry entry : proto.getEntriesList()) { entries.add( new DropboxEntry(entry.getTimeMs(), tag, entry.getData().toStringUtf8())); } @@ -373,17 +600,100 @@ public class DeviceUtils { return entries; } + @VisibleForTesting + List<DropboxEntry> getDropboxEntriesFromStdout(Set<String> tags) throws IOException { + HashMap<String, DropboxEntry> entries = new HashMap<>(); + + // The first step is to read the entry names and timestamps from the --file dump option + // output because the --print dump option does not contain timestamps. + CommandResult res; + Path fileDumpFile = mTempFileSupplier.get(); + res = + mRunUtilProvider + .get() + .runTimedCmd( + 6000, + "sh", + "-c", + String.format( + "adb -s %s shell dumpsys dropbox --file > %s", + mDevice.getSerialNumber(), fileDumpFile)); + if (res.getStatus() != CommandStatus.SUCCESS) { + throw new IOException("Dropbox dump command failed: " + res); + } + + String lastEntryName = null; + for (String line : Files.readAllLines(fileDumpFile)) { + if (DropboxEntry.isDropboxEntryName(line)) { + lastEntryName = line.trim(); + entries.put(lastEntryName, DropboxEntry.fromEntryName(line)); + } else if (DropboxEntry.isDropboxFilePath(line) && lastEntryName != null) { + entries.get(lastEntryName).parseTimeFromFilePath(line); + } + } + Files.delete(fileDumpFile); + + // Then we get the entry data from the --print dump output. Entry names parsed from the + // --print dump output are verified against the entry names from the --file dump output to + // ensure correctness. + Path printDumpFile = mTempFileSupplier.get(); + res = + mRunUtilProvider + .get() + .runTimedCmd( + 6000, + "sh", + "-c", + String.format( + "adb -s %s shell dumpsys dropbox --print > %s", + mDevice.getSerialNumber(), printDumpFile)); + if (res.getStatus() != CommandStatus.SUCCESS) { + throw new IOException("Dropbox dump command failed: " + res); + } + + lastEntryName = null; + for (String line : Files.readAllLines(printDumpFile)) { + if (DropboxEntry.isDropboxEntryName(line)) { + lastEntryName = line.trim(); + } + + if (lastEntryName != null && entries.containsKey(lastEntryName)) { + entries.get(lastEntryName).addData(line); + entries.get(lastEntryName).addData("\n"); + } + } + Files.delete(printDumpFile); + + return entries.values().stream() + .filter(entry -> tags.contains(entry.getTag())) + .collect(Collectors.toList()); + } + /** A class that stores the information of a dropbox entry. */ public static final class DropboxEntry { - private final long mTime; - private final String mTag; - private final String mData; + private long mTime; + private String mTag; + private final StringBuilder mData = new StringBuilder(); + private static final Pattern ENTRY_NAME_PATTERN = + Pattern.compile( + "\\d{4}\\-\\d{2}\\-\\d{2} \\d{2}:\\d{2}:\\d{2} .+ \\(.+, [0-9]+ .+\\)"); + private static final Pattern DATE_PATTERN = + Pattern.compile("\\d{4}\\-\\d{2}\\-\\d{2} \\d{2}:\\d{2}:\\d{2}"); + private static final Pattern FILE_NAME_PATTERN = Pattern.compile(" +/.+@[0-9]+\\..+"); /** Returns the entrt's time stamp on device. */ public long getTime() { return mTime; } + private void addData(String data) { + mData.append(data); + } + + private void parseTimeFromFilePath(String input) { + mTime = Long.parseLong(input.substring(input.indexOf('@') + 1, input.indexOf('.'))); + } + /** Returns the entrt's tag. */ public String getTag() { return mTag; @@ -391,14 +701,36 @@ public class DeviceUtils { /** Returns the entrt's data. */ public String getData() { - return mData; + return mData.toString(); } @VisibleForTesting DropboxEntry(long time, String tag, String data) { mTime = time; mTag = tag; - mData = data; + addData(data); + } + + private DropboxEntry() { + // Intentionally left blank; + } + + private static DropboxEntry fromEntryName(String name) { + DropboxEntry entry = new DropboxEntry(); + Matcher matcher = DATE_PATTERN.matcher(name); + if (!matcher.find()) { + throw new RuntimeException("Unexpected entry name: " + name); + } + entry.mTag = name.trim().substring(matcher.group().length()).trim().split(" ")[0]; + return entry; + } + + private static boolean isDropboxEntryName(String input) { + return ENTRY_NAME_PATTERN.matcher(input).find(); + } + + private static boolean isDropboxFilePath(String input) { + return FILE_NAME_PATTERN.matcher(input).find(); } } diff --git a/harness/src/main/java/com/android/csuite/core/RoboLoginConfigProvider.java b/harness/src/main/java/com/android/csuite/core/RoboLoginConfigProvider.java new file mode 100644 index 0000000..2cfb4d0 --- /dev/null +++ b/harness/src/main/java/com/android/csuite/core/RoboLoginConfigProvider.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.csuite.core; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; + +import org.junit.Assert; + +import java.nio.file.Files; +import java.nio.file.Path; + +public final class RoboLoginConfigProvider { + @VisibleForTesting static final String ROBOSCRIPT_FILE_SUFFIX = ".roboscript"; + @VisibleForTesting static final String CRAWL_GUIDANCE_FILE_SUFFIX = "_cg.txt"; + private static final String ROBOSCRIPT_CMD_FLAG = "--robo-script-file"; + private static final String CRAWL_GUIDANCE_CMD_FLAG = "--text-guide-file"; + + private final Path mLoginFilesDir; + + public RoboLoginConfigProvider(Path loginFilesDir) { + Assert.assertTrue( + "Please provide a valid directory that contains crawler login files.", + Files.isDirectory(loginFilesDir)); + this.mLoginFilesDir = loginFilesDir; + } + + /** + * Finds the config file to use from the given directory for the corresponding app package and + * returns the {@link RoboLoginConfig} that contains the resulting login arguments. The + * directory should contain only one config file per package name. If both Roboscript and + * CrawlGuidance files are present, only the Roboscript file will be used." + */ + public RoboLoginConfig findConfigFor(String packageName, boolean isUtpClient) { + Path crawlGuidanceFile = mLoginFilesDir.resolve(packageName + CRAWL_GUIDANCE_FILE_SUFFIX); + Path roboScriptFile = mLoginFilesDir.resolve(packageName + ROBOSCRIPT_FILE_SUFFIX); + + if (Files.exists(roboScriptFile) && !isUtpClient) { + return new RoboLoginConfig( + ImmutableList.of(ROBOSCRIPT_CMD_FLAG, roboScriptFile.toString())); + } + + if (Files.exists(crawlGuidanceFile) && !isUtpClient) { + return new RoboLoginConfig( + ImmutableList.of(CRAWL_GUIDANCE_CMD_FLAG, crawlGuidanceFile.toString())); + } + + if (Files.exists(roboScriptFile) && isUtpClient) { + return new RoboLoginConfig( + ImmutableList.of( + "--crawler-asset", "robo.script=" + roboScriptFile.toString())); + } + + if (Files.exists(crawlGuidanceFile) && isUtpClient) { + return new RoboLoginConfig( + ImmutableList.of("--crawl-guidance-proto-path", crawlGuidanceFile.toString())); + } + + return new RoboLoginConfig(ImmutableList.of()); + } + + /* + * A class returned by RoboLoginConfigProvider that contains the login arguments + * to be passed to the crawler. + */ + public static final class RoboLoginConfig { + private final ImmutableList<String> mLoginArgs; + + public RoboLoginConfig(ImmutableList<String> loginArgs) { + this.mLoginArgs = loginArgs; + } + + /* Returns the login arguments for this config which can be passed to the crawler. */ + public ImmutableList<String> getLoginArgs() { + return mLoginArgs; + } + } +} diff --git a/harness/src/main/java/com/android/csuite/core/TestUtils.java b/harness/src/main/java/com/android/csuite/core/TestUtils.java index 7bae9df..74b5888 100644 --- a/harness/src/main/java/com/android/csuite/core/TestUtils.java +++ b/harness/src/main/java/com/android/csuite/core/TestUtils.java @@ -26,6 +26,7 @@ import com.android.tradefed.result.FileInputStreamSource; import com.android.tradefed.result.InputStreamSource; import com.android.tradefed.result.LogDataType; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.util.ZipUtil; import com.google.common.annotations.VisibleForTesting; @@ -37,6 +38,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -47,6 +50,19 @@ public class TestUtils { private final TestArtifactReceiver mTestArtifactReceiver; private final DeviceUtils mDeviceUtils; private static final int MAX_CRASH_SNIPPET_LINES = 60; + // Pattern for finding a package name following one of the tags such as "Process:" or + // "Package:". + private static final Pattern DROPBOX_PACKAGE_NAME_PATTERN = + Pattern.compile( + "(Process|Cmdline|Package|Cmd line):(" + + " *)([a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)+)"); + + public enum TakeEffectWhen { + NEVER, + ON_FAIL, + ON_PASS, + ALWAYS, + } public static TestUtils getInstance(TestInformation testInformation, TestLogData testLogData) { return new TestUtils( @@ -116,6 +132,37 @@ public class TestUtils { } /** + * Saves test APK files when conditions on the test result is met. + * + * @param when Conditions to save the apks based on the test result. + * @param testPassed The test result. + * @param prefix Output file name prefix + * @param apks A list of files that can be files, directories, or a mix of both. + * @return true if apk files are saved as artifacts. False otherwise. + */ + public boolean saveApks( + TakeEffectWhen when, boolean testPassed, String prefix, List<File> apks) { + if (apks.isEmpty() || when == TakeEffectWhen.NEVER) { + return false; + } + + if ((when == TakeEffectWhen.ON_FAIL && testPassed) + || (when == TakeEffectWhen.ON_PASS && !testPassed)) { + return false; + } + + try { + File outputZip = ZipUtil.createZip(apks); + getTestArtifactReceiver().addTestArtifact(prefix + "-apks", LogDataType.ZIP, outputZip); + return true; + } catch (IOException e) { + CLog.e("Failed to zip the apks: " + e); + } + + return false; + } + + /** * Collect the GMS version name and version code, and save them as test result artifacts. * * @param prefix The file name prefix. @@ -195,7 +242,10 @@ public class TestUtils { List<DropboxEntry> entries = mDeviceUtils.getDropboxEntries(DeviceUtils.DROPBOX_APP_CRASH_TAGS).stream() .filter(entry -> (entry.getTime() >= startTimeOnDevice.get())) - .filter(entry -> entry.getData().contains(packageName)) + .filter( + entry -> + isDropboxEntryFromPackageProcess( + entry.getData(), packageName)) .collect(Collectors.toList()); if (entries.size() == 0) { @@ -228,17 +278,47 @@ public class TestUtils { return truncatedText; } + @VisibleForTesting + boolean isDropboxEntryFromPackageProcess(String entryData, String packageName) { + Matcher m = DROPBOX_PACKAGE_NAME_PATTERN.matcher(entryData); + + boolean matched = false; + while (m.find()) { + matched = true; + if (m.group(3).equals(packageName)) { + return true; + } + } + + if (matched) { + return false; + } + + // If the process name is not identified, fall back to checking if the package name is + // present in the entry. This is because the process name detection logic above does not + // guarantee to identify the process name. + return Pattern.compile( + String.format( + // Pattern for checking whether a given package name exists. + "(.*(?:[^a-zA-Z0-9_\\.]+)|^)%s((?:[^a-zA-Z0-9_\\.]+).*|$)", + packageName.replaceAll("\\.", "\\\\."))) + .matcher(entryData) + .find(); + } + /** * Generates a list of APK paths where the base.apk of split apk files are always on the first * index if exists. * - * <p>If the apk path is a single apk, then the apk is returned. If the apk path is a directory - * containing only one non-split apk file, the apk file is returned. If the apk path is a - * directory containing split apk files for one package, then the list of apks are returned and - * the base.apk sits on the first index. If the apk path does not contain any apk files, or - * multiple apk files without base.apk, then an IOException is thrown. + * <p>If the input path points to a single apk file, then the same path is returned. If the + * input path is a directory containing only one non-split apk file, the apk file path is + * returned. If the apk path is a directory containing split apk files for one package, then the + * list of apks are returned and the base.apk sits on the first index. If the path contains obb + * files, then they will be included at the end of the returned path list. If the apk path does + * not contain any apk files, or multiple apk files without base.apk, then an IOException is + * thrown. * - * @return A list of APK paths. + * @return A list of APK paths with OBB files if available. * @throws TestUtilsException If failed to read the apk path or unexpected number of apk files * are found under the path. */ @@ -252,44 +332,80 @@ public class TestUtils { return List.of(root); } - List<Path> apks; + List<Path> apksAndObbs; CLog.d("APK path = " + root); try (Stream<Path> fileTree = Files.walk(root)) { - apks = + apksAndObbs = fileTree.filter(Files::isRegularFile) - .filter(path -> path.getFileName().toString().endsWith(".apk")) + .filter( + path -> + path.getFileName() + .toString() + .toLowerCase() + .endsWith(".apk") + || path.getFileName() + .toString() + .toLowerCase() + .endsWith(".obb")) .collect(Collectors.toList()); } catch (IOException e) { throw new TestUtilsException("Failed to list apk files.", e); } - if (apks.isEmpty()) { - throw new TestUtilsException("The apk directory does not contain any apk files"); - } + List<Path> apkFiles = + apksAndObbs.stream() + .filter(path -> path.getFileName().toString().endsWith(".apk")) + .collect(Collectors.toList()); - // The apk path contains a single non-split apk or the base.apk of a split-apk. - if (apks.size() == 1) { - return apks; + if (apkFiles.isEmpty()) { + throw new TestUtilsException( + "Empty APK directory. Cannot find any APK files under " + root); } - if (apks.stream().map(path -> path.getParent().toString()).distinct().count() != 1) { + if (apkFiles.stream().map(path -> path.getParent().toString()).distinct().count() != 1) { throw new TestUtilsException( "Apk files are not all in the same folder: " - + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); + + Arrays.deepToString( + apksAndObbs.toArray(new Path[apksAndObbs.size()]))); + } + + if (apkFiles.size() > 1 + && apkFiles.stream() + .filter(path -> path.getFileName().toString().equals("base.apk")) + .count() + == 0) { + throw new TestUtilsException( + "Base apk is not found: " + + Arrays.deepToString( + apksAndObbs.toArray(new Path[apksAndObbs.size()]))); } - if (apks.stream().filter(path -> path.getFileName().toString().equals("base.apk")).count() - == 0) { + if (apksAndObbs.stream() + .filter( + path -> + path.getFileName().toString().endsWith(".obb") + && path.getFileName().toString().startsWith("main")) + .count() + > 1) { throw new TestUtilsException( - "Multiple non-split apk files detected: " - + Arrays.deepToString(apks.toArray(new Path[apks.size()]))); + "Multiple main obb files are found: " + + Arrays.deepToString( + apksAndObbs.toArray(new Path[apksAndObbs.size()]))); } Collections.sort( - apks, - (first, second) -> first.getFileName().toString().equals("base.apk") ? -1 : 0); + apksAndObbs, + (first, second) -> { + if (first.getFileName().toString().equals("base.apk")) { + return -1; + } else if (first.getFileName().toString().toLowerCase().endsWith(".obb")) { + return 1; + } else { + return first.getFileName().compareTo(second.getFileName()); + } + }); - return apks; + return apksAndObbs; } /** Returns the test information. */ diff --git a/harness/src/main/resources/config/csuite-base.xml b/harness/src/main/resources/config/csuite-base.xml index c6a5553..ac8e426 100644 --- a/harness/src/main/resources/config/csuite-base.xml +++ b/harness/src/main/resources/config/csuite-base.xml @@ -32,4 +32,10 @@ <object type="MODULE_INFO_PROVIDER" class="com.android.csuite.core.PackagesFileModuleInfoProvider" /> <object type="TEMPLATE_MAPPING_PROVIDER" class="com.android.csuite.core.CommandLineTemplateMappingProvider" /> <object type="TEMPLATE_MAPPING_PROVIDER" class="com.android.csuite.core.FileBasedTemplateMappingProvider" /> + + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="settings put secure immersive_mode_confirmations confirmed" /> + </target_preparer> </configuration> diff --git a/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java b/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java index 99962ab..c26b9d7 100644 --- a/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java +++ b/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java @@ -16,6 +16,8 @@ package com.android.csuite.core; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; import com.android.csuite.core.ApkInstaller.ApkInstallerException; import com.android.tradefed.util.CommandResult; @@ -27,12 +29,15 @@ import com.google.common.jimfs.Jimfs; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; @RunWith(JUnit4.class) public final class ApkInstallerTest { @@ -76,6 +81,48 @@ public final class ApkInstallerTest { sut.install(root); } + @Test + public void install_parsePackageNameFailed_throwsException() throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Files.createFile(root.resolve("base.apk")); + IRunUtil runUtil = Mockito.mock(IRunUtil.class); + Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any())) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + ApkInstaller sut = + new ApkInstaller( + "serial", + runUtil, + apk -> { + throw new IOException(); + }); + + assertThrows(ApkInstallerException.class, () -> sut.install(root)); + } + + @Test + public void install_obbExists_installObb() throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Path apkPath = root.resolve("base.apk"); + Files.createFile(apkPath); + Path obbPath = root.resolve("main.obb"); + Files.createFile(obbPath); + IRunUtil runUtil = Mockito.mock(IRunUtil.class); + Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any())) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + ApkInstaller sut = new ApkInstaller("serial", runUtil, apk -> "package.name"); + + sut.install(root); + + ArgumentCaptor<String> cmdCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(runUtil, Mockito.atLeastOnce()).runTimedCmd(anyLong(), cmdCaptor.capture()); + List<String> capturedArgs = cmdCaptor.getAllValues(); + assertTrue(capturedArgs.stream().anyMatch(arg -> arg.contains("push"))); + assertTrue(capturedArgs.stream().anyMatch(arg -> arg.contains("rm"))); + assertTrue(capturedArgs.stream().anyMatch(arg -> arg.contains("mkdir"))); + } + private static CommandResult createSuccessfulCommandResultWithStdout(String stdout) { CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS); commandResult.setExitCode(0); diff --git a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java index 87762ea..7a060ea 100644 --- a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java +++ b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java @@ -56,14 +56,14 @@ public final class AppCrawlTesterHostPreparerTest { Path path = Path.of("some"); AppCrawlTesterHostPreparer.setSdkPath(mTestInfo, path); - Path result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo); - assertThat(result.toString()).isEqualTo(path.toString()); + assertThat(result).isEqualTo(path.toString()); } @Test public void getSdkPath_wasNotSet_returnsNull() { - Path result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo); assertNull(result); } @@ -73,14 +73,14 @@ public final class AppCrawlTesterHostPreparerTest { Path path = Path.of("some"); AppCrawlTesterHostPreparer.setCrawlerBinPath(mTestInfo, path); - Path result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo); - assertThat(result.toString()).isEqualTo(path.toString()); + assertThat(result).isEqualTo(path.toString()); } @Test public void getCrawlerBinPath_wasNotSet_returnsNull() { - Path result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo); assertNull(result); } @@ -90,14 +90,14 @@ public final class AppCrawlTesterHostPreparerTest { Path path = Path.of("some"); AppCrawlTesterHostPreparer.setCredentialPath(mTestInfo, path); - Path result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo); - assertThat(result.toString()).isEqualTo(path.toString()); + assertThat(result).isEqualTo(path.toString()); } @Test public void getCredentialPath_wasNotSet_returnsNull() { - Path result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo); + String result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo); assertNull(result); } @@ -143,7 +143,8 @@ public final class AppCrawlTesterHostPreparerTest { } private AppCrawlTesterHostPreparer createTestSubject() throws Exception { - AppCrawlTesterHostPreparer suj = new AppCrawlTesterHostPreparer(() -> mRunUtil); + AppCrawlTesterHostPreparer suj = + new AppCrawlTesterHostPreparer(() -> mRunUtil, mFileSystem); OptionSetter optionSetter = new OptionSetter(suj); optionSetter.setOptionValue( AppCrawlTesterHostPreparer.SDK_TAR_OPTION, diff --git a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java index dc9132b..67ff1cf 100644 --- a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java +++ b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java @@ -15,6 +15,9 @@ */ package com.android.csuite.core; +import static com.android.csuite.core.RoboLoginConfigProvider.CRAWL_GUIDANCE_FILE_SUFFIX; +import static com.android.csuite.core.RoboLoginConfigProvider.ROBOSCRIPT_FILE_SUFFIX; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; @@ -52,6 +55,7 @@ import java.util.Arrays; @RunWith(JUnit4.class) public final class AppCrawlTesterTest { + private static final String PACKAGE_NAME = "package.name"; private final TestArtifactReceiver mTestArtifactReceiver = Mockito.mock(TestArtifactReceiver.class); private final FileSystem mFileSystem = @@ -78,6 +82,30 @@ public final class AppCrawlTesterTest { } @Test + public void start_roboscriptDirectoryProvided_throws() throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + suj.setUiAutomatorMode(true); + Path roboDir = mFileSystem.getPath("robo"); + Files.createDirectories(roboDir); + + suj.setRoboscriptFile(roboDir); + + assertThrows(AssertionError.class, () -> suj.start()); + } + + @Test + public void start_crawlGuidanceDirectoryProvided_throws() throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + suj.setUiAutomatorMode(true); + Path crawlGuidanceDir = mFileSystem.getPath("crawlguide"); + Files.createDirectories(crawlGuidanceDir); + + suj.setCrawlGuidanceProtoFile(crawlGuidanceDir); + + assertThrows(AssertionError.class, () -> suj.start()); + } + + @Test public void startAndAssertAppNoCrash_noCrashDetected_doesNotThrow() throws Exception { AppCrawlTester suj = createPreparedTestSubject(); suj.setApkPath(createApkPathWithSplitApks()); @@ -201,14 +229,13 @@ public final class AppCrawlTesterTest { } @Test - public void start_credentialIsProvidedToCrawler() throws Exception { + public void start_sdkPathIsProvidedToCrawler() throws Exception { AppCrawlTester suj = createPreparedTestSubject(); suj.setApkPath(createApkPathWithSplitApks()); suj.start(); - Mockito.verify(mRunUtil) - .setEnvVariable(Mockito.eq("GOOGLE_APPLICATION_CREDENTIALS"), Mockito.anyString()); + Mockito.verify(mRunUtil).setEnvVariable(Mockito.eq("ANDROID_SDK"), Mockito.anyString()); } @Test @@ -334,7 +361,7 @@ public final class AppCrawlTesterTest { } @Test - public void createCrawlerRunCommand_containsRequiredCrawlerParams() throws Exception { + public void createUtpCrawlerRunCommand_containsRequiredCrawlerParams() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); Files.createFile(apkRoot.resolve("some.apk")); @@ -342,16 +369,125 @@ public final class AppCrawlTesterTest { suj.setApkPath(apkRoot); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().contains("android"); + assertThat(result).asList().contains("robo"); + assertThat(result).asList().contains("--device-id"); + assertThat(result).asList().contains("--app-id"); + assertThat(result).asList().contains("--utp-binaries-dir"); + assertThat(result).asList().contains("--key-file"); + assertThat(result).asList().contains("--base-crawler-apk"); + assertThat(result).asList().contains("--stub-crawler-apk"); + } + + @Test + public void createUtpCrawlerRunCommand_containsRoboscriptFileWhenProvided() throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path roboDir = mFileSystem.getPath("/robo"); + Files.createDirectory(roboDir); + Path roboFile = Files.createFile(roboDir.resolve("app.roboscript")); + suj.setUiAutomatorMode(true); + suj.setRoboscriptFile(roboFile); + suj.start(); + + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().contains("--crawler-asset"); + assertThat(result).asList().contains("robo.script=" + roboFile.toString()); + } + + @Test + public void createUtpCrawlerRunCommand_containsCrawlGuidanceFileWhenProvided() + throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path crawlGuideDir = mFileSystem.getPath("/cg"); + Files.createDirectory(crawlGuideDir); + Path crawlGuideFile = Files.createFile(crawlGuideDir.resolve("app.crawlguide")); + + suj.setUiAutomatorMode(true); + suj.setCrawlGuidanceProtoFile(crawlGuideFile); + suj.start(); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); - assertThat(result).asList().contains("--key-store-file"); - assertThat(result).asList().contains("--key-store-password"); - assertThat(result).asList().contains("--device-serial-code"); - assertThat(result).asList().contains("--apk-file"); + assertThat(result).asList().contains("--crawl-guidance-proto-path"); } @Test - public void createCrawlerRunCommand_crawlerIsExecutedThroughJavaJar() throws Exception { + public void createUtpCrawlerRunCommand_loginDirContainsOnlyCrawlGuidanceFile_addsFilePath() + throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path loginFilesDir = mFileSystem.getPath("/login"); + Files.createDirectory(loginFilesDir); + Path crawlGuideFile = + Files.createFile(loginFilesDir.resolve(PACKAGE_NAME + CRAWL_GUIDANCE_FILE_SUFFIX)); + + suj.setUiAutomatorMode(true); + suj.setLoginConfigDir(loginFilesDir); + suj.start(); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().contains("--crawl-guidance-proto-path"); + assertThat(result).asList().contains(crawlGuideFile.toString()); + } + + @Test + public void createUtpCrawlerRunCommand_loginDirContainsOnlyRoboscriptFile_addsFilePath() + throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path loginFilesDir = mFileSystem.getPath("/login"); + Files.createDirectory(loginFilesDir); + Path roboscriptFile = + Files.createFile(loginFilesDir.resolve(PACKAGE_NAME + ROBOSCRIPT_FILE_SUFFIX)); + + suj.setUiAutomatorMode(true); + suj.setLoginConfigDir(loginFilesDir); + suj.start(); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().contains("--crawler-asset"); + assertThat(result).asList().contains("robo.script=" + roboscriptFile.toString()); + } + + @Test + public void + createUtpCrawlerRunCommand_loginDirContainsMultipleLoginFiles_addsRoboscriptFilePath() + throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path loginFilesDir = mFileSystem.getPath("/login"); + Files.createDirectory(loginFilesDir); + Path roboscriptFile = + Files.createFile(loginFilesDir.resolve(PACKAGE_NAME + ROBOSCRIPT_FILE_SUFFIX)); + Path crawlGuideFile = + Files.createFile(loginFilesDir.resolve(PACKAGE_NAME + CRAWL_GUIDANCE_FILE_SUFFIX)); + + suj.setUiAutomatorMode(true); + suj.setLoginConfigDir(loginFilesDir); + suj.start(); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().contains("--crawler-asset"); + assertThat(result).asList().contains("robo.script=" + roboscriptFile.toString()); + assertThat(result).asList().doesNotContain(crawlGuideFile.toString()); + } + + @Test + public void createUtpCrawlerRunCommand_loginDirEmpty_doesNotAddFlag() throws Exception { + AppCrawlTester suj = createPreparedTestSubject(); + Path loginFilesDir = mFileSystem.getPath("/login"); + Files.createDirectory(loginFilesDir); + + suj.setUiAutomatorMode(true); + suj.setLoginConfigDir(loginFilesDir); + suj.start(); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); + + assertThat(result).asList().doesNotContain("--crawler-asset"); + assertThat(result).asList().doesNotContain("--crawl-guidance-proto-path"); + } + + @Test + public void createUtpCrawlerRunCommand_crawlerIsExecutedThroughJavaJar() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); Files.createFile(apkRoot.resolve("some.apk")); @@ -359,14 +495,14 @@ public final class AppCrawlTesterTest { suj.setApkPath(apkRoot); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); assertThat(result).asList().contains("java"); assertThat(result).asList().contains("-jar"); } @Test - public void createCrawlerRunCommand_splitApksProvided_useApkFileAndSplitApkFilesParams() + public void createUtpCrawlerRunCommand_splitApksProvided_useApkFileAndSplitApkFilesParams() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); @@ -377,19 +513,16 @@ public final class AppCrawlTesterTest { suj.setApkPath(apkRoot); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); - assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apk-file")).count()) + assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apks-to-crawl")).count()) + .isEqualTo(1); + assertThat(Arrays.asList(result).stream().filter(s -> s.contains("config1.apk")).count()) .isEqualTo(1); - assertThat( - Arrays.asList(result).stream() - .filter(s -> s.equals("--split-apk-files")) - .count()) - .isEqualTo(2); } @Test - public void createCrawlerRunCommand_uiAutomatorModeEnabled_doesNotContainApks() + public void createUtpCrawlerRunCommand_uiAutomatorModeEnabled_doesNotContainApks() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); @@ -401,19 +534,14 @@ public final class AppCrawlTesterTest { suj.setUiAutomatorMode(true); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); - assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apk-file")).count()) - .isEqualTo(0); - assertThat( - Arrays.asList(result).stream() - .filter(s -> s.equals("--split-apk-files")) - .count()) + assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apks-to-crawl")).count()) .isEqualTo(0); } @Test - public void createCrawlerRunCommand_uiAutomatorModeEnabled_containsUiAutomatorParam() + public void createUtpCrawlerRunCommand_uiAutomatorModeEnabled_containsUiAutomatorParam() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); @@ -425,7 +553,7 @@ public final class AppCrawlTesterTest { suj.setUiAutomatorMode(true); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); assertThat( Arrays.asList(result).stream() @@ -434,13 +562,13 @@ public final class AppCrawlTesterTest { .isEqualTo(1); assertThat( Arrays.asList(result).stream() - .filter(s -> s.equals("--app-package-name")) + .filter(s -> s.equals("--app-installed-on-device")) .count()) .isEqualTo(1); } @Test - public void createCrawlerRunCommand_doesNotContainNullOrEmptyStrings() throws Exception { + public void createUtpCrawlerRunCommand_doesNotContainNullOrEmptyStrings() throws Exception { Path apkRoot = mFileSystem.getPath("apk"); Files.createDirectories(apkRoot); Files.createFile(apkRoot.resolve("base.apk")); @@ -450,10 +578,9 @@ public final class AppCrawlTesterTest { suj.setApkPath(apkRoot); suj.start(); - String[] result = suj.createCrawlerRunCommand(mTestInfo); + String[] result = suj.createUtpCrawlerRunCommand(mTestInfo); assertThat(Arrays.asList(result).stream().filter(s -> s == null).count()).isEqualTo(0); - assertThat(Arrays.asList(result).stream().map(String::trim).filter(String::isEmpty).count()) .isEqualTo(0); } @@ -463,17 +590,20 @@ public final class AppCrawlTesterTest { IRunUtil runUtil = Mockito.mock(IRunUtil.class); Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any())) .thenReturn(createSuccessfulCommandResult()); - AppCrawlTesterHostPreparer preparer = new AppCrawlTesterHostPreparer(() -> runUtil); + AppCrawlTesterHostPreparer preparer = + new AppCrawlTesterHostPreparer(() -> runUtil, mFileSystem); OptionSetter optionSetter = new OptionSetter(preparer); + + Path bin = Files.createDirectories(mFileSystem.getPath("/bin")); + Files.createFile(bin.resolve("utp-cli-android_deploy.jar")); + optionSetter.setOptionValue( AppCrawlTesterHostPreparer.SDK_TAR_OPTION, - Files.createDirectories(mFileSystem.getPath("sdk")).toString()); - optionSetter.setOptionValue( - AppCrawlTesterHostPreparer.CRAWLER_BIN_OPTION, - Files.createDirectories(mFileSystem.getPath("bin")).toString()); + Files.createDirectories(mFileSystem.getPath("/sdk")).toString()); + optionSetter.setOptionValue(AppCrawlTesterHostPreparer.CRAWLER_BIN_OPTION, bin.toString()); optionSetter.setOptionValue( AppCrawlTesterHostPreparer.CREDENTIAL_JSON_OPTION, - Files.createDirectories(mFileSystem.getPath("cred.json")).toString()); + Files.createDirectories(mFileSystem.getPath("/cred.json")).toString()); preparer.setUp(mTestInfo); } @@ -481,15 +611,14 @@ public final class AppCrawlTesterTest { Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any())) .thenReturn(createSuccessfulCommandResult()); Mockito.when(mDevice.getSerialNumber()).thenReturn("serial"); - return new AppCrawlTester("package.name", mTestUtils, () -> mRunUtil); + return new AppCrawlTester(PACKAGE_NAME, mTestUtils, () -> mRunUtil, mFileSystem); } - private AppCrawlTester createPreparedTestSubject() throws IOException, ConfigurationException, TargetSetupError { simulatePreparerWasExecutedSuccessfully(); Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any())) .thenReturn(createSuccessfulCommandResult()); - return new AppCrawlTester("package.name", mTestUtils, () -> mRunUtil); + return new AppCrawlTester(PACKAGE_NAME, mTestUtils, () -> mRunUtil, mFileSystem); } private TestUtils createTestUtils() throws DeviceNotAvailableException { diff --git a/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java b/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java index 6d6bdec..b3ea806 100644 --- a/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java +++ b/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java @@ -17,7 +17,9 @@ package com.android.csuite.core; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import android.service.dropbox.DropBoxManagerServiceDumpProto; @@ -45,6 +47,7 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -57,24 +60,289 @@ public final class DeviceUtilsTest { Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix()); @Test - public void launchPackage_packageDoesNotExist_returnsFalse() throws Exception { - when(mDevice.executeShellV2Command(Mockito.startsWith("monkey -p"))) + public void isPackageInstalled_packageIsInstalled_returnsTrue() throws Exception { + String packageName = "package.name"; + when(mDevice.executeShellV2Command(Mockito.startsWith("pm list packages"))) + .thenReturn( + createSuccessfulCommandResultWithStdout("\npackage:" + packageName + "\n")); + DeviceUtils sut = createSubjectUnderTest(); + + boolean res = sut.isPackageInstalled(packageName); + + assertTrue(res); + } + + @Test + public void isPackageInstalled_packageIsNotInstalled_returnsFalse() throws Exception { + String packageName = "package.name"; + when(mDevice.executeShellV2Command(Mockito.startsWith("pm list packages"))) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + DeviceUtils sut = createSubjectUnderTest(); + + boolean res = sut.isPackageInstalled(packageName); + + assertFalse(res); + } + + @Test + public void isPackageInstalled_commandFailed_throws() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("pm list packages"))) + .thenReturn(createFailedCommandResult()); + DeviceUtils sut = createSubjectUnderTest(); + + assertThrows(DeviceUtilsException.class, () -> sut.isPackageInstalled("package.name")); + } + + @Test + public void launchPackage_pmDumpFailedAndPackageDoesNotExist_throws() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm list packages"))) + .thenReturn(createSuccessfulCommandResultWithStdout("no packages")); + DeviceUtils sut = createSubjectUnderTest(); + + assertThrows(DeviceUtilsException.class, () -> sut.launchPackage("package.name")); + } + + @Test + public void launchPackage_pmDumpFailedAndPackageExists_throws() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm list packages"))) + .thenReturn(createSuccessfulCommandResultWithStdout("package:package.name")); DeviceUtils sut = createSubjectUnderTest(); assertThrows(DeviceUtilsException.class, () -> sut.launchPackage("package.name")); } @Test - public void launchPackage_successfullyLaunchedThePackage_returnsTrue() throws Exception { - when(mDevice.executeShellV2Command(Mockito.startsWith("monkey -p"))) + public void launchPackage_amStartCommandFailed_throws() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn( + createSuccessfulCommandResultWithStdout( + " 87f1610" + + " com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " Category:" + + " \"android.intent.category.NOTIFICATION_PREFERENCES\"")); + when(mDevice.executeShellV2Command(Mockito.startsWith("am start"))) + .thenReturn(createFailedCommandResult()); + DeviceUtils sut = createSubjectUnderTest(); + + assertThrows(DeviceUtilsException.class, () -> sut.launchPackage("com.google.android.gms")); + } + + @Test + public void launchPackage_amFailedToLaunchThePackage_throws() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn( + createSuccessfulCommandResultWithStdout( + " 87f1610" + + " com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " Category:" + + " \"android.intent.category.NOTIFICATION_PREFERENCES\"")); + when(mDevice.executeShellV2Command(Mockito.startsWith("am start"))) + .thenReturn( + createSuccessfulCommandResultWithStdout( + "Error: Activity not started, unable to resolve Intent")); + DeviceUtils sut = createSubjectUnderTest(); + + assertThrows(DeviceUtilsException.class, () -> sut.launchPackage("com.google.android.gms")); + } + + @Test + public void launchPackage_monkeyFailedButAmSucceed_doesNotThrow() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn( + createSuccessfulCommandResultWithStdout( + " 87f1610" + + " com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " Category:" + + " \"android.intent.category.NOTIFICATION_PREFERENCES\"")); + when(mDevice.executeShellV2Command(Mockito.startsWith("am start"))) .thenReturn(createSuccessfulCommandResultWithStdout("")); DeviceUtils sut = createSubjectUnderTest(); + sut.launchPackage("com.google.android.gms"); + } + + @Test + public void launchPackage_monkeySucceed_doesNotThrow() throws Exception { + when(mDevice.executeShellV2Command(Mockito.startsWith("monkey"))) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + when(mDevice.executeShellV2Command(Mockito.startsWith("pm dump"))) + .thenReturn(createFailedCommandResult()); + when(mDevice.executeShellV2Command(Mockito.startsWith("am start"))) + .thenReturn(createFailedCommandResult()); + DeviceUtils sut = createSubjectUnderTest(); + sut.launchPackage("package.name"); } @Test + public void getLaunchActivity_oneActivityIsLauncherAndMainAndDefault_returnsIt() + throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\""; + DeviceUtils sut = createSubjectUnderTest(); + + String res = sut.getLaunchActivity(pmDump); + + assertThat(res).isEqualTo("com.google.android.gms/.app.settings.GoogleSettingsActivity"); + } + + @Test + public void getLaunchActivity_oneActivityIsLauncherAndMain_returnsIt() throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " mPriority=10, mOrder=0, mHasStaticPartialTypes=false," + + " mHasDynamicPartialTypes=false"; + DeviceUtils sut = createSubjectUnderTest(); + + String res = sut.getLaunchActivity(pmDump); + + assertThat(res).isEqualTo("com.google.android.gms/.app.settings.GoogleSettingsActivity"); + } + + @Test + public void + getLaunchActivity_oneActivityIsLauncherAndOneActivityIsMain_returnsTheLauncherActivity() + throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " mPriority=10, mOrder=0, mHasStaticPartialTypes=false," + + " mHasDynamicPartialTypes=false"; + DeviceUtils sut = createSubjectUnderTest(); + + String res = sut.getLaunchActivity(pmDump); + + assertThat(res).isEqualTo("com.google.android.gms/.app.settings.GoogleSettingsActivity"); + } + + @Test + public void getLaunchActivity_oneActivityIsMain_returnsIt() throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " mPriority=10, mOrder=0, mHasStaticPartialTypes=false," + + " mHasDynamicPartialTypes=false"; + DeviceUtils sut = createSubjectUnderTest(); + + String res = sut.getLaunchActivity(pmDump); + + assertThat(res).isEqualTo("com.google.android.gms/.bugreport.BugreportActivity"); + } + + @Test + public void getLaunchActivity_oneActivityIsLauncher_returnsIt() throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Category: \"android.intent.category.LAUNCHER\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Action: \"android.intent.action.MAIN\"\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " mPriority=10, mOrder=0, mHasStaticPartialTypes=false," + + " mHasDynamicPartialTypes=false"; + DeviceUtils sut = createSubjectUnderTest(); + + String res = sut.getLaunchActivity(pmDump); + + assertThat(res).isEqualTo("com.google.android.gms/.bugreport.BugreportActivity"); + } + + @Test + public void getLaunchActivity_noMainOrLauncherActivities_throws() throws Exception { + String pmDump = + " eecc562 com.google.android.gms/.bugreport.BugreportActivity filter" + + " ac016f3\n" + + " Category: \"android.intent.category.HOME\"\n" + + " 87f1610 com.google.android.gms/.app.settings.GoogleSettingsActivity" + + " filter 7357509\n" + + " Category: \"android.intent.category.NOTIFICATION_PREFERENCES\"\n" + + " 28957f2 com.google.android.gms/.kids.SyncTailTrapperActivity filter" + + " 83cbcc0\n" + + " Category: \"android.intent.category.HOME\"\n" + + " Category: \"android.intent.category.DEFAULT\"\n" + + " mPriority=10, mOrder=0, mHasStaticPartialTypes=false," + + " mHasDynamicPartialTypes=false"; + DeviceUtils sut = createSubjectUnderTest(); + + assertThrows(DeviceUtilsException.class, () -> sut.getLaunchActivity(pmDump)); + } + + @Test public void currentTimeMillis_deviceCommandFailed_throwsException() throws Exception { DeviceUtils sut = createSubjectUnderTest(); when(mDevice.executeShellV2Command(Mockito.startsWith("echo"))) @@ -235,7 +503,7 @@ public final class DeviceUtilsTest { Mockito.anyLong(), Mockito.eq("sh"), Mockito.eq("-c"), - Mockito.contains("dumpsys dropbox"))) + Mockito.contains("dumpsys dropbox --proto"))) .thenReturn(createSuccessfulCommandResultWithStdout("")); List<DropboxEntry> result = sut.getDropboxEntries(Set.of("")); @@ -245,7 +513,7 @@ public final class DeviceUtilsTest { @Test public void getDropboxEntries_entryExists_returnsEntry() throws Exception { - Path dumpFile = Files.createTempFile(mFileSystem.getPath("/"), "test", ".tmp"); + Path dumpFile = Files.createTempFile(mFileSystem.getPath("/"), "dropbox", ".proto"); long time = 123; String data = "abc"; String tag = "tag"; @@ -259,7 +527,10 @@ public final class DeviceUtilsTest { Files.write(dumpFile, proto.toByteArray()); DeviceUtils sut = createSubjectUnderTestWithTempFile(dumpFile); when(mRunUtil.runTimedCmd( - Mockito.anyLong(), Mockito.eq("sh"), Mockito.eq("-c"), Mockito.anyString())) + Mockito.anyLong(), + Mockito.eq("sh"), + Mockito.eq("-c"), + Mockito.contains("dumpsys dropbox --proto"))) .thenReturn(createSuccessfulCommandResultWithStdout("")); List<DropboxEntry> result = sut.getDropboxEntries(Set.of(tag)); @@ -269,11 +540,106 @@ public final class DeviceUtilsTest { assertThat(result.get(0).getTag()).isEqualTo(tag); } - private DeviceUtils createSubjectUnderTestWithTempFile(Path tempFile) { + @Test + public void getDropboxEntriesFromStdout_entryExists_returnsEntry() throws Exception { + when(mRunUtil.runTimedCmd( + Mockito.anyLong(), + Mockito.eq("sh"), + Mockito.eq("-c"), + Mockito.contains("dumpsys dropbox --file"))) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + when(mRunUtil.runTimedCmd( + Mockito.anyLong(), + Mockito.eq("sh"), + Mockito.eq("-c"), + Mockito.contains("dumpsys dropbox --print"))) + .thenReturn(createSuccessfulCommandResultWithStdout("")); + Path fileDumpFile = Files.createTempFile(mFileSystem.getPath("/"), "file", ".dump"); + Path printDumpFile = Files.createTempFile(mFileSystem.getPath("/"), "print", ".dump"); + String fileResult = + "Drop box contents: 351 entries\n" + + "Max entries: 1000\n" + + "Low priority rate limit period: 2000 ms\n" + + "Low priority tags: {data_app_wtf, keymaster, system_server_wtf," + + " system_app_strictmode, system_app_wtf, system_server_strictmode," + + " data_app_strictmode, netstats}\n" + + "\n" + + "2022-09-05 04:17:21 system_server_wtf (text, 1730 bytes)\n" + + " /data/system/dropbox/system_server_wtf@1662351441269.txt\n" + + "2022-09-05 04:31:06 event_data (text, 39 bytes)\n" + + " /data/system/dropbox/event_data@1662352266197.txt\n"; + String printResult = + "Drop box contents: 351 entries\n" + + "Max entries: 1000\n" + + "Low priority rate limit period: 2000 ms\n" + + "Low priority tags: {data_app_wtf, keymaster, system_server_wtf," + + " system_app_strictmode, system_app_wtf, system_server_strictmode," + + " data_app_strictmode, netstats}\n" + + "\n" + + "========================================\n" + + "2022-09-05 04:17:21 system_server_wtf (text, 1730 bytes)\n" + + "Process: system_server\n" + + "Subject: ActivityManager\n" + + "Build:" + + " generic/cf_x86_64_phone/vsoc_x86_64:UpsideDownCake/MASTER/8990215:userdebug/dev-keys\n" + + "Dropped-Count: 0\n" + + "\n" + + "android.util.Log$TerribleFailure: Sending non-protected broadcast" + + " com.android.bluetooth.btservice.BLUETOOTH_COUNTER_METRICS_ACTION from" + + " system uid 1002 pkg com.android.bluetooth\n" + + " at android.util.Log.wtf(Log.java:332)\n" + + " at android.util.Log.wtf(Log.java:326)\n" + + " at" + + " com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:13609)\n" + + " at" + + " com.android.server.am.ActivityManagerService.broadcastIntentLocked(ActivityManagerService.java:14330)\n" + + " at" + + " com.android.server.am.ActivityManagerService.broadcastIntentInPackage(ActivityManagerService.java:14530)\n" + + " at" + + " com.android.server.am.ActivityManagerService$LocalService.broadcastIntentInPackage(ActivityManagerService.java:17065)\n" + + " at" + + " com.android.server.am.PendingIntentRecord.sendInner(PendingIntentRecord.java:526)\n" + + " at" + + " com.android.server.am.PendingIntentRecord.sendWithResult(PendingIntentRecord.java:311)\n" + + " at" + + " com.android.server.am.ActivityManagerService.sendIntentSender(ActivityManagerService.java:5379)\n" + + " at" + + " android.app.PendingIntent.sendAndReturnResult(PendingIntent.java:1012)\n" + + " at android.app.PendingIntent.send(PendingIntent.java:983)\n" + + " at" + + " com.android.server.alarm.AlarmManagerService$DeliveryTracker.deliverLocked(AlarmManagerService.java:5500)\n" + + " at" + + " com.android.server.alarm.AlarmManagerService.deliverAlarmsLocked(AlarmManagerService.java:4400)\n" + + " at" + + " com.android.server.alarm.AlarmManagerService$AlarmThread.run(AlarmManagerService.java:4711)\n" + + "Caused by: java.lang.Throwable\n" + + " at" + + " com.android.server.am.ActivityManagerService.checkBroadcastFromSystem(ActivityManagerService.java:13610)\n" + + " ... 11 more\n" + + "\n" + + "========================================\n" + + "2022-09-05 04:31:06 event_data (text, 39 bytes)\n" + + "start=1662350731248\n" + + "end=1662352266140\n" + + "\n"; + Files.write(fileDumpFile, fileResult.getBytes()); + Files.write(printDumpFile, printResult.getBytes()); + DeviceUtils sut = createSubjectUnderTestWithTempFile(fileDumpFile, printDumpFile); + + List<DropboxEntry> result = sut.getDropboxEntriesFromStdout(Set.of("system_server_wtf")); + + assertThat(result.get(0).getTime()).isEqualTo(1662351441269L); + assertThat(result.get(0).getData()).contains("Sending non-protected broadcast"); + assertThat(result.get(0).getTag()).isEqualTo("system_server_wtf"); + assertThat(result.size()).isEqualTo(1); + } + + private DeviceUtils createSubjectUnderTestWithTempFile(Path... tempFiles) { when(mDevice.getSerialNumber()).thenReturn("SERIAL"); FakeClock fakeClock = new FakeClock(); + Iterator<Path> iter = Arrays.asList(tempFiles).iterator(); return new DeviceUtils( - mDevice, fakeClock.getSleeper(), fakeClock, () -> mRunUtil, () -> tempFile); + mDevice, fakeClock.getSleeper(), fakeClock, () -> mRunUtil, () -> iter.next()); } private DeviceUtils createSubjectUnderTest() { diff --git a/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java b/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java index 292c0e2..4c2fe9c 100644 --- a/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java +++ b/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java @@ -41,9 +41,11 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; +import java.io.File; import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -139,6 +141,24 @@ public final class TestUtilsTest { } @Test + public void listApks_withApkDirectoryContainingObbFiles_returnsApksWithObb() throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Files.createFile(root.resolve("single.apk")); + Files.createFile(root.resolve("single.not_apk")); + Files.createFile(root.resolve("main.123.package.obb")); + + List<Path> res = TestUtils.listApks(root); + + List<String> fileNames = + res.stream() + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.toList()); + assertThat(fileNames).containsExactly("single.apk", "main.123.package.obb"); + } + + @Test public void listApks_withApkDirectoryContainingOtherFileTypes_returnsApksOnly() throws Exception { Path root = mFileSystem.getPath("apk"); @@ -166,6 +186,15 @@ public final class TestUtilsTest { } @Test + public void listApks_withApkDirectoryContainingOnlyObbFiles_throwException() throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Files.createFile(root.resolve("main.123.package.obb")); + + assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root)); + } + + @Test public void listApks_withNonApkFile_throwException() throws Exception { Path root = mFileSystem.getPath("single.not_apk"); Files.createFile(root); @@ -174,18 +203,42 @@ public final class TestUtilsTest { } @Test + public void listApks_withMultipleSingleApks_throwException() throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Files.createFile(root.resolve("single1.apk")); + Files.createFile(root.resolve("single2.apk")); + + assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root)); + } + + @Test public void listApks_withApksInMultipleDirectories_throwException() throws Exception { Path root = mFileSystem.getPath("apk"); Files.createDirectories(root); Files.createDirectories(root.resolve("1")); Files.createDirectories(root.resolve("2")); - Files.createFile(root.resolve("1").resolve("single.apk")); - Files.createFile(root.resolve("2").resolve("single.apk")); + Files.createFile(root.resolve("1").resolve("base.apk")); + Files.createFile(root.resolve("2").resolve("config.apk")); assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root)); } @Test + public void listApks_apksInTheSameDirectoryAndObbsInADifferentDirectory_doesNotThrow() + throws Exception { + Path root = mFileSystem.getPath("apk"); + Files.createDirectories(root); + Files.createDirectories(root.resolve("1")); + Files.createDirectories(root.resolve("2")); + Files.createFile(root.resolve("1").resolve("base.apk")); + Files.createFile(root.resolve("1").resolve("config.apk")); + Files.createFile(root.resolve("2").resolve("main.123.com.package.obb")); + + TestUtils.listApks(root); + } + + @Test public void collectScreenshot_savesToTestLog() throws Exception { TestUtils sut = createSubjectUnderTest(); InputStreamSource screenshotData = new FileInputStreamSource(mTempFolder.newFile()); @@ -202,6 +255,102 @@ public final class TestUtilsTest { } @Test + public void saveApks_always_savesOnTestPass() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = true; + + sut.saveApks( + TestUtils.TakeEffectWhen.ALWAYS, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(1)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_always_savesOnTestFail() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = false; + + sut.saveApks( + TestUtils.TakeEffectWhen.ALWAYS, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(1)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_never_doesNotSaveOnTestPass() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = true; + + sut.saveApks( + TestUtils.TakeEffectWhen.NEVER, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(0)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_never_doesNotSaveOnTestFail() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = false; + + sut.saveApks( + TestUtils.TakeEffectWhen.NEVER, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(0)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_onPass_doesNotSaveOnTestFail() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = false; + + sut.saveApks( + TestUtils.TakeEffectWhen.ON_PASS, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(0)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_onPass_savesOnTestPass() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = true; + + sut.saveApks( + TestUtils.TakeEffectWhen.ON_PASS, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(1)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_onFail_doesNotSaveOnTestPass() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = true; + + sut.saveApks( + TestUtils.TakeEffectWhen.ON_FAIL, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(0)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test + public void saveApks_onFail_savesOnTestFail() throws Exception { + TestUtils sut = createSubjectUnderTest(); + boolean testPassed = false; + + sut.saveApks( + TestUtils.TakeEffectWhen.ON_FAIL, testPassed, "apk", Arrays.asList(new File(""))); + + Mockito.verify(mMockTestArtifactReceiver, times(1)) + .addTestArtifact(Mockito.contains("apk"), Mockito.any(), Mockito.any(File.class)); + } + + @Test public void getDropboxPackageCrashLog_noEntries_returnsNull() throws Exception { TestUtils sut = createSubjectUnderTest(); when(mMockDeviceUtils.getDropboxEntries(Mockito.any())).thenReturn(List.of()); @@ -265,7 +414,7 @@ public final class TestUtilsTest { new String [DeviceUtils.DROPBOX_APP_CRASH_TAGS .size()])[0], - TEST_PACKAGE_NAME + "entry1"), + TEST_PACKAGE_NAME + " entry1"), new DeviceUtils.DropboxEntry( 2, DeviceUtils.DROPBOX_APP_CRASH_TAGS @@ -273,7 +422,7 @@ public final class TestUtilsTest { new String [DeviceUtils.DROPBOX_APP_CRASH_TAGS .size()])[0], - TEST_PACKAGE_NAME + "entry2"))); + TEST_PACKAGE_NAME + " entry2"))); String result = sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, false); @@ -296,7 +445,7 @@ public final class TestUtilsTest { new String [DeviceUtils.DROPBOX_APP_CRASH_TAGS .size()])[0], - "other.package" + "entry1"), + "other.package" + " entry1"), new DeviceUtils.DropboxEntry( 2, DeviceUtils.DROPBOX_APP_CRASH_TAGS @@ -304,7 +453,7 @@ public final class TestUtilsTest { new String [DeviceUtils.DROPBOX_APP_CRASH_TAGS .size()])[0], - TEST_PACKAGE_NAME + "entry2"))); + TEST_PACKAGE_NAME + " entry2"))); String result = sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, false); @@ -312,6 +461,202 @@ public final class TestUtilsTest { assertThat(result).contains("entry2"); } + @Test + public void isDropboxEntryFromPackageProcess_cmdlineMatched_returnsTrue() throws Exception { + String dropboxEntryData = "Cmd line: com.app.package"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processMatched_returnsTrue() throws Exception { + String dropboxEntryData = "Process: com.app.package"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processMatchedInLines_returnsTrue() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package\nline"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processNameFollowedByOtherChar_returnsTrue() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package, (time)\nline"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processNameFollowedByDot_returnsFalse() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package.sub, (time)\nline"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processNameFollowedByColon_returnsTrue() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package:sub, (time)\nline"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void isDropboxEntryFromPackageProcess_processNameFollowedByUnderscore_returnsFalse() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package_sub, (time)\nline"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_doesNotContainPackageName_returnsFalse() + throws Exception { + String dropboxEntryData = "line\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_packageNameWithUnderscorePrefix_returnsFalse() + throws Exception { + String dropboxEntryData = "line\na_com.app.package\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_packageNameWithUnderscorePostfix_returnsFalse() + throws Exception { + String dropboxEntryData = "line\ncom.app.package_a\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_packageNameWithDotPrefix_returnsFalse() + throws Exception { + String dropboxEntryData = "line\na.com.app.package\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_packageNameWithDotPostfix_returnsFalse() + throws Exception { + String dropboxEntryData = "line\ncom.app.package.a\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_packageNameWithColonPostfix_returnsTrue() + throws Exception { + String dropboxEntryData = "line\ncom.app.package:a\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void + isDropboxEntryFromPackageProcess_packageNameWithAcceptiblePrefixAndPostfix_returnsTrue() + throws Exception { + String dropboxEntryData = "line\ncom.app.package)\n"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + + @Test + public void + isDropboxEntryFromPackageProcess_wrongProcessNameWithCorrectPackageName_returnsFalse() + throws Exception { + String dropboxEntryData = "line\nProcess: com.app.package_other\ncom.app.package"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isFalse(); + } + + @Test + public void isDropboxEntryFromPackageProcess_MultipleProcessNamesWithOneMatching_returnsTrue() + throws Exception { + String dropboxEntryData = + "line\n" + + "Process: com.app.package_other\n" + + "Process: com.app.package\n" + + "Process: com.other"; + String packageName = "com.app.package"; + TestUtils sut = createSubjectUnderTest(); + + boolean res = sut.isDropboxEntryFromPackageProcess(dropboxEntryData, packageName); + + assertThat(res).isTrue(); + } + private TestUtils createSubjectUnderTest() { return new TestUtils(createTestInfo(), mMockTestArtifactReceiver, mMockDeviceUtils); } diff --git a/integration_tests/Android.bp b/integration_tests/Android.bp index f4c703b..f85bd31 100644 --- a/integration_tests/Android.bp +++ b/integration_tests/Android.bp @@ -61,9 +61,6 @@ python_library_host { srcs: [ "csuite_test_utils.py", ], - defaults: [ - "csuite_python_defaults", - ], java_data: [ "csuite_standalone_zip", ], @@ -84,9 +81,6 @@ python_test_host { libs: [ "csuite_test_utils", ], - defaults: [ - "csuite_python_defaults", - ], } python_test_host { @@ -105,9 +99,6 @@ python_test_host { ":csuite_crash_on_launch_test_app", ":csuite_no_crash_test_app", ], - defaults: [ - "csuite_python_defaults", - ], test_options: { unit_test: false, }, diff --git a/integration_tests/csuite_test_utils.py b/integration_tests/csuite_test_utils.py index 9bbc1d5..489f348 100644 --- a/integration_tests/csuite_test_utils.py +++ b/integration_tests/csuite_test_utils.py @@ -96,6 +96,11 @@ class CSuiteHarness(contextlib.AbstractContextManager): # Set the environment variable that TradeFed requires to find test modules. env['ANDROID_TARGET_OUT_TESTCASES'] = self._testcases_dir + jdk17_path = '/jdk/jdk17/linux-x86' + if os.path.isdir(jdk17_path): + env['JAVA_HOME'] = jdk17_path + java_path = jdk17_path + '/bin' + env['PATH'] = java_path + ':' + env['PATH'] return _run_command([self._launcher_binary] + flags, env=env) @@ -192,7 +197,7 @@ def _run_command(args, check=False, **kwargs) -> subprocess.CompletedProcess: # Log the command-line for debugging failed tests. Note that we convert # tokens to strings for _shlex_join. - env_str = ['env', '-i'] + ['%s=%s' % (k, v) for k, v in env.items()] + env_str = ['env', '-i'] + [f'{k}={v}' for k, v in env.items()] args_str = [str(t) for t in args] # Override some defaults. Note that 'check' deviates from this pattern to @@ -225,9 +230,9 @@ def _get_test_file(name: Text) -> pathlib.Path: test_file = test_dir.joinpath(name) if not test_file.exists(): - raise RuntimeError('Unable to find the file `%s` in the test execution dir ' - '`%s`; are you missing a data dependency in the build ' - 'module?' % (name, test_dir)) + raise RuntimeError(f'Unable to find the file `{name}` in the test ' + 'execution dir `{test_dir}`; are you missing a data ' + 'dependency in the build module?') return test_file diff --git a/pylib/Android.bp b/pylib/Android.bp index 319060f..ee11139 100644 --- a/pylib/Android.bp +++ b/pylib/Android.bp @@ -36,7 +36,4 @@ python_library_host { srcs: [ "csuite_test.py", ], - defaults: [ - "csuite_python_defaults", - ], } diff --git a/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java b/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java index ae7bb09..babde0d 100644 --- a/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java +++ b/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java @@ -16,7 +16,9 @@ package com.android.csuite.tests; +import com.android.csuite.core.ApkInstaller; import com.android.csuite.core.AppCrawlTester; +import com.android.csuite.core.TestUtils; import com.android.tradefed.config.Option; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; @@ -32,6 +34,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; /** A test that verifies that a single app can be successfully launched. */ @RunWith(DeviceJUnit4ClassRunner.class) @@ -39,8 +49,13 @@ public class AppCrawlTest extends BaseHostJUnit4Test { private static final String COLLECT_APP_VERSION = "collect-app-version"; private static final String COLLECT_GMS_VERSION = "collect-gms-version"; private static final String RECORD_SCREEN = "record-screen"; + @Rule public TestLogData mLogData = new TestLogData(); - AppCrawlTester mCrawler; + private boolean mIsLastTestPass; + private boolean mIsApkSaved = false; + + private ApkInstaller mApkInstaller; + private AppCrawlTester mCrawler; @Option(name = RECORD_SCREEN, description = "Whether to record screen during test.") private boolean mRecordScreen; @@ -60,16 +75,40 @@ public class AppCrawlTest extends BaseHostJUnit4Test { private boolean mCollectGmsVersion; @Option( - name = "apk", + name = "repack-apk", + mandatory = false, + description = + "Path to an apk file or a directory containing apk files of a single package " + + "to repack and install in Espresso mode") + private File mRepackApk; + + @Option( + name = "install-apk", mandatory = false, description = - "Path to an apk file or a directory containing apk files of a single package.") - private File mApk; + "The path to an apk file or a directory of apk files to be installed on the" + + " device. In Ui-automator mode, this includes both the target apk to" + + " install and any dependencies. In Espresso mode this can include" + + " additional libraries or dependencies.") + private final List<File> mInstallApkPaths = new ArrayList<>(); + + @Option( + name = "install-arg", + description = + "Arguments for the 'adb install-multiple' package installation command for" + + " UI-automator mode.") + private final List<String> mInstallArgs = new ArrayList<>(); @Option(name = "package-name", mandatory = true, description = "Package name of testing app.") private String mPackageName; @Option( + name = "crawl-controller-endpoint", + mandatory = false, + description = "The crawl controller endpoint to target.") + private String mCrawlControllerEndpoint; + + @Option( name = "ui-automator-mode", mandatory = false, description = @@ -77,29 +116,102 @@ public class AppCrawlTest extends BaseHostJUnit4Test { + " mode.") private boolean mUiAutomatorMode = false; + @Option( + name = "timeout-sec", + mandatory = false, + description = "The timeout for the crawl test.") + private int mTimeoutSec = 60; + + @Option( + name = "robo-script-file", + description = "A Roboscript file to be executed by the crawler.") + private File mRoboscriptFile; + + // TODO(b/234512223): add support for contextual roboscript files + + @Option( + name = "crawl-guidance-proto-file", + description = "A CrawlGuidance file to be executed by the crawler.") + private File mCrawlGuidanceProtoFile; + + @Option( + name = "login-config-dir", + description = + "A directory containing Roboscript and CrawlGuidance files with login" + + " credentials that are passed to the crawler. There should be one config" + + " file per package name. If both Roboscript and CrawlGuidance files are" + + " present, only the Roboscript file will be used.") + private File mLoginConfigDir; + + @Option( + name = "save-apk-when", + description = "When to save apk files to the test result artifacts.") + private TestUtils.TakeEffectWhen mSaveApkWhen = TestUtils.TakeEffectWhen.NEVER; + @Before - public void setUp() { + public void setUp() throws ApkInstaller.ApkInstallerException, IOException { + mIsLastTestPass = false; + mCrawler = AppCrawlTester.newInstance(mPackageName, getTestInformation(), mLogData); if (!mUiAutomatorMode) { - Preconditions.checkNotNull( - mApk, "Apk file path is required when not running in UIAutomator mode"); + setApkForEspressoMode(); } - - mCrawler = AppCrawlTester.newInstance(mPackageName, getTestInformation(), mLogData); + mCrawler.setCrawlControllerEndpoint(mCrawlControllerEndpoint); mCrawler.setRecordScreen(mRecordScreen); mCrawler.setCollectGmsVersion(mCollectGmsVersion); mCrawler.setCollectAppVersion(mCollectAppVersion); mCrawler.setUiAutomatorMode(mUiAutomatorMode); - mCrawler.setApkPath(mApk.toPath()); + mCrawler.setRoboscriptFile(toPathOrNull(mRoboscriptFile)); + mCrawler.setCrawlGuidanceProtoFile(toPathOrNull(mCrawlGuidanceProtoFile)); + mCrawler.setLoginConfigDir(toPathOrNull(mLoginConfigDir)); + mCrawler.setTimeoutSec(mTimeoutSec); + + mApkInstaller = ApkInstaller.getInstance(getDevice()); + mApkInstaller.install( + mInstallApkPaths.stream().map(File::toPath).collect(Collectors.toList()), + mInstallArgs); + } + + /** Helper method to fetch the path of optional File variables. */ + private static Path toPathOrNull(@Nullable File f) { + return f == null ? null : f.toPath(); + } + + /** + * For Espresso mode, checks that a path with the location of the apk to repackage was provided + */ + private void setApkForEspressoMode() { + Preconditions.checkNotNull( + mRepackApk, "Apk file path is required when not running in UIAutomator mode"); + // set the root path of the target apk for Espresso mode + mCrawler.setApkPath(mRepackApk.toPath()); } @Test public void testAppCrash() throws DeviceNotAvailableException { mCrawler.startAndAssertAppNoCrash(); + mIsLastTestPass = true; } @After - public void tearDown() throws DeviceNotAvailableException { - getDevice().uninstallPackage(mPackageName); + public void tearDown() throws DeviceNotAvailableException, ApkInstaller.ApkInstallerException { + TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); + + if (!mIsApkSaved) { + mIsApkSaved = + testUtils.saveApks( + mSaveApkWhen, mIsLastTestPass, mPackageName, mInstallApkPaths) + && testUtils.saveApks( + mSaveApkWhen, + mIsLastTestPass, + mPackageName, + Arrays.asList(mRepackApk)); + } + + mApkInstaller.uninstallAllInstalledPackages(); + if (!mUiAutomatorMode) { + getDevice().uninstallPackage(mPackageName); + } + mCrawler.cleanUp(); } } diff --git a/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java b/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java index 49f27fa..1c48981 100644 --- a/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java +++ b/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java @@ -43,6 +43,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; /** A test that verifies that a single app can be successfully launched. */ @RunWith(DeviceJUnit4ClassRunner.class) @@ -53,6 +54,8 @@ public class AppLaunchTest extends BaseHostJUnit4Test { @VisibleForTesting static final String RECORD_SCREEN = "record-screen"; @Rule public TestLogData mLogData = new TestLogData(); private ApkInstaller mApkInstaller; + private boolean mIsLastTestPass; + private boolean mIsApkSaved = false; @Option(name = RECORD_SCREEN, description = "Whether to record screen during test.") private boolean mRecordScreen; @@ -88,6 +91,11 @@ public class AppLaunchTest extends BaseHostJUnit4Test { description = "Arguments for the 'adb install-multiple' package installation command.") private final List<String> mInstallArgs = new ArrayList<>(); + @Option( + name = "save-apk-when", + description = "When to save apk files to the test result artifacts.") + private TestUtils.TakeEffectWhen mSaveApkWhen = TestUtils.TakeEffectWhen.NEVER; + @Option(name = "package-name", description = "Package name of testing app.") private String mPackageName; @@ -99,16 +107,14 @@ public class AppLaunchTest extends BaseHostJUnit4Test { @Before public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException { Assert.assertNotNull("Package name cannot be null", mPackageName); + mIsLastTestPass = false; DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); mApkInstaller = ApkInstaller.getInstance(getDevice()); - for (File apkPath : mApkPaths) { - CLog.d("Installing " + apkPath); - mApkInstaller.install( - apkPath.toPath(), mInstallArgs.toArray(new String[mInstallArgs.size()])); - } + mApkInstaller.install( + mApkPaths.stream().map(File::toPath).collect(Collectors.toList()), mInstallArgs); if (mCollectGmsVersion) { testUtils.collectGmsVersion(mPackageName); @@ -118,7 +124,6 @@ public class AppLaunchTest extends BaseHostJUnit4Test { testUtils.collectAppVersion(mPackageName); } - deviceUtils.resetPackage(mPackageName); deviceUtils.freezeRotation(); } @@ -135,6 +140,7 @@ public class AppLaunchTest extends BaseHostJUnit4Test { } else { launchPackageAndCheckForCrash(); } + mIsLastTestPass = true; } @After @@ -142,6 +148,11 @@ public class AppLaunchTest extends BaseHostJUnit4Test { DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); + if (!mIsApkSaved) { + mIsApkSaved = + testUtils.saveApks(mSaveApkWhen, mIsLastTestPass, mPackageName, mApkPaths); + } + if (mScreenshotAfterLaunch) { testUtils.collectScreenshot(mPackageName); } @@ -158,11 +169,22 @@ public class AppLaunchTest extends BaseHostJUnit4Test { DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); + try { + if (!deviceUtils.isPackageInstalled(mPackageName)) { + Assert.fail( + "Package " + + mPackageName + + " is not installed on the device. Aborting the test."); + } + } catch (DeviceUtilsException e) { + Assert.fail("Failed to check the installed package list: " + e.getMessage()); + } + DeviceTimestamp startTime = deviceUtils.currentTimeMillis(); try { deviceUtils.launchPackage(mPackageName); } catch (DeviceUtilsException e) { - Assert.fail(e.getMessage()); + Assert.fail("Failed to launch package " + mPackageName + ": " + e.getMessage()); } CLog.d("Waiting %s milliseconds for the app to launch fully.", mAppLaunchTimeoutMs); diff --git a/test_scripts/src/main/java/com/android/pixel/OWNERS b/test_scripts/src/main/java/com/android/pixel/OWNERS index 05ffe9a..aa4bbf9 100644 --- a/test_scripts/src/main/java/com/android/pixel/OWNERS +++ b/test_scripts/src/main/java/com/android/pixel/OWNERS @@ -1,2 +1,2 @@ murphykuo@google.com -huilingchi@google.com +elisahsu@google.com diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java index 92c2f59..ba0e695 100644 --- a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java +++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java @@ -28,21 +28,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class AppLaunchLockTest extends PixelAppCompatTestBase { - private static final int LAUNCH_TIME_MS = 30000; // 30 seconds + private static final int LAUNCH_TIME_MS = 15000; // 15 seconds private static final long WAIT_ONE_SECOND_IN_MS = 1000; private static final String DISMISS_KEYGUARD = "wm dismiss-keyguard"; @Test public void testLockDevice() throws Exception { - // Launch the 3P app - getDeviceUtils().launchApp(getPackage()); - - // Wait for the 3P app to appear - getUiDevice().wait(Until.hasObject(By.pkg(getPackage()).depth(0)), LAUNCH_TIME_MS); - getUiDevice().waitForIdle(); - Assert.assertTrue( - "3P app main page should show up", - getUiDevice().hasObject(By.pkg(getPackage()).depth(0))); + launchAndWaitAppOpen(LAUNCH_TIME_MS); if (getUiDevice().isScreenOn()) { getUiDevice().sleep(); diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java index 6c694c8..bb571dd 100644 --- a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java +++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java @@ -64,16 +64,7 @@ public class AppLaunchRecentAppTest extends PixelAppCompatTestBase { @Test public void testLaunchFromRecentApps() throws Exception { - // Launch the 3P app - getDeviceUtils().launchApp(getPackage()); - - // Wait for the 3P app to appear - getUiDevice() - .wait(Until.hasObject(By.pkg(getPackage()).depth(0)), WAIT_FIFTEEN_SECONDS_IN_MS); - getUiDevice().waitForIdle(); - Assert.assertTrue( - "3P app main page should show up", - getUiDevice().hasObject(By.pkg(getPackage()).depth(0))); + launchAndWaitAppOpen(WAIT_FIFTEEN_SECONDS_IN_MS); getUiDevice().pressRecentApps(); getUiDevice().wait(Until.hasObject(By.text("Screenshot")), WAIT_FIFTEEN_SECONDS_IN_MS); diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java index 62f6e1a..a2e65d0 100644 --- a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java +++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java @@ -17,8 +17,6 @@ package com.android.pixel.tests; import android.os.SystemClock; -import android.support.test.uiautomator.By; -import android.support.test.uiautomator.Until; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -35,7 +33,7 @@ public class AppLaunchRotateTest extends PixelAppCompatTestBase { private static final String ROTATE_PORTRAIT = "content insert --uri content://settings/system" + " --bind name:s:user_rotation --bind value:i:0"; - private static final int LAUNCH_TIME_MS = 30000; // 30 seconds + private static final int LAUNCH_TIME_MS = 15000; // 15 seconds private static final long WAIT_ONE_SECOND_IN_MS = 1000; @Override @@ -47,15 +45,7 @@ public class AppLaunchRotateTest extends PixelAppCompatTestBase { @Test public void testRotateDevice() throws Exception { - // Launch the 3P app - getDeviceUtils().launchApp(getPackage()); - - // Wait for the 3P app to appear - getUiDevice().wait(Until.hasObject(By.pkg(getPackage()).depth(0)), LAUNCH_TIME_MS); - getUiDevice().waitForIdle(); - Assert.assertTrue( - "3P app main page should show up", - getUiDevice().hasObject(By.pkg(getPackage()).depth(0))); + launchAndWaitAppOpen(LAUNCH_TIME_MS); // Turn off the automatic rotation getUiDevice().freezeRotation(); diff --git a/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java b/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java index d5fb892..aa79bb5 100644 --- a/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java +++ b/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java @@ -20,17 +20,19 @@ import static androidx.test.platform.app.InstrumentationRegistry.getArguments; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import android.app.KeyguardManager; +import android.support.test.uiautomator.By; import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.Until; import com.android.pixel.utils.DeviceUtils; import org.junit.After; +import org.junit.Assert; import org.junit.Before; /** Base class for Pixel app compatibility tests. */ public abstract class PixelAppCompatTestBase { private static final String KEY_PACKAGE_NAME = "package"; - private DeviceUtils mDeviceUtils; private UiDevice mDevice; private KeyguardManager mKeyguardManager; @@ -38,6 +40,7 @@ public abstract class PixelAppCompatTestBase { @Before public void setUp() throws Exception { + getDeviceUtils().setTestName(this.getClass().getSimpleName()); getDeviceUtils().createLogDataDir(); getDeviceUtils().wakeAndUnlockScreen(); // Start from the home screen @@ -78,4 +81,15 @@ public abstract class PixelAppCompatTestBase { } return mPackage; } + + protected void launchAndWaitAppOpen(long timeout) { + // Launch the 3P app + getDeviceUtils().launchApp(getPackage()); + + // Wait given timeout to ensure the 3P app completely loads + getUiDevice().wait(Until.hasObject(By.text(getPackage())), timeout); + Assert.assertTrue( + "3P app main page should show up", + getUiDevice().hasObject(By.pkg(getPackage()).depth(0))); + } } diff --git a/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java b/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java index 9d29dbc..1b665e3 100644 --- a/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java +++ b/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java @@ -25,11 +25,14 @@ import android.support.test.uiautomator.By; import android.support.test.uiautomator.UiDevice; import android.util.Log; +import com.google.common.base.Preconditions; + import org.junit.Assert; import java.io.File; import java.io.IOException; import java.nio.file.Paths; +import java.util.Optional; public class DeviceUtils { private static final String TAG = DeviceUtils.class.getSimpleName(); @@ -39,6 +42,8 @@ public class DeviceUtils { private static final long VIDEO_TAIL_BUFFER = 500; private static final String DISMISS_KEYGUARD = "wm dismiss-keyguard"; + private String mFolderDir = LOG_DATA_DIR; + private String mTestName = TAG; private RecordingThread mCurrentThread; private File mLogDataDir; private UiDevice mDevice; @@ -47,9 +52,24 @@ public class DeviceUtils { mDevice = device; } + /** + * Sets the test name and the folder path for the current test. + * + * @param testName The test name. + */ + public void setTestName(String testName) { + Optional<String> optionalTestName = Optional.ofNullable(testName); + if (optionalTestName.isPresent()) { + mTestName = optionalTestName.get(); + mFolderDir = String.join("/", LOG_DATA_DIR, optionalTestName.get()); + } else { + Preconditions.checkNotNull(testName, "testName cannot be null"); + } + } + /** Create a directory to save test screenshots, screenrecord and text files. */ public void createLogDataDir() { - mLogDataDir = new File(LOG_DATA_DIR); + mLogDataDir = new File(mFolderDir); if (mLogDataDir.exists()) { String[] children = mLogDataDir.list(); for (String file : children) { @@ -104,8 +124,9 @@ public class DeviceUtils { public void takeScreenshot(String packageName, String description) { File screenshot = new File( - LOG_DATA_DIR, - String.format("%s_screenshot_%s.png", packageName, description)); + mFolderDir, + String.format( + "%s_%s_screenshot_%s.png", mTestName, packageName, description)); mDevice.takeScreenshot(screenshot); } @@ -118,7 +139,8 @@ public class DeviceUtils { Log.v(TAG, "Started Recording"); mCurrentThread = new RecordingThread( - "test-screen-record", String.format("%s_screenrecord", packageName)); + "test-screen-record", + String.format("%s_%s_screenrecord", mTestName, packageName)); mCurrentThread.start(); } diff --git a/test_scripts/src/main/java/com/android/webview/Android.bp b/test_scripts/src/main/java/com/android/webview/Android.bp new file mode 100644 index 0000000..62aed62 --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/Android.bp @@ -0,0 +1,37 @@ +//Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library_host { + name: "webview-app-compat-tests-lib", + srcs: ["lib/**/*.java"], + libs: [ + "junit", + ], + static_libs: [ + "tradefed", + ], +} + +java_test_host { + name: "webview-app-compat-unittests", + srcs: ["unittests/**/*.java"], + static_libs: [ + "webview-app-compat-tests-lib", + ], + test_suites: ["general-tests"], +} diff --git a/test_scripts/src/main/java/com/android/webview/lib/WebviewInstallerToolPreparer.java b/test_scripts/src/main/java/com/android/webview/lib/WebviewInstallerToolPreparer.java new file mode 100644 index 0000000..52e5c94 --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/lib/WebviewInstallerToolPreparer.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.webview.tests; + +import com.android.tradefed.config.Option; +import com.android.tradefed.config.Option.Importance; +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.targetprep.ITargetPreparer; +import com.android.tradefed.targetprep.TargetSetupError; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; +import com.android.tradefed.util.RunUtil; + +import org.junit.Assert; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.annotation.Nullable; + +public class WebviewInstallerToolPreparer implements ITargetPreparer { + private static final long COMMAND_TIMEOUT_MILLIS = 5 * 60 * 1000; + private static final String WEBVIEW_INSTALLER_TOOL_PATH = "WEBVIEW_INSTALLER_TOOL_PATH"; + private static final String GCLOUD_CLI_PATH = "GCLOUD_CLI_PATH"; + + private File mGcloudCliDir; + private RunUtilProvider mRunUtilProvider; + + @Option( + name = "gcloud-cli-zip-archive", + description = "Path to the google cli zip archive.", + importance = Importance.ALWAYS) + private File mGcloudCliZipArchive; + + @Option( + name = "webview-installer-tool", + description = "Path to the webview installer executable.", + importance = Importance.ALWAYS) + private File mWebviewInstallerTool; + + public WebviewInstallerToolPreparer(RunUtilProvider runUtilProvider) { + mRunUtilProvider = runUtilProvider; + } + + public WebviewInstallerToolPreparer() { + this(() -> new RunUtil()); + } + + public static CommandResult runWebviewInstallerToolCommand( + TestInformation testInformation, + @Nullable String webviewVersion, + @Nullable String releaseChannel, + List<String> extraArgs) { + RunUtil runUtil = new RunUtil(); + runUtil.setEnvVariable("HOME", getGcloudCliPath(testInformation)); + + List<String> commandLineArgs = + new ArrayList<>( + Arrays.asList( + getWebviewInstallerToolPath(testInformation), + "--non-next", + "--serial", + testInformation.getDevice().getSerialNumber(), + "-vvv", + "--gsutil", + Paths.get( + getGcloudCliPath(testInformation), + "google-cloud-sdk", + "bin", + "gsutil") + .toFile() + .getAbsolutePath())); + commandLineArgs.addAll(extraArgs); + + if (webviewVersion != null) { + commandLineArgs.addAll(Arrays.asList("--chrome-version", webviewVersion)); + } + + if (releaseChannel != null) { + commandLineArgs.addAll(Arrays.asList("--channel", releaseChannel)); + } + + return runUtil.runTimedCmd( + COMMAND_TIMEOUT_MILLIS, + System.out, + System.out, + commandLineArgs.toArray(new String[0])); + } + + public static void setGcloudCliPath(TestInformation testInformation, File gcloudCliDir) { + testInformation + .getBuildInfo() + .addBuildAttribute(GCLOUD_CLI_PATH, gcloudCliDir.getAbsolutePath()); + } + + public static void setWebviewInstallerToolPath( + TestInformation testInformation, File webviewInstallerTool) { + testInformation + .getBuildInfo() + .addBuildAttribute( + WEBVIEW_INSTALLER_TOOL_PATH, webviewInstallerTool.getAbsolutePath()); + } + + public static String getWebviewInstallerToolPath(TestInformation testInformation) { + return testInformation.getBuildInfo().getBuildAttributes().get(WEBVIEW_INSTALLER_TOOL_PATH); + } + + public static String getGcloudCliPath(TestInformation testInformation) { + return testInformation.getBuildInfo().getBuildAttributes().get(GCLOUD_CLI_PATH); + } + + @Override + public void setUp(TestInformation testInfo) throws TargetSetupError { + Assert.assertNotEquals( + "Argument --webview-installer-tool must be used.", mWebviewInstallerTool, null); + Assert.assertNotEquals( + "Argument --gcloud-cli-zip must be used.", mGcloudCliZipArchive, null); + try { + RunUtil runUtil = mRunUtilProvider.get(); + mGcloudCliDir = Files.createTempDirectory(null).toFile(); + CommandResult unzipRes = + runUtil.runTimedCmd( + COMMAND_TIMEOUT_MILLIS, + "unzip", + mGcloudCliZipArchive.getAbsolutePath(), + "-d", + mGcloudCliDir.getAbsolutePath()); + + Assert.assertEquals( + "Unable to unzip the gcloud cli zip archive", + unzipRes.getStatus(), + CommandStatus.SUCCESS); + + // The 'gcloud init' command creates configuration files for gsutil and other + // applications that use the gcloud sdk in the home directory. We can isolate + // the effects of these configuration files to the processes that run the + // gcloud and gsutil executables tracked by this class by setting the home + // directory for processes that run those executables to a temporary directory + // also tracked by this class. + runUtil.setEnvVariable("HOME", mGcloudCliDir.getAbsolutePath()); + File gcloudBin = + mGcloudCliDir + .toPath() + .resolve(Paths.get("google-cloud-sdk", "bin", "gcloud")) + .toFile(); + String gcloudInitScript = + String.format( + "printf \"1\\n1\" | %s init --console-only", + gcloudBin.getAbsolutePath()); + CommandResult gcloudInitRes = + runUtil.runTimedCmd( + COMMAND_TIMEOUT_MILLIS, + System.out, + System.out, + "sh", + "-c", + gcloudInitScript); + Assert.assertEquals( + "gcloud cli initialization failed", + gcloudInitRes.getStatus(), + CommandStatus.SUCCESS); + + CommandResult chmodRes = + runUtil.runTimedCmd( + COMMAND_TIMEOUT_MILLIS, + System.out, + System.out, + "chmod", + "755", + "-v", + mWebviewInstallerTool.getAbsolutePath()); + + Assert.assertEquals( + "The 'chmod 755 -v <WebView installer tool>' command failed", + chmodRes.getStatus(), + CommandStatus.SUCCESS); + + } catch (Exception ex) { + throw new TargetSetupError("Caught an exception during setup:\n" + ex); + } + setGcloudCliPath(testInfo, mGcloudCliDir); + setWebviewInstallerToolPath(testInfo, mWebviewInstallerTool); + } + + @Override + public void tearDown(TestInformation testInfo, Throwable e) { + // Clean up some files. + mRunUtilProvider + .get() + .runTimedCmd(COMMAND_TIMEOUT_MILLIS, "rm", "-rf", mGcloudCliDir.getAbsolutePath()); + } + + interface RunUtilProvider { + RunUtil get(); + } +} diff --git a/test_scripts/src/main/java/com/android/webview/lib/WebviewPackage.java b/test_scripts/src/main/java/com/android/webview/lib/WebviewPackage.java new file mode 100644 index 0000000..6347f9f --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/lib/WebviewPackage.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.webview.tests; + +import com.android.tradefed.util.AaptParser; + +import org.junit.Assert; + +import java.nio.file.Path; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WebviewPackage implements Comparable<WebviewPackage> { + private final String mPackageName; + private final String mVersion; + private final long mVersionCode; + private Path mApkPath; + + public WebviewPackage(String packageName, String version, long versionCode, Path apkPath) { + this(packageName, version, versionCode); + mApkPath = apkPath; + } + + public WebviewPackage(String packageName, String version, long versionCode) { + mPackageName = packageName; + mVersion = version; + mVersionCode = versionCode; + } + + public static WebviewPackage buildFromApk(Path apkPath) { + AaptParser aaptParser = AaptParser.parse(apkPath.toFile()); + return new WebviewPackage( + aaptParser.getPackageName(), + aaptParser.getVersionName(), + Long.parseLong(aaptParser.getVersionCode()), + apkPath); + } + + public static WebviewPackage buildFromDumpsys(String dumpsys) { + String regexPattern = "Current WebView package \\(name, version\\): \\((.*), (.*)\\)"; + Pattern pattern = Pattern.compile(regexPattern); + Matcher matcher = pattern.matcher(dumpsys); + Assert.assertTrue( + String.format( + "Cannot find a sub string matching the regex in the dumpsys\n%s", dumpsys), + matcher.find()); + return buildFromDumpsys(matcher.group(1), dumpsys); + } + + public static WebviewPackage buildFromDumpsys(String webviewPackage, String dumpsys) { + String regexPattern = + String.format( + "Valid package %s \\(versionName: (.*), versionCode: (\\d+)," + + " targetSdkVersion: (\\d+)\\)", + webviewPackage.replace(".", "\\.")); + Pattern pattern = Pattern.compile(regexPattern); + Matcher matcher = pattern.matcher(dumpsys); + Assert.assertTrue( + String.format( + "Cannot find a sub string matching the regex in the dumpsys\n%s", dumpsys), + matcher.find()); + return new WebviewPackage( + webviewPackage, matcher.group(1), Long.parseLong(matcher.group(2))); + } + + public Path getPath() { + Assert.assertTrue( + "The apk path was not set for this WebviewPackage instance", mApkPath != null); + return mApkPath; + } + + public String getPackageName() { + return mPackageName; + } + + public String getVersion() { + return mVersion; + } + + public long getVersionCode() { + return mVersionCode; + } + + @Override + public int compareTo(WebviewPackage otherWebviewPkg) { + return Long.compare(this.getVersionCode(), otherWebviewPkg.getVersionCode()); + } + + @Override + public boolean equals(final Object obj) { + final WebviewPackage otherWebviewPkg = (WebviewPackage) obj; + return this.getPackageName().equals(otherWebviewPkg.getPackageName()) + && this.getVersion().equals(otherWebviewPkg.getVersion()); + } +} diff --git a/test_scripts/src/main/java/com/android/webview/lib/WebviewUtils.java b/test_scripts/src/main/java/com/android/webview/lib/WebviewUtils.java new file mode 100644 index 0000000..2d47066 --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/lib/WebviewUtils.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.webview.tests; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.invoker.TestInformation; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import org.junit.Assert; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class WebviewUtils { + private TestInformation mTestInformation; + + public WebviewUtils(TestInformation testInformation) { + mTestInformation = testInformation; + } + + public WebviewPackage installWebview(String webviewVersion, String releaseChannel) + throws IOException, InterruptedException, DeviceNotAvailableException { + List<String> extraArgs = new ArrayList<>(); + if (webviewVersion == null + && Arrays.asList("beta", "stable").contains(releaseChannel.toLowerCase())) { + // Get current version of WebView in the stable or beta release channels. + CLog.i( + "Getting the latest nightly official release version of the %s branch", + releaseChannel); + String releaseChannelVersion = getNightlyBranchBuildVersion(releaseChannel); + Assert.assertNotNull( + String.format( + "Could not retrieve the latest " + + "nightly release version of the %s channel", + releaseChannel), + releaseChannelVersion); + // Install the latest official build compiled for the beta or stable branches. + extraArgs.addAll( + Arrays.asList("--milestone", releaseChannelVersion.split("\\.", 2)[0])); + } + CommandResult commandResult = + WebviewInstallerToolPreparer.runWebviewInstallerToolCommand( + mTestInformation, webviewVersion, releaseChannel, extraArgs); + + Assert.assertEquals( + "The WebView installer tool failed to install WebView:\n" + + commandResult.toString(), + commandResult.getStatus(), + CommandStatus.SUCCESS); + + printWebviewVersion(); + return getCurrentWebviewPackage(); + } + + private static String getNightlyBranchBuildVersion(String releaseChannel) + throws IOException, MalformedURLException { + final URL omahaProxyUrl = new URL("https://omahaproxy.appspot.com/all?os=webview"); + try (BufferedReader bufferedReader = + new BufferedReader( + new InputStreamReader(omahaProxyUrl.openConnection().getInputStream()))) { + String csvLine = null; + while ((csvLine = bufferedReader.readLine()) != null) { + String[] csvLineValues = csvLine.split(","); + if (csvLineValues[1].toLowerCase().equals(releaseChannel.toLowerCase())) { + return csvLineValues[2]; + } + } + } + return null; + } + + public void uninstallWebview( + WebviewPackage webviewPackage, WebviewPackage preInstalledWebviewPackage) + throws DeviceNotAvailableException { + Assert.assertNotEquals( + "Test is attempting to uninstall the preinstalled WebView provider", + webviewPackage, + preInstalledWebviewPackage); + updateWebviewImplementation(preInstalledWebviewPackage.getPackageName()); + mTestInformation + .getDevice() + .executeAdbCommand("uninstall", webviewPackage.getPackageName()); + printWebviewVersion(); + } + + private void updateWebviewImplementation(String webviewPackageName) + throws DeviceNotAvailableException { + CommandResult res = + mTestInformation + .getDevice() + .executeShellV2Command( + String.format( + "cmd webviewupdate set-webview-implementation %s", + webviewPackageName)); + Assert.assertEquals( + "Failed to set webview update: " + res, res.getStatus(), CommandStatus.SUCCESS); + } + + public WebviewPackage getCurrentWebviewPackage() throws DeviceNotAvailableException { + String dumpsys = mTestInformation.getDevice().executeShellCommand("dumpsys webviewupdate"); + return WebviewPackage.buildFromDumpsys(dumpsys); + } + + public void printWebviewVersion() throws DeviceNotAvailableException { + WebviewPackage currentWebview = getCurrentWebviewPackage(); + printWebviewVersion(currentWebview); + } + + public void printWebviewVersion(WebviewPackage currentWebview) + throws DeviceNotAvailableException { + CLog.i("Current webview implementation: %s", currentWebview.getPackageName()); + CLog.i("Current webview version: %s", currentWebview.getVersion()); + } +} diff --git a/test_scripts/src/main/java/com/android/webview/tests/WebviewAppCrawlTest.java b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppCrawlTest.java new file mode 100644 index 0000000..6ea0e3b --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppCrawlTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.webview.tests; + +import com.android.csuite.core.ApkInstaller; +import com.android.csuite.core.ApkInstaller.ApkInstallerException; +import com.android.csuite.core.AppCrawlTester; +import com.android.csuite.core.DeviceUtils; +import com.android.csuite.core.TestUtils; +import com.android.tradefed.config.Option; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import com.google.common.base.Preconditions; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nullable; + +/** A test that verifies that a single app can be successfully launched. */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class WebviewAppCrawlTest extends BaseHostJUnit4Test { + @Rule public TestLogData mLogData = new TestLogData(); + + private static final String COLLECT_APP_VERSION = "collect-app-version"; + private static final String COLLECT_GMS_VERSION = "collect-gms-version"; + private static final long COMMAND_TIMEOUT_MILLIS = 5 * 60 * 1000; + + private WebviewUtils mWebviewUtils; + private WebviewPackage mPreInstalledWebview; + private ApkInstaller mApkInstaller; + private AppCrawlTester mCrawler; + + @Option(name = "record-screen", description = "Whether to record screen during test.") + private boolean mRecordScreen; + + @Option(name = "webview-version-to-test", description = "Version of Webview to test.") + private String mWebviewVersionToTest; + + @Option( + name = "release-channel", + description = "Release channel to fetch Webview from, i.e. stable.") + private String mReleaseChannel; + + @Option(name = "package-name", description = "Package name of testing app.") + private String mPackageName; + + @Option( + name = "install-apk", + description = + "The path to an apk file or a directory of apk files of a singe package to be" + + " installed on device. Can be repeated.") + private List<File> mApkPaths = new ArrayList<>(); + + @Option( + name = "install-arg", + description = "Arguments for the 'adb install-multiple' package installation command.") + private final List<String> mInstallArgs = new ArrayList<>(); + + @Option( + name = "app-launch-timeout-ms", + description = "Time to wait for an app to launch in msecs.") + private int mAppLaunchTimeoutMs = 20000; + + @Option( + name = COLLECT_APP_VERSION, + description = + "Whether to collect package version information and store the information in" + + " test log files.") + private boolean mCollectAppVersion; + + @Option( + name = COLLECT_GMS_VERSION, + description = + "Whether to collect GMS core version information and store the information in" + + " test log files.") + private boolean mCollectGmsVersion; + + @Option( + name = "repack-apk", + mandatory = false, + description = + "Path to an apk file or a directory containing apk files of a single package " + + "to repack and install in Espresso mode") + private File mRepackApk; + + @Option( + name = "crawl-controller-endpoint", + mandatory = false, + description = "The crawl controller endpoint to target.") + private String mCrawlControllerEndpoint; + + @Option( + name = "ui-automator-mode", + mandatory = false, + description = + "Run the crawler with UIAutomator mode. Apk option is not required in this" + + " mode.") + private boolean mUiAutomatorMode = false; + + @Option( + name = "robo-script-file", + description = "A Roboscript file to be executed by the crawler.") + private File mRoboscriptFile; + + // TODO(b/234512223): add support for contextual roboscript files + + @Option( + name = "crawl-guidance-proto-file", + description = "A CrawlGuidance file to be executed by the crawler.") + private File mCrawlGuidanceProtoFile; + + @Option( + name = "timeout-sec", + mandatory = false, + description = "The timeout for the crawl test.") + private int mTimeoutSec = 60; + + @Option( + name = "save-apk-when", + description = "When to save apk files to the test result artifacts.") + private TestUtils.TakeEffectWhen mSaveApkWhen = TestUtils.TakeEffectWhen.NEVER; + + @Option( + name = "login-config-dir", + description = + "A directory containing Roboscript and CrawlGuidance files with login" + + " credentials that are passed to the crawler. There should be one config" + + " file per package name. If both Roboscript and CrawlGuidance files are" + + " present, only the Roboscript file will be used.") + private File mLoginConfigDir; + + @Before + public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException { + Assert.assertNotNull("Package name cannot be null", mPackageName); + Assert.assertTrue( + "Either the --release-channel or --webview-version-to-test arguments " + + "must be used", + mWebviewVersionToTest != null || mReleaseChannel != null); + + mCrawler = AppCrawlTester.newInstance(mPackageName, getTestInformation(), mLogData); + if (!mUiAutomatorMode) { + setApkForEspressoMode(); + } + mCrawler.setCrawlControllerEndpoint(mCrawlControllerEndpoint); + mCrawler.setRecordScreen(mRecordScreen); + mCrawler.setCollectGmsVersion(mCollectGmsVersion); + mCrawler.setCollectAppVersion(mCollectAppVersion); + mCrawler.setUiAutomatorMode(mUiAutomatorMode); + mCrawler.setRoboscriptFile(toPathOrNull(mRoboscriptFile)); + mCrawler.setCrawlGuidanceProtoFile(toPathOrNull(mCrawlGuidanceProtoFile)); + mCrawler.setLoginConfigDir(toPathOrNull(mLoginConfigDir)); + mCrawler.setTimeoutSec(mTimeoutSec); + + mApkInstaller = ApkInstaller.getInstance(getDevice()); + mWebviewUtils = new WebviewUtils(getTestInformation()); + mPreInstalledWebview = mWebviewUtils.getCurrentWebviewPackage(); + + for (File apkPath : mApkPaths) { + CLog.d("Installing " + apkPath); + mApkInstaller.install(apkPath.toPath(), mInstallArgs); + } + + DeviceUtils.getInstance(getDevice()).freezeRotation(); + mWebviewUtils.printWebviewVersion(); + } + + /** + * For Espresso mode, checks that a path with the location of the apk to repackage was provided + */ + private void setApkForEspressoMode() { + Preconditions.checkNotNull( + mRepackApk, "Apk file path is required when not running in UIAutomator mode"); + // set the root path of the target apk for Espresso mode + mCrawler.setApkPath(mRepackApk.toPath()); + } + + private static Path toPathOrNull(@Nullable File f) { + return f == null ? null : f.toPath(); + } + + @Test + public void testAppCrawl() + throws DeviceNotAvailableException, InterruptedException, ApkInstallerException, + IOException { + AssertionError lastError = null; + WebviewPackage lastWebviewInstalled = + mWebviewUtils.installWebview(mWebviewVersionToTest, mReleaseChannel); + + try { + mCrawler.startAndAssertAppNoCrash(); + } catch (AssertionError e) { + lastError = e; + } finally { + mWebviewUtils.uninstallWebview(lastWebviewInstalled, mPreInstalledWebview); + } + + // If the app doesn't crash, complete the test. + if (lastError == null) { + return; + } + + // If the app crashes, try the app with the original webview version that comes with the + // device. + try { + mCrawler.startAndAssertAppNoCrash(); + } catch (AssertionError newError) { + CLog.w( + "The app %s crashed both with and without the webview installation," + + " ignoring the failure...", + mPackageName); + return; + } + throw new AssertionError( + String.format( + "Package %s crashed since webview version %s", + mPackageName, lastWebviewInstalled.getVersion()), + lastError); + } + + @After + public void tearDown() throws DeviceNotAvailableException, ApkInstallerException { + TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); + testUtils.collectScreenshot(mPackageName); + + DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); + deviceUtils.stopPackage(mPackageName); + deviceUtils.unfreezeRotation(); + + mApkInstaller.uninstallAllInstalledPackages(); + mWebviewUtils.printWebviewVersion(); + + if (!mUiAutomatorMode) { + getDevice().uninstallPackage(mPackageName); + } + + mCrawler.cleanUp(); + } +} diff --git a/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java index d08d2e3..a8f72d1 100644 --- a/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java +++ b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java @@ -23,15 +23,11 @@ import com.android.csuite.core.DeviceUtils.DeviceTimestamp; import com.android.csuite.core.DeviceUtils.DeviceUtilsException; import com.android.csuite.core.TestUtils; import com.android.tradefed.config.Option; -import com.android.tradefed.config.Option.Importance; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; -import com.android.tradefed.util.AaptParser; -import com.android.tradefed.util.CommandResult; -import com.android.tradefed.util.CommandStatus; import com.android.tradefed.util.RunUtil; import org.junit.After; @@ -44,23 +40,30 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + /** A test that verifies that a single app can be successfully launched. */ @RunWith(DeviceJUnit4ClassRunner.class) public class WebviewAppLaunchTest extends BaseHostJUnit4Test { @Rule public TestLogData mLogData = new TestLogData(); + + private static final long COMMAND_TIMEOUT_MILLIS = 5 * 60 * 1000; + private WebviewUtils mWebviewUtils; + private WebviewPackage mPreInstalledWebview; private ApkInstaller mApkInstaller; - private List<File> mOrderedWebviewApks = new ArrayList<>(); @Option(name = "record-screen", description = "Whether to record screen during test.") private boolean mRecordScreen; + @Option(name = "webview-version-to-test", description = "Version of Webview to test.") + private String mWebviewVersionToTest; + + @Option( + name = "release-channel", + description = "Release channel to fetch Webview from, i.e. stable.") + private String mReleaseChannel; + @Option(name = "package-name", description = "Package name of testing app.") private String mPackageName; @@ -81,43 +84,41 @@ public class WebviewAppLaunchTest extends BaseHostJUnit4Test { description = "Time to wait for an app to launch in msecs.") private int mAppLaunchTimeoutMs = 20000; - @Option( - name = "webview-apk-dir", - description = "The path to the webview apk.", - importance = Importance.ALWAYS) - private File mWebviewApkDir; - @Before public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException { Assert.assertNotNull("Package name cannot be null", mPackageName); - - readWebviewApkDirectory(); + Assert.assertTrue( + "Either the --release-channel or --webview-version-to-test arguments " + + "must be used", + mWebviewVersionToTest != null || mReleaseChannel != null); mApkInstaller = ApkInstaller.getInstance(getDevice()); + mWebviewUtils = new WebviewUtils(getTestInformation()); + mPreInstalledWebview = mWebviewUtils.getCurrentWebviewPackage(); + for (File apkPath : mApkPaths) { CLog.d("Installing " + apkPath); - mApkInstaller.install( - apkPath.toPath(), mInstallArgs.toArray(new String[mInstallArgs.size()])); + mApkInstaller.install(apkPath.toPath(), mInstallArgs); } - DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); - deviceUtils.freezeRotation(); - - printWebviewVersion(); + DeviceUtils.getInstance(getDevice()).freezeRotation(); + mWebviewUtils.printWebviewVersion(); } @Test public void testAppLaunch() - throws DeviceNotAvailableException, ApkInstallerException, IOException { + throws DeviceNotAvailableException, InterruptedException, ApkInstallerException, + IOException { AssertionError lastError = null; - // Try the latest webview version - WebviewPackage lastWebviewInstalled = installWebview(mOrderedWebviewApks.get(0)); + WebviewPackage lastWebviewInstalled = + mWebviewUtils.installWebview(mWebviewVersionToTest, mReleaseChannel); + try { assertAppLaunchNoCrash(); } catch (AssertionError e) { lastError = e; } finally { - uninstallWebview(); + mWebviewUtils.uninstallWebview(lastWebviewInstalled, mPreInstalledWebview); } // If the app doesn't crash, complete the test. @@ -136,20 +137,6 @@ public class WebviewAppLaunchTest extends BaseHostJUnit4Test { mPackageName); return; } - - for (int idx = 1; idx < mOrderedWebviewApks.size(); idx++) { - lastWebviewInstalled = installWebview(mOrderedWebviewApks.get(idx)); - try { - assertAppLaunchNoCrash(); - } catch (AssertionError e) { - lastError = e; - continue; - } finally { - uninstallWebview(); - } - break; - } - throw new AssertionError( String.format( "Package %s crashed since webview version %s", @@ -167,92 +154,11 @@ public class WebviewAppLaunchTest extends BaseHostJUnit4Test { deviceUtils.unfreezeRotation(); mApkInstaller.uninstallAllInstalledPackages(); - printWebviewVersion(); - } - - private void readWebviewApkDirectory() { - mOrderedWebviewApks = Arrays.asList(mWebviewApkDir.listFiles()); - Collections.sort( - mOrderedWebviewApks, - new Comparator<File>() { - @Override - public int compare(File apk1, File apk2) { - return getVersionCode(apk2).compareTo(getVersionCode(apk1)); - } - - private Long getVersionCode(File apk) { - return Long.parseLong(AaptParser.parse(apk).getVersionCode()); - } - }); - } - - private void printWebviewVersion(WebviewPackage currentWebview) - throws DeviceNotAvailableException { - CLog.i("Current webview implementation: %s", currentWebview.getPackageName()); - CLog.i("Current webview version: %s", currentWebview.getVersion()); - } - - private void printWebviewVersion() throws DeviceNotAvailableException { - WebviewPackage currentWebview = getCurrentWebviewPackage(); - printWebviewVersion(currentWebview); - } - - private WebviewPackage installWebview(File apk) - throws ApkInstallerException, IOException, DeviceNotAvailableException { - ApkInstaller.getInstance(getDevice()).install(apk.toPath()); - CommandResult res = - getDevice() - .executeShellV2Command( - "cmd webviewupdate set-webview-implementation com.android.webview"); - Assert.assertEquals( - "Failed to set webview update: " + res, res.getStatus(), CommandStatus.SUCCESS); - WebviewPackage currentWebview = getCurrentWebviewPackage(); - printWebviewVersion(currentWebview); - return currentWebview; - } - - private void uninstallWebview() throws DeviceNotAvailableException { - getDevice() - .executeShellCommand( - "cmd webviewupdate set-webview-implementation com.google.android.webview"); - getDevice().executeAdbCommand("uninstall", "com.android.webview"); - } - - private WebviewPackage getCurrentWebviewPackage() throws DeviceNotAvailableException { - String dumpsys = getDevice().executeShellCommand("dumpsys webviewupdate"); - return WebviewPackage.parseFrom(dumpsys); - } - - private static class WebviewPackage { - private final String mPackageName; - private final String mVersion; - - private WebviewPackage(String packageName, String version) { - mPackageName = packageName; - mVersion = version; - } - - static WebviewPackage parseFrom(String dumpsys) { - Pattern pattern = - Pattern.compile("Current WebView package \\(name, version\\): \\((.*?)\\)"); - Matcher matcher = pattern.matcher(dumpsys); - Assert.assertTrue("Cannot parse webview package info from: " + dumpsys, matcher.find()); - String[] packageInfo = matcher.group(1).split(","); - return new WebviewPackage(packageInfo[0].strip(), packageInfo[1].strip()); - } - - String getPackageName() { - return mPackageName; - } - - String getVersion() { - return mVersion; - } + mWebviewUtils.printWebviewVersion(); } private void assertAppLaunchNoCrash() throws DeviceNotAvailableException { - DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); - deviceUtils.resetPackage(mPackageName); + DeviceUtils.getInstance(getDevice()).resetPackage(mPackageName); TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); if (mRecordScreen) { @@ -269,9 +175,8 @@ public class WebviewAppLaunchTest extends BaseHostJUnit4Test { private void launchPackageAndCheckForCrash() throws DeviceNotAvailableException { CLog.d("Launching package: %s.", mPackageName); - DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData); - + DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice()); DeviceTimestamp startTime = deviceUtils.currentTimeMillis(); try { deviceUtils.launchPackage(mPackageName); diff --git a/test_scripts/src/main/java/com/android/webview/unittests/WebviewAppCompatUnitTests.java b/test_scripts/src/main/java/com/android/webview/unittests/WebviewAppCompatUnitTests.java new file mode 100644 index 0000000..7ade092 --- /dev/null +++ b/test_scripts/src/main/java/com/android/webview/unittests/WebviewAppCompatUnitTests.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.webview.unittests; + +import com.android.webview.tests.WebviewPackage; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RunWith(JUnit4.class) +public class WebviewAppCompatUnitTests { + private static final String WEBVIEW_PACKAGE = "com.android.webview"; + + @Test + public void testSuccessfulParseBuildFromDumpsys() { + String dumpsys = + "Current WebView package (name, version): (com.android.webview, 102.0.5005.125)\n" + + "Valid package com.android.webview " + + "(versionName: 102.0.5005.125, versionCode: 5005625," + + " targetSdkVersion: 33)"; + WebviewPackage webviewPackage = WebviewPackage.buildFromDumpsys(dumpsys); + Assert.assertEquals(webviewPackage.getPackageName(), "com.android.webview"); + Assert.assertEquals(webviewPackage.getVersion(), "102.0.5005.125"); + Assert.assertEquals(webviewPackage.getVersionCode(), 5005625); + } + + @Test + public void testSortWebviewPackages() { + String pkgName = "com.android.webview"; + List<WebviewPackage> webviewPackages = + Arrays.asList( + new WebviewPackage(WEBVIEW_PACKAGE, "101.0.4911.122", 4911122), + new WebviewPackage(WEBVIEW_PACKAGE, "100.0.3911.122", 3911122), + new WebviewPackage(WEBVIEW_PACKAGE, "104.0.7911.122", 7911122)) + .stream() + .sorted() + .collect(Collectors.toList()); // Sort by natural order + Assert.assertEquals( + "Webview Packages were not properly sorted by natural order", + webviewPackages, + Arrays.asList( + new WebviewPackage(WEBVIEW_PACKAGE, "100.0.3911.122", 3911122), + new WebviewPackage(WEBVIEW_PACKAGE, "101.0.4911.122", 4911122), + new WebviewPackage(WEBVIEW_PACKAGE, "104.0.7911.122", 7911122))); + } + + @Test + public void testWebViewPackagesAreEqual() { + Assert.assertEquals( + "Webview Packages were not equal", + new WebviewPackage(WEBVIEW_PACKAGE, "100.0.3911.122", 3911122), + new WebviewPackage(WEBVIEW_PACKAGE, "100.0.3911.122", 3911122)); + } + + @Test + public void testWebViewPackagesAreNotEqual() { + Assert.assertNotEquals( + "Webview Packages are equal", + new WebviewPackage(WEBVIEW_PACKAGE, "101.0.3911.122", 4911122), + new WebviewPackage(WEBVIEW_PACKAGE, "100.0.3911.122", 3911122)); + } +} diff --git a/test_targets/csuite-app-crawl/Android.bp b/test_targets/csuite-app-crawl/Android.bp index 6da9902..131e45d 100644 --- a/test_targets/csuite-app-crawl/Android.bp +++ b/test_targets/csuite-app-crawl/Android.bp @@ -19,5 +19,6 @@ package { csuite_test { name: "csuite-app-crawl", test_plan_include: "plan.xml", - test_config_template: "template.xml" + test_config_template: "ui-automator-crawl.xml", + extra_test_config_templates: ["espresso-crawl.xml", "pre-installed-crawl.xml"] } diff --git a/test_targets/csuite-app-crawl/espresso-crawl.xml b/test_targets/csuite-app-crawl/espresso-crawl.xml new file mode 100644 index 0000000..4c64a94 --- /dev/null +++ b/test_targets/csuite-app-crawl/espresso-crawl.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="C-Suite Crawler test configuration"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> + <option name="run-command" value="input keyevent KEYCODE_MENU"/> + <option name="run-command" value="input keyevent KEYCODE_HOME"/> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="set-option" value="package-name:{package}"/> + <option name="set-option" value="repack-apk:app\://{package}"/> + <option name="class" value="com.android.csuite.tests.AppCrawlTest" /> + </test> +</configuration>
\ No newline at end of file diff --git a/test_targets/csuite-app-crawl/template.xml b/test_targets/csuite-app-crawl/pre-installed-crawl.xml index 406cf68..52bca98 100644 --- a/test_targets/csuite-app-crawl/template.xml +++ b/test_targets/csuite-app-crawl/pre-installed-crawl.xml @@ -14,6 +14,7 @@ limitations under the License. --> <configuration description="C-Suite Crawler test configuration"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> <option name="run-command" value="input keyevent KEYCODE_MENU"/> @@ -21,7 +22,7 @@ </target_preparer> <test class="com.android.tradefed.testtype.HostTest" > <option name="set-option" value="package-name:{package}"/> - <option name="set-option" value="apk:app\://{package}"/> + <option name="set-option" value="ui-automator-mode:true"/> <option name="class" value="com.android.csuite.tests.AppCrawlTest" /> </test> </configuration>
\ No newline at end of file diff --git a/test_targets/csuite-app-crawl/ui-automator-crawl.xml b/test_targets/csuite-app-crawl/ui-automator-crawl.xml new file mode 100644 index 0000000..cc41031 --- /dev/null +++ b/test_targets/csuite-app-crawl/ui-automator-crawl.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="C-Suite Crawler test configuration"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> + <option name="run-command" value="input keyevent KEYCODE_MENU"/> + <option name="run-command" value="input keyevent KEYCODE_HOME"/> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="set-option" value="package-name:{package}"/> + <option name="set-option" value="install-apk:app\://{package}"/> + <option name="set-option" value="install-arg:-g"/> + <option name="set-option" value="ui-automator-mode:true"/> + <option name="class" value="com.android.csuite.tests.AppCrawlTest" /> + </test> +</configuration>
\ No newline at end of file diff --git a/test_targets/csuite-app-launch/Android.bp b/test_targets/csuite-app-launch/Android.bp index 5a25f8a..9bf1194 100644 --- a/test_targets/csuite-app-launch/Android.bp +++ b/test_targets/csuite-app-launch/Android.bp @@ -18,6 +18,6 @@ package { csuite_test { name: "csuite-app-launch", - test_config_template: "default.xml", - extra_test_config_templates: ["pre-installed-apps.xml"] + test_config_template: "default-launch.xml", + extra_test_config_templates: ["pre-installed-launch.xml"] } diff --git a/test_targets/csuite-app-launch/default.xml b/test_targets/csuite-app-launch/default-launch.xml index e583002..5dacb60 100644 --- a/test_targets/csuite-app-launch/default.xml +++ b/test_targets/csuite-app-launch/default-launch.xml @@ -15,6 +15,7 @@ --> <configuration description="Launches an app and check for crashes"> <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> <option name="run-command" value="input keyevent KEYCODE_MENU"/> @@ -23,6 +24,7 @@ <test class="com.android.tradefed.testtype.HostTest" > <option name="set-option" value="package-name:{package}"/> <option name="set-option" value="install-apk:app\://{package}"/> + <option name="set-option" value="install-arg:-g"/> <option name="class" value="com.android.csuite.tests.AppLaunchTest" /> </test> </configuration>
\ No newline at end of file diff --git a/test_targets/csuite-app-launch/pre-installed-apps.xml b/test_targets/csuite-app-launch/pre-installed-launch.xml index 5eaba02..9b1f03d 100644 --- a/test_targets/csuite-app-launch/pre-installed-apps.xml +++ b/test_targets/csuite-app-launch/pre-installed-launch.xml @@ -15,6 +15,7 @@ --> <configuration description="Launches an app that exists on the device and check for crashes"> <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> <option name="run-command" value="input keyevent KEYCODE_MENU"/> diff --git a/test_targets/csuite-pre-installed-app-launch/template.xml b/test_targets/csuite-pre-installed-app-launch/template.xml index 5eaba02..9b1f03d 100644 --- a/test_targets/csuite-pre-installed-app-launch/template.xml +++ b/test_targets/csuite-pre-installed-app-launch/template.xml @@ -15,6 +15,7 @@ --> <configuration description="Launches an app that exists on the device and check for crashes"> <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> <option name="run-command" value="input keyevent KEYCODE_MENU"/> diff --git a/test_targets/pixel-app-launch-lock-recentapp/Android.bp b/test_targets/pixel-app-launch-lock-recentapp/Android.bp new file mode 100644 index 0000000..720c249 --- /dev/null +++ b/test_targets/pixel-app-launch-lock-recentapp/Android.bp @@ -0,0 +1,22 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +csuite_test { + name: "pixel-app-launch-lock-recentapp", + test_config_template: "template.xml", +} diff --git a/test_targets/pixel-app-launch-lock-recentapp/OWNERS b/test_targets/pixel-app-launch-lock-recentapp/OWNERS new file mode 100644 index 0000000..aa4bbf9 --- /dev/null +++ b/test_targets/pixel-app-launch-lock-recentapp/OWNERS @@ -0,0 +1,2 @@ +murphykuo@google.com +elisahsu@google.com diff --git a/test_targets/pixel-app-launch-lock-recentapp/template.xml b/test_targets/pixel-app-launch-lock-recentapp/template.xml new file mode 100644 index 0000000..6f546a7 --- /dev/null +++ b/test_targets/pixel-app-launch-lock-recentapp/template.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2022 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Launches an app and lock/unlock the device, then Launches an app by tapping recent app key"> + <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="aapt-version" value="AAPT2" /> + <option name="test-file-name" value="app://{package}"/> + <option name="test-file-name" value="PixelAppCompTests.apk" /> + </target_preparer> + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <!-- repeatable: The key of the DIRECTORY to pull --> + <option name = "directory-keys" value = "/sdcard/logData" /> + </metrics_collector> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="package" value="com.android.pixel.tests" /> + <option name="include-filter" value="com.android.pixel.tests.AppLaunchLockTest" /> + <option name="include-filter" value="com.android.pixel.tests.AppLaunchRecentAppTest" /> + <option name="instrumentation-arg" key="package" value="{package}" /> + <option name="isolated-storage" value="false" /> + </test> +</configuration> diff --git a/test_targets/pixel-app-launch-lock/template.xml b/test_targets/pixel-app-launch-lock/template.xml index 7751426..75737c6 100644 --- a/test_targets/pixel-app-launch-lock/template.xml +++ b/test_targets/pixel-app-launch-lock/template.xml @@ -16,6 +16,7 @@ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> + <option name="aapt-version" value="AAPT2" /> <option name="test-file-name" value="app://{package}"/> <option name="test-file-name" value="PixelAppCompTests.apk" /> </target_preparer> diff --git a/test_targets/pixel-app-launch-recentapp/template.xml b/test_targets/pixel-app-launch-recentapp/template.xml index 86742b7..87e3bc7 100644 --- a/test_targets/pixel-app-launch-recentapp/template.xml +++ b/test_targets/pixel-app-launch-recentapp/template.xml @@ -16,6 +16,7 @@ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> + <option name="aapt-version" value="AAPT2" /> <option name="test-file-name" value="app://{package}"/> <option name="test-file-name" value="PixelAppCompTests.apk" /> </target_preparer> diff --git a/test_targets/pixel-app-launch-rotate/template.xml b/test_targets/pixel-app-launch-rotate/template.xml index ff7871a..f2ce7fd 100644 --- a/test_targets/pixel-app-launch-rotate/template.xml +++ b/test_targets/pixel-app-launch-rotate/template.xml @@ -16,6 +16,7 @@ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> + <option name="aapt-version" value="AAPT2" /> <option name="test-file-name" value="app://{package}"/> <option name="test-file-name" value="PixelAppCompTests.apk" /> </target_preparer> diff --git a/test_targets/webview-app-crawl/Android.bp b/test_targets/webview-app-crawl/Android.bp new file mode 100644 index 0000000..57d4192 --- /dev/null +++ b/test_targets/webview-app-crawl/Android.bp @@ -0,0 +1,23 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +csuite_test { + name: "webview-app-crawl", + test_plan_include: "plan.xml", + test_config_template: "ui-automator-mode.xml", +} diff --git a/test_targets/webview-app-crawl/OWNERS b/test_targets/webview-app-crawl/OWNERS new file mode 100644 index 0000000..af3a7c8 --- /dev/null +++ b/test_targets/webview-app-crawl/OWNERS @@ -0,0 +1,2 @@ +amitku@google.com +rmhasan@google.com
\ No newline at end of file diff --git a/test_targets/webview-app-crawl/plan.xml b/test_targets/webview-app-crawl/plan.xml new file mode 100644 index 0000000..83c3e05 --- /dev/null +++ b/test_targets/webview-app-crawl/plan.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="WebView C-Suite Crawler Test Plan"> + <target_preparer class="com.android.webview.tests.WebviewInstallerToolPreparer"/> + <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/> +</configuration> diff --git a/test_targets/webview-app-crawl/ui-automator-mode.xml b/test_targets/webview-app-crawl/ui-automator-mode.xml new file mode 100644 index 0000000..ec44190 --- /dev/null +++ b/test_targets/webview-app-crawl/ui-automator-mode.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Crawl's an app after installing WebView"> + <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller" /> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/> + <option name="run-command" value="input keyevent KEYCODE_MENU"/> + <option name="run-command" value="input keyevent KEYCODE_HOME"/> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="set-option" value="package-name:{package}"/> + <option name="set-option" value="install-apk:app\://{package}"/> + <option name="set-option" value="ui-automator-mode:true"/> + <option name="set-option" value="install-arg:-g"/> + <option name="class" value="com.android.webview.tests.WebviewAppCrawlTest" /> + </test> +</configuration> diff --git a/test_targets/webview-app-launch/Android.bp b/test_targets/webview-app-launch/Android.bp index 3658018..8d54421 100644 --- a/test_targets/webview-app-launch/Android.bp +++ b/test_targets/webview-app-launch/Android.bp @@ -18,5 +18,6 @@ package { csuite_test { name: "webview-app-launch", + test_plan_include: "plan.xml", test_config_template: "default.xml", } diff --git a/test_targets/webview-app-launch/plan.xml b/test_targets/webview-app-launch/plan.xml new file mode 100644 index 0000000..5d3c48f --- /dev/null +++ b/test_targets/webview-app-launch/plan.xml @@ -0,0 +1,3 @@ +<configuration description="WebView C-Suite Crawler Test Plan"> + <target_preparer class="com.android.webview.tests.WebviewInstallerToolPreparer"/> +</configuration> diff --git a/tools/csuite_test/csuite_test.go b/tools/csuite_test/csuite_test.go index 3a14b96..b9f698d 100644 --- a/tools/csuite_test/csuite_test.go +++ b/tools/csuite_test/csuite_test.go @@ -104,6 +104,7 @@ func (cSuiteTest *CSuiteTest) GenerateAndroidBuildActions(ctx android.ModuleCont if cSuiteTest.csuiteTestProperties.Test_config_template == nil { ctx.ModuleErrorf(`'test_config_template' is missing.`) + return } configTemplatePath := cSuiteTest.buildCopyConfigTemplateCommand(ctx, rule, *cSuiteTest.csuiteTestProperties.Test_config_template) diff --git a/tools/csuite_test/csuite_test_test.go b/tools/csuite_test/csuite_test_test.go index 9da6ffd..5e0878f 100644 --- a/tools/csuite_test/csuite_test_test.go +++ b/tools/csuite_test/csuite_test_test.go @@ -16,6 +16,7 @@ package csuite import ( "android/soong/android" + "android/soong/java" "io/ioutil" "os" "strings" @@ -25,108 +26,89 @@ import ( var buildDir string func TestBpContainsTestHostPropsThrowsError(t *testing.T) { - ctx, _ := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml", data_native_bins: "bin" } - `) - - _, errs := ctx.ParseBlueprintsFiles("Android.bp") - - android.FailIfNoMatchingErrors(t, `unrecognized property`, errs) + `, + "unrecognized property", + ) } func TestBpContainsManifestThrowsError(t *testing.T) { - ctx, _ := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml", test_config: "AndroidTest.xml" } - `) - - _, errs := ctx.ParseBlueprintsFiles("Android.bp") - - android.FailIfNoMatchingErrors(t, `unrecognized property`, errs) + `, + "unrecognized property", + ) } func TestBpMissingNameThrowsError(t *testing.T) { - ctx, _ := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { test_config_template: "config_template.xml" } - `) - - _, errs := ctx.ParseBlueprintsFiles("Android.bp") - - android.FailIfNoMatchingErrors(t, `'name' is missing`, errs) + `, + `'name' is missing`, + ) } func TestBpMissingTemplatePathThrowsError(t *testing.T) { - ctx, config := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { name: "plan_name", } - `) - - ctx.ParseBlueprintsFiles("Android.bp") - _, errs := ctx.PrepareBuildActions(config) - - android.FailIfNoMatchingErrors(t, `'test_config_template' is missing`, errs) + `, + `'test_config_template' is missing`, + ) } func TestBpTemplatePathUnexpectedFileExtensionThrowsError(t *testing.T) { - ctx, config := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml.template" } - `) - - ctx.ParseBlueprintsFiles("Android.bp") - _, errs := ctx.PrepareBuildActions(config) - - android.FailIfNoMatchingErrors(t, `Config template path should ends with .xml`, errs) + `, + `Config template path should ends with .xml`, + ) } func TestBpExtraTemplateUnexpectedFileExtensionThrowsError(t *testing.T) { - ctx, config := createContextAndConfig(t, ` + createContextAndConfigExpectingErrors(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml", extra_test_config_templates: ["another.xml.template"] } - `) - - ctx.ParseBlueprintsFiles("Android.bp") - _, errs := ctx.PrepareBuildActions(config) - - android.FailIfNoMatchingErrors(t, `Config template path should ends with .xml`, errs) + `, + `Config template path should ends with .xml`, + ) } func TestBpValidExtraTemplateDoesNotThrowError(t *testing.T) { - ctx, config := createContextAndConfig(t, ` + createContextAndConfig(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml", extra_test_config_templates: ["another.xml"] } `) - - parseBpAndBuild(t, ctx, config) } func TestValidBpMissingPlanIncludeDoesNotThrowError(t *testing.T) { - ctx, config := createContextAndConfig(t, ` + createContextAndConfig(t, ` csuite_test { name: "plan_name", test_config_template: "config_template.xml" } `) - - parseBpAndBuild(t, ctx, config) } func TestValidBpMissingPlanIncludeGeneratesPlanXmlWithoutPlaceholders(t *testing.T) { @@ -137,8 +119,6 @@ func TestValidBpMissingPlanIncludeGeneratesPlanXmlWithoutPlaceholders(t *testing } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if strings.Contains(content, "{") || strings.Contains(content, "}") { @@ -154,8 +134,6 @@ func TestGeneratedTestPlanContainsPlanName(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if !strings.Contains(content, "plan_name") { @@ -171,8 +149,6 @@ func TestGeneratedTestPlanContainsTemplatePath(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if !strings.Contains(content, "config/plan_name/config_template.xml.template") { @@ -189,8 +165,6 @@ func TestGeneratedTestPlanContainsExtraTemplatePath(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if !strings.Contains(content, "config/plan_name/extra.xml.template") { @@ -209,8 +183,6 @@ func TestGeneratedTestPlanDoesNotContainExtraTemplatePath(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if strings.Contains(content, "extra-templates") { @@ -226,8 +198,6 @@ func TestTemplateFileCopyRuleExists(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite") assertFileCopyRuleExists(t, params, "config_template.xml", "config/plan_name/config_template.xml.template") } @@ -241,8 +211,6 @@ func TestExtraTemplateFileCopyRuleExists(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite") assertFileCopyRuleExists(t, params, "config_template.xml", "config/plan_name/extra.xml.template") } @@ -256,8 +224,6 @@ func TestGeneratedTestPlanContainsPlanInclude(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common") content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml")) if !strings.Contains(content, `"includes/plan_name.xml"`) { @@ -274,8 +240,6 @@ func TestPlanIncludeFileCopyRuleExists(t *testing.T) { } `) - parseBpAndBuild(t, ctx, config) - params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite") assertFileCopyRuleExists(t, params, "include.xml", "config/includes/plan_name.xml") } @@ -291,14 +255,6 @@ func TestMain(m *testing.M) { os.Exit(run()) } -func parseBpAndBuild(t *testing.T, ctx *android.TestContext, config android.Config) { - _, parsingErrs := ctx.ParseBlueprintsFiles("Android.bp") - _, buildErrs := ctx.PrepareBuildActions(config) - - android.FailIfErrored(t, parsingErrs) - android.FailIfErrored(t, buildErrs) -} - func assertFileCopyRuleExists(t *testing.T, params android.TestingBuildParams, src string, dst string) { assertPathsContains(t, getAllInputPaths(params), src) assertWritablePathsContainsRel(t, getAllOutputPaths(params), dst) @@ -370,11 +326,25 @@ func tearDown() { } func createContextAndConfig(t *testing.T, bp string) (*android.TestContext, android.Config) { + return createContextAndConfigExpectingErrors(t, bp, "") +} + +func createContextAndConfigExpectingErrors(t *testing.T, bp string, error string) (*android.TestContext, android.Config) { t.Helper() - config := android.TestArchConfig(buildDir, nil, bp, nil) - ctx := android.NewTestArchContext(config) - ctx.RegisterModuleType("csuite_test", CSuiteTestFactory) - ctx.Register() - return ctx, config + testPreparer := android.GroupFixturePreparers( + java.PrepareForTestWithJavaDefaultModules, + android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) { + ctx.RegisterModuleType("csuite_test", CSuiteTestFactory) + }), + android.FixtureWithRootAndroidBp(bp), + ) + + if error != "" { + testPreparer = testPreparer.ExtendWithErrorHandler(android.FixtureExpectsOneErrorPattern(error)) + } + + result := testPreparer.RunTest(t) + + return result.TestContext, result.Config } diff --git a/tools/script/Android.bp b/tools/script/Android.bp index c1eb990..a9832f9 100644 --- a/tools/script/Android.bp +++ b/tools/script/Android.bp @@ -22,9 +22,6 @@ python_binary_host { srcs: [ "generate_module.py", ], - defaults: [ - "csuite_python_defaults", - ], } python_test_host { @@ -41,7 +38,4 @@ python_test_host { test_options: { unit_test: true, }, - defaults: [ - "csuite_python_defaults", - ], } |