aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:12:34 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:12:34 +0000
commitfc56cd83d35949518dc3ef504f36eecb097a0924 (patch)
tree033dc92ba1dfd9d3f438e4ee99f4897a4ac0644f
parent9a9db75f4baa82556b9dd3c486321117c7ccaaf9 (diff)
parente43fe576896287dcdd51be7fa2699f3e4822a9ba (diff)
downloadcsuite-android14-mainline-wifi-release.tar.gz
Snap for 10447354 from e43fe576896287dcdd51be7fa2699f3e4822a9ba to mainline-wifi-releaseaml_wif_341410080aml_wif_341310010aml_wif_341110010aml_wif_341011010aml_wif_340913010android14-mainline-wifi-release
Change-Id: I810a29bcbe5a53e60ac87bd9c2848fc159bc9f36
-rw-r--r--Android.bp12
-rw-r--r--harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java2
-rw-r--r--harness/src/main/java/com/android/csuite/core/ApkInstaller.java152
-rw-r--r--harness/src/main/java/com/android/csuite/core/AppCrawlTester.java292
-rw-r--r--harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java29
-rw-r--r--harness/src/main/java/com/android/csuite/core/DeviceUtils.java372
-rw-r--r--harness/src/main/java/com/android/csuite/core/RoboLoginConfigProvider.java92
-rw-r--r--harness/src/main/java/com/android/csuite/core/TestUtils.java166
-rw-r--r--harness/src/main/resources/config/csuite-base.xml6
-rw-r--r--harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java47
-rw-r--r--harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java21
-rw-r--r--harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java213
-rw-r--r--harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java384
-rw-r--r--harness/src/test/java/com/android/csuite/core/TestUtilsTest.java357
-rw-r--r--integration_tests/Android.bp9
-rw-r--r--integration_tests/csuite_test_utils.py13
-rw-r--r--pylib/Android.bp3
-rw-r--r--test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java136
-rw-r--r--test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java36
-rw-r--r--test_scripts/src/main/java/com/android/pixel/OWNERS2
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java12
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java11
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java14
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java16
-rw-r--r--test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java30
-rw-r--r--test_scripts/src/main/java/com/android/webview/Android.bp37
-rw-r--r--test_scripts/src/main/java/com/android/webview/lib/WebviewInstallerToolPreparer.java214
-rw-r--r--test_scripts/src/main/java/com/android/webview/lib/WebviewPackage.java109
-rw-r--r--test_scripts/src/main/java/com/android/webview/lib/WebviewUtils.java136
-rw-r--r--test_scripts/src/main/java/com/android/webview/tests/WebviewAppCrawlTest.java267
-rw-r--r--test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java159
-rw-r--r--test_scripts/src/main/java/com/android/webview/unittests/WebviewAppCompatUnitTests.java82
-rw-r--r--test_targets/csuite-app-crawl/Android.bp3
-rw-r--r--test_targets/csuite-app-crawl/espresso-crawl.xml28
-rw-r--r--test_targets/csuite-app-crawl/pre-installed-crawl.xml (renamed from test_targets/csuite-app-crawl/template.xml)3
-rw-r--r--test_targets/csuite-app-crawl/ui-automator-crawl.xml30
-rw-r--r--test_targets/csuite-app-launch/Android.bp4
-rw-r--r--test_targets/csuite-app-launch/default-launch.xml (renamed from test_targets/csuite-app-launch/default.xml)2
-rw-r--r--test_targets/csuite-app-launch/pre-installed-launch.xml (renamed from test_targets/csuite-app-launch/pre-installed-apps.xml)1
-rw-r--r--test_targets/csuite-pre-installed-app-launch/template.xml1
-rw-r--r--test_targets/pixel-app-launch-lock-recentapp/Android.bp22
-rw-r--r--test_targets/pixel-app-launch-lock-recentapp/OWNERS2
-rw-r--r--test_targets/pixel-app-launch-lock-recentapp/template.xml35
-rw-r--r--test_targets/pixel-app-launch-lock/template.xml1
-rw-r--r--test_targets/pixel-app-launch-recentapp/template.xml1
-rw-r--r--test_targets/pixel-app-launch-rotate/template.xml1
-rw-r--r--test_targets/webview-app-crawl/Android.bp23
-rw-r--r--test_targets/webview-app-crawl/OWNERS2
-rw-r--r--test_targets/webview-app-crawl/plan.xml19
-rw-r--r--test_targets/webview-app-crawl/ui-automator-mode.xml31
-rw-r--r--test_targets/webview-app-launch/Android.bp1
-rw-r--r--test_targets/webview-app-launch/plan.xml3
-rw-r--r--tools/csuite_test/csuite_test.go1
-rw-r--r--tools/csuite_test/csuite_test_test.go122
-rw-r--r--tools/script/Android.bp6
55 files changed, 3240 insertions, 533 deletions
diff --git a/Android.bp b/Android.bp
index 5295e32..d6691e4 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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",
- ],
}