Merge Android R
Bug: 168057903
Merged-In: Ide482c0b5b58c9b846a1bb9f33a870f77be7b551
Change-Id: Ib1881c8c913b0d66e6d5050cb4ba378530ffe05f
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..948fa79
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,10 @@
+[Builtin Hooks]
+bpfmt = true
+google_java_format = true
+pylint = true
+xmllint = true
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py
+ --sha ${PREUPLOAD_COMMIT}
+ --config_xml config/checkstyle/checkstyle.xml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..955adc1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# Android App Compatibility Test Suite (C-Suite)
+
+C-Suite consists of the testing framework and test cases, designed to detect and report app
+compatibility issues.
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..1eb7457
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
+ <!ENTITY defaultCopyrightCheck SYSTEM "../../../../../prebuilts/checkstyle/default-copyright-check.xml">
+ <!ENTITY defaultJavadocChecks SYSTEM "../../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
+ <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
+ <!ENTITY defaultModuleChecks SYSTEM "../../../../../prebuilts/checkstyle/default-module-checks.xml">
+]>
+
+<module name="Checker">
+ &defaultModuleChecks;
+ &defaultCopyrightCheck;
+ <module name="TreeWalker">
+ &defaultJavadocChecks;
+ &defaultTreewalkerChecks;
+ </module>
+ <module name="SuppressionFilter">
+ <property name="file" value="config/checkstyle/suppressions.xml" />
+ </module>
+</module>
diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml
new file mode 100644
index 0000000..218bc02
--- /dev/null
+++ b/config/checkstyle/suppressions.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE suppressions PUBLIC "-//Puppy Crawl//DTD Suppressions 1.1//EN" "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
+<suppressions>
+
+ <!-- Let google-java-format handle the indentation. -->
+ <suppress files=".*" checks="Indentation" />
+
+</suppressions>
diff --git a/harness/Android.bp b/harness/Android.bp
new file mode 100644
index 0000000..2dec437
--- /dev/null
+++ b/harness/Android.bp
@@ -0,0 +1,43 @@
+// Copyright (C) 2018 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.
+
+java_library_host {
+ name: "csuite-harness",
+ srcs: [
+ "src/main/java/**/*.java",
+ ],
+ java_resource_dirs: [
+ "src/main/resources",
+ ],
+ libs: [
+ "tradefed",
+ ],
+}
+
+java_test_host {
+ name: "csuite-harness-tests",
+ srcs: [
+ "src/test/java/**/*.java",
+ ],
+ libs: [
+ "csuite-harness",
+ "tradefed",
+ ],
+ static_libs: [
+ "mockito-host",
+ "objenesis",
+ "testng",
+ ],
+ test_suites: ["general-tests"],
+}
diff --git a/harness/AndroidTest.xml b/harness/AndroidTest.xml
new file mode 100644
index 0000000..f3fb637
--- /dev/null
+++ b/harness/AndroidTest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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="Executes the C-Suite harness unit tests">
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="class" value="com.android.compatibility.CSuiteUnitTests" />
+ </test>
+</configuration>
diff --git a/harness/TEST_MAPPING b/harness/TEST_MAPPING
new file mode 100644
index 0000000..50ed1cd
--- /dev/null
+++ b/harness/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+ "postsubmit": [
+ {
+ "name": "csuite-harness-tests",
+ "host": true
+ }
+ ]
+}
diff --git a/harness/src/main/java/com/android/compatibility/AppCompatibilityTest.java b/harness/src/main/java/com/android/compatibility/AppCompatibilityTest.java
new file mode 100644
index 0000000..74c4907
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/AppCompatibilityTest.java
@@ -0,0 +1,671 @@
+/*
+ * Copyright (C) 2012 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.compatibility;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionCopier;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceUnresponsiveException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.LogcatReceiver;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.CompatibilityTestResult;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IShardableTest;
+import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.PublicApkUtil;
+import com.android.tradefed.util.PublicApkUtil.ApkInfo;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.StreamUtil;
+
+import com.google.common.base.Strings;
+
+import org.json.JSONException;
+import org.junit.Assert;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Test that determines application compatibility. The test iterates through the apks in a given
+ * directory. The test installs, launches, and uninstalls each apk.
+ */
+public abstract class AppCompatibilityTest
+ implements IDeviceTest,
+ IRemoteTest,
+ IShardableTest,
+ IConfigurationReceiver,
+ ITestFilterReceiver {
+
+ @Option(
+ name = "product",
+ description = "The product, corresponding to the borgcron job product arg.")
+ private String mProduct;
+
+ @Option(
+ name = "base-dir",
+ description = "The directory of the results excluding the date.",
+ importance = Option.Importance.ALWAYS)
+ // TODO(b/36786754): Add `mandatory = true` when cmdfiles are moved over
+ private File mBaseDir;
+
+ @Option(
+ name = "date",
+ description =
+ "The date to run, in the form YYYYMMDD. If not set, then the latest "
+ + "results will be used.")
+ private String mDate;
+
+ @Option(name = "test-label", description = "Unique test identifier label.")
+ private String mTestLabel = "AppCompatibility";
+
+ @Option(
+ name = "reboot-after-apks",
+ description = "Reboot the device after a centain number of apks. 0 means no reboot.")
+ private int mRebootNumber = 100;
+
+ @Option(
+ name = "fallback-to-apk-scan",
+ description =
+ "Fallback to scanning for apks in base directory if ranking information "
+ + "is missing.")
+ private boolean mFallbackToApkScan = false;
+
+ @Option(
+ name = "retry-count",
+ description = "Number of times to retry a failed test case. 0 means no retry.")
+ private int mRetryCount = 5;
+
+ @Option(name = "include-filter", description = "The include filter of the test names to run.")
+ protected Set<String> mIncludeFilters = new HashSet<>();
+
+ @Option(name = "exclude-filter", description = "The exclude filter of the test names to run.")
+ protected Set<String> mExcludeFilters = new HashSet<>();
+
+ private static final long DOWNLOAD_TIMEOUT_MS = 60 * 1000;
+ private static final int DOWNLOAD_RETRIES = 3;
+ private static final long JOIN_TIMEOUT_MS = 5 * 60 * 1000;
+ private static final int LOGCAT_SIZE_BYTES = 20 * 1024 * 1024;
+
+ private ITestDevice mDevice;
+ private LogcatReceiver mLogcat;
+ private IConfiguration mConfiguration;
+
+ // The number of tests run so far
+ private int mTestCount = 0;
+
+ // indicates the current sharding setup
+ private int mShardCount = 1;
+ private int mShardIndex = 0;
+
+ protected final String mLauncherPackage;
+ protected final String mRunnerClass;
+ protected final String mPackageBeingTestedKey;
+
+ protected AppCompatibilityTest(
+ String launcherPackage, String runnerClass, String packageBeingTestedKey) {
+ this.mLauncherPackage = launcherPackage;
+ this.mRunnerClass = runnerClass;
+ this.mPackageBeingTestedKey = packageBeingTestedKey;
+ }
+
+ /**
+ * Creates and sets up an instrumentation test with information about the test runner as well as
+ * the package being tested (provided as a parameter).
+ */
+ protected abstract InstrumentationTest createInstrumentationTest(String packageBeingTested);
+
+ /** Sets up some default aspects of the instrumentation test. */
+ protected final InstrumentationTest createDefaultInstrumentationTest(
+ String packageBeingTested) {
+ InstrumentationTest instrTest = new InstrumentationTest();
+ instrTest.setPackageName(mLauncherPackage);
+ instrTest.setConfiguration(mConfiguration);
+ instrTest.addInstrumentationArg(mPackageBeingTestedKey, packageBeingTested);
+ instrTest.setRunnerName(mRunnerClass);
+ return instrTest;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public final void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ CLog.d("Start of launch test run method. base-dir: %s", mBaseDir);
+ Assert.assertNotNull("Base dir cannot be null", mBaseDir);
+ Assert.assertTrue("Base dir should be a directory", mBaseDir.isDirectory());
+
+ if (mProduct == null) {
+ mProduct = mDevice.getProductType();
+ CLog.i("\"--product\" not specified, using property from device instead: %s", mProduct);
+ }
+ Assert.assertTrue(
+ String.format(
+ "Shard index out of range: expected [0, %d), got %d",
+ mShardCount, mShardIndex),
+ mShardIndex >= 0 && mShardIndex < mShardCount);
+
+ File apkDir = null;
+ try {
+ apkDir = PublicApkUtil.constructApkDir(mBaseDir.getPath(), mDate);
+ } catch (IOException e) {
+ CLog.e(e);
+ throw new RuntimeException(e);
+ }
+ CLog.d("apkDir: %s.", apkDir);
+ Assert.assertNotNull("Could not find the output dir", apkDir);
+ List<ApkInfo> apkList = null;
+ try {
+ apkList = shardApkList(PublicApkUtil.getApkList(mProduct, apkDir, mFallbackToApkScan));
+ } catch (IOException e) {
+ CLog.e(e);
+ throw new RuntimeException(e);
+ }
+ CLog.d("Completed sharding apkList. Number of items: %s", apkList.size());
+ Assert.assertNotNull("Could not download apk list", apkList);
+
+ apkList = filterApk(apkList);
+ CLog.d("Completed filtering apkList. Number of items: %s", apkList.size());
+
+ long start = System.currentTimeMillis();
+ listener.testRunStarted(mTestLabel, apkList.size());
+ mLogcat = new LogcatReceiver(getDevice(), LOGCAT_SIZE_BYTES, 0);
+ mLogcat.start();
+
+ try {
+ downloadAndTestApks(listener, apkDir, apkList);
+ } catch (InterruptedException e) {
+ CLog.e(e);
+ throw new RuntimeException(e);
+ } finally {
+ mLogcat.stop();
+ listener.testRunEnded(
+ System.currentTimeMillis() - start, new HashMap<String, Metric>());
+ }
+ }
+
+ /**
+ * Downloads and tests all the APKs in the apk list.
+ *
+ * @param listener The {@link ITestInvocationListener}.
+ * @param kharonDir The {@link File} of the CNS dir containing the APKs.
+ * @param apkList The sharded list of {@link ApkInfo} objects.
+ * @throws DeviceNotAvailableException
+ * @throws InterruptedException if a download thread was interrupted.
+ */
+ private void downloadAndTestApks(
+ ITestInvocationListener listener, File kharonDir, List<ApkInfo> apkList)
+ throws DeviceNotAvailableException, InterruptedException {
+ CLog.d("Started downloading and testing apks.");
+ ApkInfo testingApk = null;
+ File testingFile = null;
+ for (ApkInfo downloadingApk : apkList) {
+ ApkDownloadRunnable downloader = new ApkDownloadRunnable(kharonDir, downloadingApk);
+ Thread downloadThread = new Thread(downloader);
+ downloadThread.start();
+
+ testApk(listener, testingApk, testingFile);
+
+ try {
+ downloadThread.join(JOIN_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ FileUtil.deleteFile(downloader.getDownloadedFile());
+ throw e;
+ }
+ testingApk = downloadingApk;
+ testingFile = downloader.getDownloadedFile();
+ }
+ // One more time since the first time through the loop we don't test
+ testApk(listener, testingApk, testingFile);
+ CLog.d("Completed downloading and testing apks.");
+ }
+
+ /**
+ * Attempts to install and launch an APK and reports the results.
+ *
+ * @param listener The {@link ITestInvocationListener}.
+ * @param apkInfo The {@link ApkInfo} to run the test against.
+ * @param apkFile The downloaded {@link File}.
+ * @throws DeviceNotAvailableException
+ */
+ private void testApk(ITestInvocationListener listener, ApkInfo apkInfo, File apkFile)
+ throws DeviceNotAvailableException {
+ if (apkInfo == null || apkFile == null) {
+ CLog.d("apkInfo or apkFile is null.");
+ FileUtil.deleteFile(apkFile);
+ return;
+ }
+ CLog.d(
+ "Started testing package: %s, apk file: %s.",
+ apkInfo.packageName, apkFile.getAbsolutePath());
+
+ mTestCount++;
+ if (mRebootNumber != 0 && mTestCount % mRebootNumber == 0) {
+ mDevice.reboot();
+ }
+ mLogcat.clear();
+
+ TestDescription testId = createTestDescription(apkInfo.packageName);
+ listener.testStarted(testId, System.currentTimeMillis());
+
+ CompatibilityTestResult result = new CompatibilityTestResult();
+ result.rank = apkInfo.rank;
+ // Default to package name since name is a required field. This will be replaced by
+ // AaptParser in installApk()
+ result.name = apkInfo.packageName;
+ result.packageName = apkInfo.packageName;
+ result.versionString = apkInfo.versionString;
+ result.versionCode = apkInfo.versionCode;
+
+ try {
+ // Install the app, and also skip aapt check if we've fell back to apk scan
+ installApk(result, apkFile, mFallbackToApkScan);
+ boolean installationSuccess = result.status == null;
+
+ for (int i = 0; i <= mRetryCount; i++) {
+ if (installationSuccess) {
+ // Clear test result between retries
+ result.status = null;
+ result.message = null;
+ launchApk(result);
+ mDevice.executeShellCommand(
+ String.format("am force-stop %s", apkInfo.packageName));
+ }
+ if (result.status == null) {
+ result.status = CompatibilityTestResult.STATUS_SUCCESS;
+ break;
+ }
+ }
+
+ if (installationSuccess) {
+ mDevice.uninstallPackage(result.packageName);
+ }
+ } finally {
+ reportResult(listener, testId, result);
+ try {
+ postLogcat(result, listener);
+ } catch (JSONException e) {
+ CLog.w("Posting failed: %s.", e.getMessage());
+ }
+ listener.testEnded(
+ testId, System.currentTimeMillis(), Collections.<String, String>emptyMap());
+ FileUtil.deleteFile(apkFile);
+ CLog.d("Completed testing package: %s.", apkInfo.packageName);
+ }
+ }
+
+ /**
+ * Checks that the file is correct and attempts to install it.
+ *
+ * <p>Will set the result status to error if the APK could not be installed or if it contains
+ * conflicting information.
+ *
+ * @param result the {@link CompatibilityTestResult} containing the APK info.
+ * @param apkFile the APK file to install.
+ * @throws DeviceNotAvailableException
+ */
+ private void installApk(CompatibilityTestResult result, File apkFile, boolean skipAaptCheck)
+ throws DeviceNotAvailableException {
+ if (!skipAaptCheck) {
+ CLog.d("Parsing apk file: %s.", apkFile.getAbsolutePath());
+ AaptParser parser = AaptParser.parse(apkFile);
+ if (parser == null) {
+ CLog.d(
+ "Failed to parse apk file: %s, package: %s, error: %s.",
+ apkFile.getAbsolutePath(), result.packageName, result.message);
+ result.status = CompatibilityTestResult.STATUS_ERROR;
+ result.message = "aapt fail";
+ return;
+ }
+
+ result.name = parser.getLabel();
+
+ if (!equalsOrNull(result.packageName, parser.getPackageName())
+ || !equalsOrNull(result.versionString, parser.getVersionName())
+ || !equalsOrNull(result.versionCode, parser.getVersionCode())) {
+ CLog.d(
+ "Package info mismatch: want %s v%s (%s), got %s v%s (%s)",
+ result.packageName,
+ result.versionCode,
+ result.versionString,
+ parser.getPackageName(),
+ parser.getVersionCode(),
+ parser.getVersionName());
+ result.status = CompatibilityTestResult.STATUS_ERROR;
+ result.message = "package info mismatch";
+ return;
+ }
+ CLog.d("Completed parsing apk file: %s.", apkFile.getAbsolutePath());
+ }
+
+ try {
+ String error = mDevice.installPackage(apkFile, true);
+ if (error != null) {
+ result.status = CompatibilityTestResult.STATUS_ERROR;
+ result.message = error;
+ CLog.d(
+ "Failed to install apk file: %s, package: %s, error: %s.",
+ apkFile.getAbsolutePath(), result.packageName, result.message);
+ return;
+ }
+ } catch (DeviceUnresponsiveException e) {
+ result.status = CompatibilityTestResult.STATUS_ERROR;
+ result.message = "install timeout";
+ CLog.d(
+ "Installing apk file %s timed out, package: %s, error: %s.",
+ apkFile.getAbsolutePath(), result.packageName, result.message);
+ return;
+ }
+ CLog.d("Completed installing apk file %s.", apkFile.getAbsolutePath());
+ }
+
+ /**
+ * Method which attempts to launch an APK.
+ *
+ * <p>Will set the result status to failure if the APK could not be launched.
+ *
+ * @param result the {@link CompatibilityTestResult} containing the APK info.
+ * @throws DeviceNotAvailableException
+ */
+ private void launchApk(CompatibilityTestResult result) throws DeviceNotAvailableException {
+ CLog.d("Lauching package: %s.", result.packageName);
+ InstrumentationTest instrTest = createInstrumentationTest(result.packageName);
+ instrTest.setDevice(mDevice);
+
+ FailureCollectingListener failureListener = new FailureCollectingListener();
+ instrTest.run(failureListener);
+
+ if (failureListener.getStackTrace() != null) {
+ CLog.w("Failed to launch package: %s.", result.packageName);
+ result.status = CompatibilityTestResult.STATUS_FAILURE;
+ result.message = failureListener.getStackTrace();
+ }
+
+ CLog.d("Completed launching package: %s", result.packageName);
+ }
+
+ /** Helper method which reports a test failed if the status is either a failure or an error. */
+ private void reportResult(
+ ITestInvocationListener listener, TestDescription id, CompatibilityTestResult result) {
+ String message = result.message != null ? result.message : "unknown";
+ if (CompatibilityTestResult.STATUS_ERROR.equals(result.status)) {
+ listener.testFailed(id, "ERROR:" + message);
+ } else if (CompatibilityTestResult.STATUS_FAILURE.equals(result.status)) {
+ listener.testFailed(id, "FAILURE:" + message);
+ }
+ }
+
+ /** Helper method which posts the logcat. */
+ private void postLogcat(CompatibilityTestResult result, ITestInvocationListener listener)
+ throws JSONException {
+ InputStreamSource stream = null;
+ String header =
+ String.format(
+ "%s%s%s\n",
+ CompatibilityTestResult.SEPARATOR,
+ result.toJsonString(),
+ CompatibilityTestResult.SEPARATOR);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (InputStreamSource logcatData = mLogcat.getLogcatData()) {
+ try {
+ baos.write(header.getBytes());
+ StreamUtil.copyStreams(logcatData.createInputStream(), baos);
+ stream = new ByteArrayInputStreamSource(baos.toByteArray());
+ baos.flush();
+ baos.close();
+ } catch (IOException e) {
+ CLog.e("error inserting compatibility test result into logcat");
+ CLog.e(e);
+ // fallback to logcat data
+ stream = logcatData;
+ }
+ listener.testLog("logcat_" + result.packageName, LogDataType.LOGCAT, stream);
+ } finally {
+ StreamUtil.cancel(stream);
+ }
+ }
+
+ /** Helper method which takes a list of {@link ApkInfo} objects and returns the sharded list. */
+ private List<ApkInfo> shardApkList(List<ApkInfo> apkList) {
+ List<ApkInfo> shardedList = new ArrayList<>(apkList.size() / mShardCount + 1);
+ for (int i = mShardIndex; i < apkList.size(); i += mShardCount) {
+ shardedList.add(apkList.get(i));
+ }
+ return shardedList;
+ }
+
+ /**
+ * Helper method which takes a list of {@link ApkInfo} objects and returns the filtered list.
+ */
+ protected List<ApkInfo> filterApk(List<ApkInfo> apkList) {
+ List<ApkInfo> filteredList = new ArrayList<>();
+
+ for (ApkInfo apk : apkList) {
+ if (filterTest(apk.packageName)) {
+ filteredList.add(apk);
+ }
+ }
+
+ return filteredList;
+ }
+
+ /**
+ * Return true if a test matches one or more of the include filters AND does not match any of
+ * the exclude filters. If no include filters are given all tests should return true as long as
+ * they do not match any of the exclude filters.
+ */
+ protected boolean filterTest(String testName) {
+ if (mExcludeFilters.contains(testName)) {
+ return false;
+ }
+ if (mIncludeFilters.size() == 0 || mIncludeFilters.contains(testName)) {
+ return true;
+ }
+ return false;
+ }
+
+ /** Returns true if either object is null or if both objects are equal. */
+ private static boolean equalsOrNull(Object a, Object b) {
+ return a == null || b == null || a.equals(b);
+ }
+
+ /** Helper {@link Runnable} which downloads a file, and can be used in another thread. */
+ private class ApkDownloadRunnable implements Runnable {
+ private final File mKharonDir;
+ private final ApkInfo mApkInfo;
+
+ private File mDownloadedFile = null;
+
+ ApkDownloadRunnable(File kharonDir, ApkInfo apkInfo) {
+ mKharonDir = kharonDir;
+ mApkInfo = apkInfo;
+ }
+
+ @Override
+ public void run() {
+ // No-op if mApkInfo is null
+ if (mApkInfo == null) {
+ CLog.d("ApkInfo is null.");
+ return;
+ }
+
+ File sourceFile = new File(mKharonDir, mApkInfo.fileName);
+ try {
+ mDownloadedFile =
+ PublicApkUtil.downloadFile(
+ sourceFile, DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
+ } catch (IOException e) {
+ // Log and ignore
+ CLog.e("Could not download apk from %s.", sourceFile);
+ CLog.e(e);
+ }
+ CLog.d("Completed downloading apk file: %s.", mDownloadedFile.getAbsolutePath());
+ }
+
+ public File getDownloadedFile() {
+ return mDownloadedFile;
+ }
+ }
+
+ @Override
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ mDevice = device;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public ITestDevice getDevice() {
+ return mDevice;
+ }
+
+ /** Return a {@link IRunUtil} instance to execute commands with. */
+ IRunUtil getRunUtil() {
+ return RunUtil.getDefault();
+ }
+
+ private IRemoteTest getTestShard(int shardCount, int shardIndex) {
+ AppCompatibilityTest shard;
+ try {
+ shard = getClass().newInstance();
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new IllegalStateException(
+ "The class "
+ + getClass().getName()
+ + " has no public constructor with no arguments, but all subclasses of "
+ + AppCompatibilityTest.class.getName()
+ + " should",
+ e);
+ }
+ try {
+ OptionCopier.copyOptions(this, shard);
+ } catch (ConfigurationException e) {
+ CLog.e("Failed to copy test options: %s.", e.getMessage());
+ }
+ shard.mShardIndex = shardIndex;
+ shard.mShardCount = shardCount;
+ return shard;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Collection<IRemoteTest> split(int shardCountHint) {
+ if (shardCountHint <= 1) {
+ // cannot shard or already sharded
+ return null;
+ }
+ Collection<IRemoteTest> shards = new ArrayList<>(shardCountHint);
+ for (int index = 0; index < shardCountHint; index++) {
+ shards.add(getTestShard(shardCountHint, index));
+ }
+ return shards;
+ }
+
+ /**
+ * Get a test description for use in logging. For compatibility with logs, this should be
+ * TestDescription(launcher package, package being run).
+ */
+ private TestDescription createTestDescription(String packageBeingTested) {
+ return new TestDescription(mLauncherPackage, packageBeingTested);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addIncludeFilter(String filter) {
+ checkArgument(!Strings.isNullOrEmpty(filter), "Include filter cannot be null or empty.");
+ mIncludeFilters.add(filter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addAllIncludeFilters(Set<String> filters) {
+ checkNotNull(filters, "Include filters cannot be null.");
+ mIncludeFilters.addAll(filters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clearIncludeFilters() {
+ mIncludeFilters.clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<String> getIncludeFilters() {
+ return Collections.unmodifiableSet(mIncludeFilters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addExcludeFilter(String filter) {
+ checkArgument(!Strings.isNullOrEmpty(filter), "Exclude filter cannot be null or empty.");
+ mExcludeFilters.add(filter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addAllExcludeFilters(Set<String> filters) {
+ checkNotNull(filters, "Exclude filters cannot be null.");
+ mExcludeFilters.addAll(filters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clearExcludeFilters() {
+ mExcludeFilters.clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<String> getExcludeFilters() {
+ return Collections.unmodifiableSet(mExcludeFilters);
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/AppCrawlerCompatibilityTest.java b/harness/src/main/java/com/android/compatibility/AppCrawlerCompatibilityTest.java
new file mode 100644
index 0000000..5c0fe11
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/AppCrawlerCompatibilityTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 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.compatibility;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.InstrumentationTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@OptionClass(alias = "app-compatibility-crawler")
+public final class AppCrawlerCompatibilityTest extends AppCompatibilityTest {
+ private static final String WALKMAN_RUN_MS_LABEL = "maxDuration";
+ private static final String WALKMAN_STEPS_LABEL = "maxSteps";
+
+ @Option(
+ name = "walkman-run-ms",
+ description = "Time to run walkman in msecs (only used if test-strategy=walkman).")
+ private int mWalkmanRunMs = 60 * 1000;
+
+ @Option(
+ name = "walkman-steps",
+ description =
+ "Max number of steps to run walkman (only used if test-strategy=walkman)."
+ + " -1 for no limit")
+ private int mWalkmanSteps = -1;
+
+ public AppCrawlerCompatibilityTest() {
+ super(
+ "com.google.android.apps.common.walkman.apps",
+ "com.google.android.apps.common.testing.testrunner"
+ + ".Google3InstrumentationTestRunner",
+ /*
+ * We are using /google/data/ro/teams/walkman/walkman.apk which has parameter
+ * "packages" unlike the up-to-date version in source which uses "package"
+ * see: com.google.android.apps.common.walkman.apps.EngineFactory::getCrawlEngine.
+ * This currently works with the up-to-date version in source, as well.
+ *
+ * Neither of these should be confused with "package_to_launch", which is used by
+ * AppCompatibilityRunner
+ */
+ "packages");
+ }
+
+ @Override
+ public InstrumentationTest createInstrumentationTest(String packageBeingTested) {
+ InstrumentationTest instrTest = createDefaultInstrumentationTest(packageBeingTested);
+
+ instrTest.addInstrumentationArg(WALKMAN_RUN_MS_LABEL, Integer.toString(mWalkmanRunMs));
+ instrTest.addInstrumentationArg(WALKMAN_STEPS_LABEL, Integer.toString(mWalkmanSteps));
+
+ String launcherClass = mLauncherPackage + ".WalkmanInstrumentationEntry";
+ instrTest.setClassName(launcherClass);
+ /*
+ * InstrumentationTest can't deduce the exact test to run, so we specify it
+ * manually. Note that the TestDescription we use here is a different one from
+ * the one returned by {@link TestStrategy#createTestDescription}.
+ *
+ * This list is required to be mutable, so we wrap in ArrayList.
+ */
+ instrTest.setTestsToRun(
+ new ArrayList<>(Arrays.asList(new TestDescription(launcherClass, "testEntry"))));
+
+ return instrTest;
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/AppLaunchCompatibilityTest.java b/harness/src/main/java/com/android/compatibility/AppLaunchCompatibilityTest.java
new file mode 100644
index 0000000..dd1f648
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/AppLaunchCompatibilityTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 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.compatibility;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.testtype.InstrumentationTest;
+
+/** Uses AppCompatibilityRunner to check if the app starts correctly. */
+@OptionClass(alias = "app-compatibility")
+public final class AppLaunchCompatibilityTest extends AppCompatibilityTest {
+ private static final String APP_LAUNCH_TIMEOUT_LABEL = "app_launch_timeout_ms";
+ private static final String WORKSPACE_LAUNCH_TIMEOUT_LABEL = "workspace_launch_timeout_ms";
+
+ @Option(
+ name = "app-launch-timeout-ms",
+ description = "Time to wait for app to launch in msecs.")
+ private int mAppLaunchTimeoutMs = 15000;
+
+ @Option(
+ name = "workspace-launch-timeout-ms",
+ description = "Time to wait when launched back into the workspace in msecs.")
+ private int mWorkspaceLaunchTimeoutMs = 2000;
+
+ public AppLaunchCompatibilityTest() {
+ super(
+ "com.android.compatibilitytest",
+ "com.android.compatibilitytest.AppCompatibilityRunner",
+ "package_to_launch");
+ }
+
+ @Override
+ public InstrumentationTest createInstrumentationTest(String packageBeingTested) {
+ InstrumentationTest instrTest = createDefaultInstrumentationTest(packageBeingTested);
+ instrTest.addInstrumentationArg(
+ APP_LAUNCH_TIMEOUT_LABEL, Integer.toString(mAppLaunchTimeoutMs));
+ instrTest.addInstrumentationArg(
+ WORKSPACE_LAUNCH_TIMEOUT_LABEL, Integer.toString(mWorkspaceLaunchTimeoutMs));
+ return instrTest;
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java b/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java
new file mode 100644
index 0000000..d022231
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.compatibility;
+
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+
+public final class FailureCollectingListener implements ITestInvocationListener {
+ private String mTestTrace = null;
+
+ @Override
+ public void testFailed(TestDescription test, String trace) {
+ setStackTrace(trace != null ? trace : "unknown failure");
+ }
+
+ @Override
+ public void testAssumptionFailure(TestDescription test, String trace) {
+ setStackTrace(trace != null ? trace : "unknown assumption failure");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void testRunFailed(String errorMessage) {
+ setStackTrace(errorMessage);
+ }
+
+ /**
+ * Fetches the stack trace if any.
+ *
+ * @return the stack trace.
+ */
+ public String getStackTrace() {
+ return mTestTrace;
+ }
+
+ /**
+ * Sets the stack trace.
+ *
+ * @param stackTrace {@link String} stack trace to set.
+ */
+ public void setStackTrace(String stackTrace) {
+ this.mTestTrace = stackTrace;
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
new file mode 100644
index 0000000..b742c2f
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 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.compatibility.targetprep;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.TestAppInstallSetup;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** A Tradefed preparer that downloads and installs an app on the target device. */
+public final class AppSetupPreparer implements ITargetPreparer {
+
+ public static final String OPTION_GCS_APK_DIR = "gcs-apk-dir";
+
+ @Option(name = "package-name", description = "Package name of the app being tested.")
+ private String mPackageName;
+
+ private final TestAppInstallSetup mAppInstallSetup;
+
+ public AppSetupPreparer() {
+ this(null, new TestAppInstallSetup());
+ }
+
+ @VisibleForTesting
+ public AppSetupPreparer(String packageName, TestAppInstallSetup appInstallSetup) {
+ this.mPackageName = packageName;
+ this.mAppInstallSetup = appInstallSetup;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws DeviceNotAvailableException, BuildError, TargetSetupError {
+ // TODO(b/147159584): Use a utility to get dynamic options.
+ String gcsApkDirOption = buildInfo.getBuildAttributes().get(OPTION_GCS_APK_DIR);
+ checkNotNull(gcsApkDirOption, "Option %s is not set.", OPTION_GCS_APK_DIR);
+
+ File apkDir = new File(gcsApkDirOption);
+ checkArgument(
+ apkDir.isDirectory(),
+ String.format("GCS Apk Directory %s is not a directory", apkDir));
+
+ File packageDir = new File(apkDir.getPath(), mPackageName);
+ checkArgument(
+ packageDir.isDirectory(),
+ String.format("Package directory %s is not a directory", packageDir));
+
+ mAppInstallSetup.setAltDir(packageDir);
+
+ List<String> apkFilePaths;
+ try {
+ apkFilePaths = listApkFilePaths(packageDir);
+ } catch (IOException e) {
+ throw new TargetSetupError(
+ String.format("Failed to access files in %s.", packageDir), e);
+ }
+
+ if (apkFilePaths.isEmpty()) {
+ throw new TargetSetupError(
+ String.format("Failed to find apk files in %s.", packageDir));
+ }
+
+ if (apkFilePaths.size() == 1) {
+ mAppInstallSetup.addTestFileName(apkFilePaths.get(0));
+ } else {
+ mAppInstallSetup.addSplitApkFileNames(String.join(",", apkFilePaths));
+ }
+
+ mAppInstallSetup.setUp(device, buildInfo);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+ mAppInstallSetup.tearDown(testInfo, e);
+ }
+
+ private List<String> listApkFilePaths(File downloadDir) throws IOException {
+ return Files.walk(Paths.get(downloadDir.getPath()))
+ .map(x -> x.getFileName().toString())
+ .filter(s -> s.endsWith(".apk"))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java
new file mode 100644
index 0000000..7e50f3f
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 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.compatibility.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.ITargetPreparer;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+
+/**
+ * A Tradefed preparer that receives module preparer options and stores the values in IBuildInfo.
+ */
+public final class AppSetupPreparerConfigurationReceiver implements ITargetPreparer {
+
+ @Option(
+ name = AppSetupPreparer.OPTION_GCS_APK_DIR,
+ description = "GCS path where the test apk files are located.")
+ private File mOptionGcsApkDir;
+
+ public AppSetupPreparerConfigurationReceiver() {
+ this(null);
+ }
+
+ @VisibleForTesting
+ public AppSetupPreparerConfigurationReceiver(File optionGcsApkDir) {
+ mOptionGcsApkDir = optionGcsApkDir;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) {
+ if (mOptionGcsApkDir == null) {
+ return;
+ }
+ buildInfo.addBuildAttribute(
+ AppSetupPreparer.OPTION_GCS_APK_DIR, mOptionGcsApkDir.getPath());
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java b/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
new file mode 100644
index 0000000..fb34dbf
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
@@ -0,0 +1,416 @@
+/*
+ * Copyright (C) 2012 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.compatibility.testtype;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.android.compatibility.FailureCollectingListener;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.LogcatReceiver;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.CompatibilityTestResult;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.IDeviceTest;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.ITestFilterReceiver;
+import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.StreamUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+
+import org.json.JSONException;
+import org.junit.Assert;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/** A test that verifies that a single app can be successfully launched. */
+public class AppLaunchTest
+ implements IDeviceTest, IRemoteTest, IConfigurationReceiver, ITestFilterReceiver {
+
+ @Option(name = "package-name", description = "Package name of testing app.")
+ private String mPackageName;
+
+ @Option(name = "test-label", description = "Unique test identifier label.")
+ private String mTestLabel = "AppCompatibility";
+
+ /** @deprecated */
+ @Deprecated
+ @Option(
+ name = "retry-count",
+ description = "Number of times to retry a failed test case. 0 means no retry.")
+ private int mRetryCount = 0;
+
+ @Option(name = "include-filter", description = "The include filter of the test type.")
+ protected Set<String> mIncludeFilters = new HashSet<>();
+
+ @Option(name = "exclude-filter", description = "The exclude filter of the test type.")
+ protected Set<String> mExcludeFilters = new HashSet<>();
+
+ @Option(
+ name = "app-launch-timeout-ms",
+ description = "Time to wait for app to launch in msecs.")
+ private int mAppLaunchTimeoutMs = 15000;
+
+ private static final String LAUNCH_TEST_RUNNER =
+ "com.android.compatibilitytest.AppCompatibilityRunner";
+ private static final String LAUNCH_TEST_PACKAGE = "com.android.compatibilitytest";
+ private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
+ private static final String APP_LAUNCH_TIMEOUT_LABEL = "app_launch_timeout_ms";
+ private static final int LOGCAT_SIZE_BYTES = 20 * 1024 * 1024;
+
+ private ITestDevice mDevice;
+ private LogcatReceiver mLogcat;
+ private IConfiguration mConfiguration;
+
+ public AppLaunchTest() {}
+
+ @VisibleForTesting
+ public AppLaunchTest(String packageName) {
+ mPackageName = packageName;
+ }
+
+ @VisibleForTesting
+ public AppLaunchTest(String packageName, int retryCount) {
+ mPackageName = packageName;
+ mRetryCount = retryCount;
+ }
+
+ /**
+ * Creates and sets up an instrumentation test with information about the test runner as well as
+ * the package being tested (provided as a parameter).
+ */
+ protected InstrumentationTest createInstrumentationTest(String packageBeingTested) {
+ InstrumentationTest instrTest = new InstrumentationTest();
+
+ instrTest.setPackageName(LAUNCH_TEST_PACKAGE);
+ instrTest.setConfiguration(mConfiguration);
+ instrTest.addInstrumentationArg(PACKAGE_TO_LAUNCH, packageBeingTested);
+ instrTest.setRunnerName(LAUNCH_TEST_RUNNER);
+ instrTest.setDevice(mDevice);
+ instrTest.addInstrumentationArg(
+ APP_LAUNCH_TIMEOUT_LABEL, Integer.toString(mAppLaunchTimeoutMs));
+
+ return instrTest;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void run(final TestInformation testInfo, final ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ CLog.d("Start of run method.");
+ CLog.d("Include filters: %s", mIncludeFilters);
+ CLog.d("Exclude filters: %s", mExcludeFilters);
+
+ Assert.assertNotNull("Package name cannot be null", mPackageName);
+
+ TestDescription testDescription = createTestDescription();
+
+ if (!inFilter(testDescription.toString())) {
+ CLog.d("Test case %s doesn't match any filter", testDescription);
+ return;
+ }
+ CLog.d("Complete filtering test case: %s", testDescription);
+
+ long start = System.currentTimeMillis();
+ listener.testRunStarted(mTestLabel, 1);
+ mLogcat = new LogcatReceiver(getDevice(), LOGCAT_SIZE_BYTES, 0);
+ mLogcat.start();
+
+ try {
+ testPackage(testInfo, testDescription, listener);
+ } catch (InterruptedException e) {
+ CLog.e(e);
+ throw new RuntimeException(e);
+ } finally {
+ mLogcat.stop();
+ listener.testRunEnded(
+ System.currentTimeMillis() - start, new HashMap<String, Metric>());
+ }
+ }
+
+ /**
+ * Attempts to test a package and reports the results.
+ *
+ * @param listener The {@link ITestInvocationListener}.
+ * @throws DeviceNotAvailableException
+ */
+ private void testPackage(
+ final TestInformation testInfo,
+ TestDescription testDescription,
+ ITestInvocationListener listener)
+ throws DeviceNotAvailableException, InterruptedException {
+ CLog.d("Started testing package: %s.", mPackageName);
+
+ listener.testStarted(testDescription, System.currentTimeMillis());
+
+ CompatibilityTestResult result = createCompatibilityTestResult();
+ result.packageName = mPackageName;
+
+ try {
+ for (int i = 0; i <= mRetryCount; i++) {
+ result.status = null;
+ result.message = null;
+ // Clear test result between retries.
+ launchPackage(testInfo, result);
+ if (result.status == CompatibilityTestResult.STATUS_SUCCESS) {
+ return;
+ }
+ }
+ } finally {
+ reportResult(listener, testDescription, result);
+ stopPackage();
+ try {
+ postLogcat(result, listener);
+ } catch (JSONException e) {
+ CLog.w("Posting failed: %s.", e.getMessage());
+ }
+ listener.testEnded(
+ testDescription,
+ System.currentTimeMillis(),
+ Collections.<String, String>emptyMap());
+
+ CLog.d("Completed testing package: %s.", mPackageName);
+ }
+ }
+
+ /**
+ * Method which attempts to launch a package.
+ *
+ * <p>Will set the result status to success if the package could be launched. Otherwise the
+ * result status will be set to failure.
+ *
+ * @param result the {@link CompatibilityTestResult} containing the package info.
+ * @throws DeviceNotAvailableException
+ */
+ private void launchPackage(final TestInformation testInfo, CompatibilityTestResult result)
+ throws DeviceNotAvailableException {
+ CLog.d("Launching package: %s.", result.packageName);
+
+ CommandResult resetResult = resetPackage();
+ if (resetResult.getStatus() != CommandStatus.SUCCESS) {
+ result.status = CompatibilityTestResult.STATUS_ERROR;
+ result.message = resetResult.getStatus() + resetResult.getStderr();
+ return;
+ }
+
+ InstrumentationTest instrTest = createInstrumentationTest(result.packageName);
+
+ FailureCollectingListener failureListener = createFailureListener();
+ instrTest.run(testInfo, failureListener);
+ CLog.d("Stack Trace: %s", failureListener.getStackTrace());
+
+ if (failureListener.getStackTrace() != null) {
+ CLog.w("Failed to launch package: %s.", result.packageName);
+ result.status = CompatibilityTestResult.STATUS_FAILURE;
+ result.message = failureListener.getStackTrace();
+ } else {
+ result.status = CompatibilityTestResult.STATUS_SUCCESS;
+ }
+
+ CLog.d("Completed launching package: %s", result.packageName);
+ }
+
+ /** Helper method which reports a test failed if the status is either a failure or an error. */
+ private void reportResult(
+ ITestInvocationListener listener, TestDescription id, CompatibilityTestResult result) {
+ String message = result.message != null ? result.message : "unknown";
+ String tag = errorStatusToTag(result.status);
+ if (tag != null) {
+ listener.testFailed(id, result.status + ":" + message);
+ }
+ }
+
+ private String errorStatusToTag(String status) {
+ if (status.equals(CompatibilityTestResult.STATUS_ERROR)) {
+ return "ERROR";
+ }
+ if (status.equals(CompatibilityTestResult.STATUS_FAILURE)) {
+ return "FAILURE";
+ }
+ return null;
+ }
+
+ /** Helper method which posts the logcat. */
+ private void postLogcat(CompatibilityTestResult result, ITestInvocationListener listener)
+ throws JSONException {
+ InputStreamSource stream = null;
+ String header =
+ String.format(
+ "%s%s%s\n",
+ CompatibilityTestResult.SEPARATOR,
+ result.toJsonString(),
+ CompatibilityTestResult.SEPARATOR);
+
+ try (InputStreamSource logcatData = mLogcat.getLogcatData()) {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ baos.write(header.getBytes());
+ StreamUtil.copyStreams(logcatData.createInputStream(), baos);
+ stream = new ByteArrayInputStreamSource(baos.toByteArray());
+ } catch (IOException e) {
+ CLog.e("error inserting compatibility test result into logcat");
+ CLog.e(e);
+ // fallback to logcat data
+ stream = logcatData;
+ }
+ listener.testLog("logcat_" + result.packageName, LogDataType.LOGCAT, stream);
+ } finally {
+ StreamUtil.cancel(stream);
+ }
+ }
+
+ /**
+ * Return true if a test matches one or more of the include filters AND does not match any of
+ * the exclude filters. If no include filters are given all tests should return true as long as
+ * they do not match any of the exclude filters.
+ */
+ protected boolean inFilter(String testName) {
+ if (mExcludeFilters.contains(testName)) {
+ return false;
+ }
+ if (mIncludeFilters.size() == 0 || mIncludeFilters.contains(testName)) {
+ return true;
+ }
+ return false;
+ }
+
+ protected CommandResult resetPackage() throws DeviceNotAvailableException {
+ return mDevice.executeShellV2Command(String.format("pm clear %s", mPackageName));
+ }
+
+ private void stopPackage() throws DeviceNotAvailableException {
+ mDevice.executeShellCommand(String.format("am force-stop %s", mPackageName));
+ }
+
+ @Override
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void setDevice(ITestDevice device) {
+ mDevice = device;
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public ITestDevice getDevice() {
+ return mDevice;
+ }
+
+ public int getmRetryCount() {
+ return mRetryCount;
+ }
+
+ /**
+ * Get a test description for use in logging. For compatibility with logs, this should be
+ * TestDescription(test class name, test type).
+ */
+ private TestDescription createTestDescription() {
+ return new TestDescription(getClass().getSimpleName(), mPackageName);
+ }
+
+ /** Get a FailureCollectingListener for failure listening. */
+ private FailureCollectingListener createFailureListener() {
+ return new FailureCollectingListener();
+ }
+
+ /**
+ * Get a CompatibilityTestResult for encapsulating compatibility run results for a single app
+ * package tested.
+ */
+ private CompatibilityTestResult createCompatibilityTestResult() {
+ return new CompatibilityTestResult();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addIncludeFilter(String filter) {
+ checkArgument(!Strings.isNullOrEmpty(filter), "Include filter cannot be null or empty.");
+ mIncludeFilters.add(filter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addAllIncludeFilters(Set<String> filters) {
+ checkNotNull(filters, "Include filters cannot be null.");
+ mIncludeFilters.addAll(filters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clearIncludeFilters() {
+ mIncludeFilters.clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<String> getIncludeFilters() {
+ return Collections.unmodifiableSet(mIncludeFilters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addExcludeFilter(String filter) {
+ checkArgument(!Strings.isNullOrEmpty(filter), "Exclude filter cannot be null or empty.");
+ mExcludeFilters.add(filter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addAllExcludeFilters(Set<String> filters) {
+ checkNotNull(filters, "Exclude filters cannot be null.");
+ mExcludeFilters.addAll(filters);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void clearExcludeFilters() {
+ mExcludeFilters.clear();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<String> getExcludeFilters() {
+ return Collections.unmodifiableSet(mExcludeFilters);
+ }
+}
diff --git a/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java b/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java
new file mode 100644
index 0000000..e1f92b9
--- /dev/null
+++ b/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 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.tradefed.result;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.regex.Pattern;
+
+/** encapsulates compatibility run results for a single app package tested */
+public class CompatibilityTestResult {
+
+ public static final String KEY_PACKAGE = "app_package";
+ // Keep app_version for backwards compatibility
+ public static final String KEY_VERSION_CODE = "app_version_code";
+ public static final String KEY_VERSION_STRING = "app_version_string";
+ public static final String KEY_NAME = "app_name";
+ public static final String KEY_RANK = "app_rank";
+ public static final String KEY_STATUS = "status";
+ public static final String KEY_MESSAGE = "message";
+
+ public static final String SEPARATOR = "=@ppcomp@t=";
+ public static final Pattern REGEX =
+ Pattern.compile(String.format("^%s(.*?)%s", SEPARATOR, SEPARATOR));
+
+ public static final String STATUS_SUCCESS = "success";
+ public static final String STATUS_ERROR = "error"; // installation errors etc
+ public static final String STATUS_FAILURE = "failure"; // app launch failures
+
+ public String packageName = null;
+ public String versionString = null;
+ public String versionCode = null;
+ public String name = null;
+ public Integer rank = null;
+ public String status = null;
+ public String message = null;
+
+ /**
+ * Return the Serialized fields into JSON string
+ *
+ * @throws JSONException
+ */
+ public String toJsonString() throws JSONException {
+ JSONObject o = new JSONObject();
+ o.put(KEY_PACKAGE, packageName);
+ o.put(KEY_VERSION_STRING, versionString);
+ o.put(KEY_VERSION_CODE, versionString);
+ o.put(KEY_NAME, name);
+ o.put(KEY_RANK, rank);
+ o.put(KEY_STATUS, status);
+ o.put(KEY_MESSAGE, message);
+ return o.toString();
+ }
+
+ /**
+ * Reconstructs an instance from a JSON string
+ *
+ * @param json
+ * @return the {@link CompatibilityTestResult} instance from the JSON serialized string.
+ * @throws JSONException
+ */
+ public static CompatibilityTestResult fromJsonString(String json) throws JSONException {
+ JSONObject o = new JSONObject(json);
+ CompatibilityTestResult result = new CompatibilityTestResult();
+ result.packageName = o.getString(KEY_PACKAGE);
+ if (o.has(KEY_VERSION_STRING)) {
+ result.versionString = o.getString(KEY_VERSION_STRING);
+ }
+ if (o.has(KEY_VERSION_CODE)) {
+ result.versionString = o.getString(KEY_VERSION_CODE);
+ }
+ if (o.has(KEY_NAME)) {
+ result.name = o.getString(KEY_NAME);
+ }
+ if (o.has(KEY_RANK)) {
+ result.rank = o.getInt(KEY_RANK);
+ }
+ result.status = o.getString(KEY_STATUS);
+ if (o.has(KEY_MESSAGE)) {
+ result.message = o.getString(KEY_MESSAGE);
+ }
+ return result;
+ }
+}
diff --git a/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java b/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java
new file mode 100644
index 0000000..3e2915e
--- /dev/null
+++ b/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2016 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.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to download files from x20 to the corp TF environment. In particular, it is used in
+ * conjunction with the <a href="http://go/app-compatibility-readme">App Compatibility pipeline</a>.
+ */
+public class PublicApkUtil {
+ private static final Pattern DATE_FORMAT = Pattern.compile("\\d{8}");
+ private static final long DOWNLOAD_TIMEOUT_MS = 60 * 1000;
+ private static final int DOWNLOAD_RETRIES = 3;
+ private static final String LATEST_FILE = "latest.txt";
+
+ /**
+ * Helper method which constructs the dated CNS directory from the base directory and either the
+ * supplied date option or the most recent directory.
+ *
+ * @param baseDir The base directory with the "latest" file and dated subdirectories.
+ * @param subDir A specific target directory, or null if using the latest file.
+ * @return The {@link File} of the x20 dir where the APKs are stored.
+ * @throws IOException
+ */
+ public static File constructApkDir(String baseDir, String subDir) throws IOException {
+ if (subDir != null) {
+ return new File(baseDir, subDir);
+ }
+ File latestFile = null;
+ try {
+ latestFile =
+ downloadFile(
+ new File(baseDir, LATEST_FILE), DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
+ String date = FileUtil.readStringFromFile(latestFile).trim();
+ if (DATE_FORMAT.matcher(date).matches()) {
+ return new File(baseDir, date);
+ }
+ return null;
+ } finally {
+ FileUtil.deleteFile(latestFile);
+ }
+ }
+
+ /**
+ * A configurable helper method for downloading a remote file.
+ *
+ * @param remoteFile The remote {@link File} location.
+ * @param downloadTimeout The download timeout in milliseconds.
+ * @param downloadRetries The download retry count, in case of failure.
+ * @return The local {@link File} that was downloaded.
+ * @throws IOException
+ */
+ public static File downloadFile(File remoteFile, long downloadTimeout, int downloadRetries)
+ throws IOException {
+ CLog.i("Attempting to download %s", remoteFile);
+ File tmpFile = FileUtil.createTempFile(remoteFile.getName(), null);
+ FileUtil.copyFile(remoteFile, tmpFile);
+ return tmpFile;
+ }
+
+ /**
+ * Helper method which downloads the ranking file and returns the list of apks.
+ *
+ * @param flavor The APK variant to pick.
+ * @param dir The {@link File} of the dated x20 dir.
+ * @param fallbackToApkScan fallback to scan for apk files in folder if no ranking csv file
+ * @return The list of {@link ApkInfo} objects.
+ * @throws IOException
+ */
+ public static List<ApkInfo> getApkList(String flavor, File dir, boolean fallbackToApkScan)
+ throws IOException {
+ File apkFile = new File(dir, String.format("%s_ranking.csv", flavor));
+ if (!apkFile.exists() && fallbackToApkScan) {
+ return getApkListFromDirectory(dir);
+ } else {
+ return getApkListFromRankingInfo(apkFile);
+ }
+ }
+
+ /**
+ * Constructs a list of degenerate {@link ApkInfo} based on apks files found in provided base
+ * directory. The {@link ApkInfo} instance only contains relative filename, without the ranking
+ * information.
+ *
+ * @param baseDir
+ * @return
+ * @throws IOException
+ */
+ private static List<ApkInfo> getApkListFromDirectory(File baseDir) throws IOException {
+ List<ApkInfo> apkList = new ArrayList<>();
+ File[] apks =
+ baseDir.listFiles(
+ new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ // filters out all apk files
+ return name.endsWith(".apk");
+ }
+ });
+ for (File apk : apks) {
+ AaptParser parser = AaptParser.parse(apk);
+ if (parser == null) {
+ throw new IOException(
+ String.format("Failed to parse apk file %s", apk.getCanonicalPath()));
+ }
+ ApkInfo apkInfo =
+ new ApkInfo(
+ -1,
+ parser.getPackageName(),
+ parser.getVersionName(),
+ parser.getVersionCode(),
+ apk.getName());
+ apkList.add(apkInfo);
+ }
+ return apkList;
+ }
+
+ /**
+ * Parses ranking information csv file into the data structure representing a list of apks with
+ * ranking and package information
+ *
+ * @param rankingInfo the path to ranking csv file
+ * @return
+ * @throws IOException
+ */
+ private static List<ApkInfo> getApkListFromRankingInfo(File rankingInfo) throws IOException {
+ List<ApkInfo> apkList = new ArrayList<>();
+ File copiedFile = null;
+ BufferedReader br = null;
+ try {
+ copiedFile = downloadFile(rankingInfo, DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
+ br = new BufferedReader(new FileReader(copiedFile));
+ String line;
+ boolean firstLine = true;
+ while ((line = br.readLine()) != null) {
+ if (firstLine) {
+ firstLine = false;
+ } else {
+ try {
+ apkList.add(ApkInfo.fromCsvLine(line));
+ } catch (IllegalArgumentException e) {
+ CLog.e("Ranking file not formatted properly, skipping.");
+ CLog.e(e);
+ }
+ }
+ }
+ } finally {
+ StreamUtil.close(br);
+ FileUtil.deleteFile(copiedFile);
+ }
+ return apkList;
+ }
+
+ /**
+ * Helper class which holds information about the ranking list such as rank, package name, etc.
+ */
+ public static class ApkInfo {
+ public final int rank;
+ public final String packageName;
+ public final String versionString;
+ public final String versionCode;
+ public final String fileName;
+
+ public ApkInfo(
+ int rank,
+ String packageName,
+ String versionString,
+ String versionCode,
+ String fileName) {
+ this.rank = rank;
+ this.packageName = packageName;
+ this.versionString = versionString;
+ this.versionCode = versionCode;
+ this.fileName = fileName;
+ }
+
+ public static ApkInfo fromCsvLine(String line) {
+ String[] cols = QuotationAwareTokenizer.tokenizeLine(line, ",");
+ int rank = -1;
+ try {
+ rank = Integer.parseInt(cols[0]);
+ } catch (NumberFormatException e) {
+ // rethrow as IAE with content of problematic line
+ throw new IllegalArgumentException(
+ String.format("Invalid line (rank field not a number): %s", line), e);
+ }
+ if (cols.length != 5) {
+ throw new IllegalArgumentException(
+ String.format("Invalid line (expected 5 data columns): %s", line));
+ }
+ return new ApkInfo(rank, cols[1], cols[2], cols[3], cols[4]);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Package: %s v%s (%s), rank: %d, file: %s",
+ packageName, versionCode, versionString, rank, fileName);
+ }
+ }
+}
diff --git a/harness/src/main/resources/config/csuite-base.xml b/harness/src/main/resources/config/csuite-base.xml
new file mode 100644
index 0000000..6431154
--- /dev/null
+++ b/harness/src/main/resources/config/csuite-base.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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="CSuite Main Test Plan">
+ <include name="everything" />
+ <device_recovery class="com.android.tradefed.device.WaitDeviceRecovery" />
+ <build_provider class="com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider" />
+
+ <option name="enable-root" value="true" />
+ <option name="max-log-size" value="200" />
+
+ <option name="compatibility:primary-abi-only" value="true" />
+
+ <template-include name="reporters" default="basic-reporters" />
+ <result_reporter class="com.android.compatibility.common.tradefed.result.ConsoleReporter" />
+ <result_reporter class="com.android.compatibility.common.tradefed.result.suite.CompatibilityProtoResultReporter" />
+ <result_reporter class="com.android.tradefed.result.suite.SuiteResultReporter" />
+
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparerConfigurationReceiver" />
+</configuration>
diff --git a/harness/src/main/resources/config/launch.xml b/harness/src/main/resources/config/launch.xml
new file mode 100644
index 0000000..a1a158d
--- /dev/null
+++ b/harness/src/main/resources/config/launch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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 Compatibility Launch Test Plan">
+ <include name="csuite-base" />
+
+ <option name="plan" value="launch" />
+
+ <option name="compatibility:module-metadata-include-filter" key="plan" value="app-launch" />
+</configuration>
diff --git a/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java b/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java
new file mode 100644
index 0000000..18f3368
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2019 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.compatibility;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.util.PublicApkUtil.ApkInfo;
+
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+@RunWith(JUnit4.class)
+public final class AppCompatibilityTestTest {
+
+ private ConcreteAppCompatibilityTest mSut;
+
+ private class ConcreteAppCompatibilityTest extends AppCompatibilityTest {
+
+ ConcreteAppCompatibilityTest() {
+ super(null, null, null);
+ }
+
+ @Override
+ protected InstrumentationTest createInstrumentationTest(String packageBeingTested) {
+ return null;
+ }
+ }
+
+ @Before
+ public void setUp() {
+ mSut = new ConcreteAppCompatibilityTest();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addIncludeFilter_nullIncludeFilter_throwsException() {
+ mSut.addIncludeFilter(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addIncludeFilter_emptyIncludeFilter_throwsException() {
+ mSut.addIncludeFilter("");
+ }
+
+ @Test
+ public void addIncludeFilter_validIncludeFilter() {
+ mSut.addIncludeFilter("test_filter");
+
+ assertTrue(mSut.mIncludeFilters.contains("test_filter"));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void addAllIncludeFilters_nullIncludeFilter_throwsException() {
+ mSut.addAllIncludeFilters(null);
+ }
+
+ @Test
+ public void addAllIncludeFilters_validIncludeFilters() {
+ Set<String> test_filters = new TreeSet<>();
+ test_filters.add("filter_one");
+ test_filters.add("filter_two");
+
+ mSut.addAllIncludeFilters(test_filters);
+
+ assertTrue(mSut.mIncludeFilters.contains("filter_one"));
+ assertTrue(mSut.mIncludeFilters.contains("filter_two"));
+ }
+
+ @Test
+ public void clearIncludeFilters() {
+ mSut.addIncludeFilter("filter_test");
+
+ mSut.clearIncludeFilters();
+
+ assertTrue(mSut.mIncludeFilters.isEmpty());
+ }
+
+ @Test
+ public void getIncludeFilters() {
+ mSut.addIncludeFilter("filter_test");
+
+ assertEquals(mSut.mIncludeFilters, mSut.getIncludeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addExcludeFilter_nullExcludeFilter_throwsException() {
+ mSut.addExcludeFilter(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addExcludeFilter_emptyExcludeFilter_throwsException() {
+ mSut.addExcludeFilter("");
+ }
+
+ @Test
+ public void addExcludeFilter_validExcludeFilter() {
+ mSut.addExcludeFilter("test_filter");
+
+ assertTrue(mSut.mExcludeFilters.contains("test_filter"));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void addAllExcludeFilters_nullExcludeFilters_throwsException() {
+ mSut.addAllExcludeFilters(null);
+ }
+
+ @Test
+ public void addAllExcludeFilters_validExcludeFilters() {
+ Set<String> test_filters = new TreeSet<>();
+ test_filters.add("filter_one");
+ test_filters.add("filter_two");
+
+ mSut.addAllExcludeFilters(test_filters);
+
+ assertTrue(mSut.mExcludeFilters.contains("filter_one"));
+ assertTrue(mSut.mExcludeFilters.contains("filter_two"));
+ }
+
+ @Test
+ public void clearExcludeFilters() {
+ mSut.addExcludeFilter("filter_test");
+
+ mSut.clearExcludeFilters();
+
+ assertTrue(mSut.mExcludeFilters.isEmpty());
+ }
+
+ @Test
+ public void getExcludeFilters() {
+ mSut.addExcludeFilter("filter_test");
+
+ assertEquals(mSut.mExcludeFilters, mSut.getExcludeFilters());
+ }
+
+ @Test
+ public void filterApk_withNoFilter() {
+ List<ApkInfo> testList = createApkList();
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(filteredList, testList);
+ }
+
+ @Test
+ public void filterApk_withRelatedIncludeFilters() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_one");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(convertList(filteredList), Arrays.asList("filter_one"));
+ }
+
+ @Test
+ public void filterApk_withUnrelatedIncludeFilters() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_three");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertTrue(filteredList.isEmpty());
+ }
+
+ @Test
+ public void filterApk_withRelatedExcludeFilters() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addExcludeFilter("filter_one");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(convertList(filteredList), Arrays.asList("filter_two"));
+ }
+
+ @Test
+ public void filterApk_withUnrelatedExcludeFilters() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addExcludeFilter("filter_three");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(filteredList, testList);
+ }
+
+ @Test
+ public void filterApk_withSameIncludeAndExcludeFilters() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_one");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertTrue(filteredList.isEmpty());
+ }
+
+ @Test
+ public void filterApk_withDifferentIncludeAndExcludeFilter() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_one");
+ mSut.addIncludeFilter("filter_two");
+ mSut.addExcludeFilter("filter_two");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(convertList(filteredList), Arrays.asList("filter_one"));
+ }
+
+ @Test
+ public void filterApk_withUnrelatedIncludeFilterAndRelatedExcludeFilter() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_three");
+ mSut.addExcludeFilter("filter_two");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertTrue(filteredList.isEmpty());
+ }
+
+ @Test
+ public void filterApk_withRelatedIncludeFilterAndUnrelatedExcludeFilter() {
+ List<ApkInfo> testList = createApkList();
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_three");
+
+ List<ApkInfo> filteredList = mSut.filterApk(testList);
+
+ assertEquals(convertList(filteredList), Arrays.asList("filter_one"));
+ }
+
+ private List<ApkInfo> createApkList() {
+ List<ApkInfo> testList = new ArrayList<>();
+ ApkInfo apk_info_one = new ApkInfo(0, "filter_one", "", "", "");
+ ApkInfo apk_info_two = new ApkInfo(0, "filter_two", "", "", "");
+ testList.add(apk_info_one);
+ testList.add(apk_info_two);
+ return testList;
+ }
+
+ private List<String> convertList(List<ApkInfo> apkList) {
+ List<String> convertedList = new ArrayList<>();
+ for (ApkInfo apkInfo : apkList) {
+ convertedList.add(apkInfo.packageName);
+ }
+ return convertedList;
+ }
+
+ @Test
+ public void filterTest_withEmptyFilter() {
+ assertTrue(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withRelatedIncludeFilter() {
+ mSut.addIncludeFilter("filter_one");
+
+ assertTrue(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withUnrelatedIncludeFilter() {
+ mSut.addIncludeFilter("filter_two");
+
+ assertFalse(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withRelatedExcludeFilter() {
+ mSut.addExcludeFilter("filter_one");
+
+ assertFalse(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withUnrelatedExcludeFilter() {
+ mSut.addExcludeFilter("filter_two");
+
+ assertTrue(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withSameIncludeAndExcludeFilters() {
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_one");
+
+ assertFalse(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withUnrelatedIncludeFilterAndRelatedExcludeFilter() {
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_two");
+
+ assertFalse(mSut.filterTest("filter_two"));
+ }
+
+ @Test
+ public void filterTest_withRelatedIncludeFilterAndUnrelatedExcludeFilter() {
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_two");
+
+ assertTrue(mSut.filterTest("filter_one"));
+ }
+
+ @Test
+ public void filterTest_withUnRelatedIncludeFilterAndUnrelatedExcludeFilter() {
+ mSut.addIncludeFilter("filter_one");
+ mSut.addExcludeFilter("filter_two");
+
+ assertFalse(mSut.filterTest("filter_three"));
+ }
+}
diff --git a/harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java b/harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java
new file mode 100644
index 0000000..b87402e
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.compatibility;
+
+import com.android.compatibility.targetprep.AppSetupPreparerConfigurationReceiverTest;
+import com.android.compatibility.targetprep.AppSetupPreparerTest;
+import com.android.compatibility.testtype.AppLaunchTestTest;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+import org.junit.runners.Suite.SuiteClasses;
+
+@RunWith(Suite.class)
+@SuiteClasses({
+ AppCompatibilityTestTest.class,
+ AppLaunchTestTest.class,
+ AppSetupPreparerTest.class,
+ AppSetupPreparerConfigurationReceiverTest.class,
+})
+public final class CSuiteUnitTests {
+ // Intentionally empty.
+}
diff --git a/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java
new file mode 100644
index 0000000..e708a67
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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.compatibility.targetprep;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.IBuildInfo;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public final class AppSetupPreparerConfigurationReceiverTest {
+
+ @Test
+ public void setUp_noneNullGcsApkDirOption_putsInBuildInfo() {
+ File optionGcsApkDir = new File("dir");
+ AppSetupPreparerConfigurationReceiver preparer =
+ new AppSetupPreparerConfigurationReceiver(optionGcsApkDir);
+ IBuildInfo buildInfo = new BuildInfo();
+
+ preparer.setUp(null, buildInfo);
+
+ assertThat(buildInfo.getBuildAttributes())
+ .containsEntry(AppSetupPreparer.OPTION_GCS_APK_DIR, optionGcsApkDir.getPath());
+ }
+
+ @Test
+ public void setUp_nullGcsApkDirOption_doesNotPutInBuildInfo() {
+ File optionGcsApkDir = null;
+ AppSetupPreparerConfigurationReceiver preparer =
+ new AppSetupPreparerConfigurationReceiver(optionGcsApkDir);
+ IBuildInfo buildInfo = new BuildInfo();
+
+ preparer.setUp(null, buildInfo);
+
+ assertThat(buildInfo.getBuildAttributes())
+ .doesNotContainKey(AppSetupPreparer.OPTION_GCS_APK_DIR);
+ }
+}
diff --git a/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
new file mode 100644
index 0000000..7614f05
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 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.compatibility.targetprep;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.TestAppInstallSetup;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertThrows;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+@RunWith(JUnit4.class)
+public class AppSetupPreparerTest {
+
+ private static final String OPTION_GCS_APK_DIR = "gcs-apk-dir";
+ public static final ITestDevice NULL_DEVICE = null;
+
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private final IBuildInfo mBuildInfo = new BuildInfo();
+ private final TestAppInstallSetup mMockAppInstallSetup = mock(TestAppInstallSetup.class);
+ private final AppSetupPreparer mPreparer =
+ new AppSetupPreparer("package_name", mMockAppInstallSetup);
+
+ @Test
+ public void setUp_gcsApkDirIsNull_throwsException()
+ throws DeviceNotAvailableException, TargetSetupError {
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, null);
+
+ assertThrows(NullPointerException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ }
+
+ @Test
+ public void setUp_gcsApkDirIsNotDir_throwsException()
+ throws IOException, DeviceNotAvailableException, TargetSetupError {
+ File tempFile = tempFolder.newFile("temp_file_name");
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, tempFile.getPath());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ }
+
+ @Test
+ public void setUp_packageDirDoesNotExist_throwsError()
+ throws IOException, DeviceNotAvailableException, TargetSetupError {
+ File gcsApkDir = tempFolder.newFolder("gcs_apk_dir");
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, gcsApkDir.getPath());
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ }
+
+ @Test
+ public void setUp_apkDoesNotExist() throws Exception {
+ File gcsApkDir = tempFolder.newFolder("gcs_apk_dir");
+ createPackageFile(gcsApkDir, "package_name", "non_apk_file");
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, gcsApkDir.getPath());
+
+ assertThrows(TargetSetupError.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ }
+
+ @Test
+ public void setUp_installSplitApk() throws Exception {
+ File gcsApkDir = tempFolder.newFolder("gcs_apk_dir");
+ File packageDir = new File(gcsApkDir.getPath(), "package_name");
+ createPackageFile(gcsApkDir, "package_name", "apk_name_1.apk");
+ createPackageFile(gcsApkDir, "package_name", "apk_name_2.apk");
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, gcsApkDir.getPath());
+
+ mPreparer.setUp(NULL_DEVICE, mBuildInfo);
+
+ verify(mMockAppInstallSetup).setAltDir(packageDir);
+ verify(mMockAppInstallSetup).addSplitApkFileNames("apk_name_2.apk,apk_name_1.apk");
+ verify(mMockAppInstallSetup).setUp(any(), any());
+ }
+
+ @Test
+ public void setUp_installNonSplitApk() throws Exception {
+ File gcsApkDir = tempFolder.newFolder("gcs_apk_dir");
+ File packageDir = new File(gcsApkDir.getPath(), "package_name");
+ createPackageFile(gcsApkDir, "package_name", "apk_name_1.apk");
+ mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, gcsApkDir.getPath());
+
+ mPreparer.setUp(NULL_DEVICE, mBuildInfo);
+
+ verify(mMockAppInstallSetup).setAltDir(packageDir);
+ verify(mMockAppInstallSetup).addTestFileName("apk_name_1.apk");
+ verify(mMockAppInstallSetup).setUp(any(), any());
+ }
+
+ @Test
+ public void tearDown() throws Exception {
+ TestInformation testInfo = TestInformation.newBuilder().build();
+
+ mPreparer.tearDown(testInfo, null);
+
+ verify(mMockAppInstallSetup, times(1)).tearDown(testInfo, null);
+ }
+
+ private File createPackageFile(File parentDir, String packageName, String apkName)
+ throws IOException {
+ File packageDir =
+ Files.createDirectories(Paths.get(parentDir.getAbsolutePath(), packageName))
+ .toFile();
+
+ return Files.createFile(Paths.get(packageDir.getAbsolutePath(), apkName)).toFile();
+ }
+}
diff --git a/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java b/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
new file mode 100644
index 0000000..4f9aa35
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2019 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.compatibility.testtype;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.testtype.InstrumentationTest;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public final class AppLaunchTestTest {
+
+ private final ITestInvocationListener mMockListener = mock(ITestInvocationListener.class);
+ private static final String TEST_PACKAGE_NAME = "package_name";
+ private static final TestInformation NULL_TEST_INFORMATION = null;
+
+ @Test
+ public void run_testFailed() throws DeviceNotAvailableException {
+ InstrumentationTest instrumentationTest = createFailingInstrumentationTest();
+ AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyFailedAndEndedCall(mMockListener);
+ }
+
+ @Test
+ public void run_testPassed() throws DeviceNotAvailableException {
+ InstrumentationTest instrumentationTest = createPassingInstrumentationTest();
+ AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyPassedAndEndedCall(mMockListener);
+ }
+
+ @Test
+ public void run_packageResetSuccess() throws DeviceNotAvailableException {
+ ITestDevice mMockDevice = mock(ITestDevice.class);
+ when(mMockDevice.executeShellV2Command(String.format("pm clear %s", TEST_PACKAGE_NAME)))
+ .thenReturn(new CommandResult(CommandStatus.SUCCESS));
+ AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyPassedAndEndedCall(mMockListener);
+ }
+
+ @Test
+ public void run_packageResetError() throws DeviceNotAvailableException {
+ ITestDevice mMockDevice = mock(ITestDevice.class);
+ when(mMockDevice.executeShellV2Command(String.format("pm clear %s", TEST_PACKAGE_NAME)))
+ .thenReturn(new CommandResult(CommandStatus.FAILED));
+ AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyFailedAndEndedCall(mMockListener);
+ }
+
+ @Test
+ public void run_testRetry_passedAfterTwoFailings() throws Exception {
+ InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(2);
+ AppLaunchTest appLaunchTest = createLaunchTestWithRetry(instrumentationTest, 2);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyPassedAndEndedCall(mMockListener);
+ }
+
+ @Test
+ public void run_testRetry_failedAfterThreeFailings() throws Exception {
+ InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(3);
+ AppLaunchTest appLaunchTest = createLaunchTestWithRetry(instrumentationTest, 2);
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ verifyFailedAndEndedCall(mMockListener);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addIncludeFilter_nullIncludeFilter_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addIncludeFilter_emptyIncludeFilter_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("");
+ }
+
+ @Test
+ public void addIncludeFilter_validIncludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("test_filter");
+
+ assertTrue(sut.mIncludeFilters.contains("test_filter"));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void addAllIncludeFilters_nullIncludeFilter_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addAllIncludeFilters(null);
+ }
+
+ @Test
+ public void addAllIncludeFilters_validIncludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+ Set<String> test_filters = new HashSet<>();
+ test_filters.add("filter_one");
+ test_filters.add("filter_two");
+
+ sut.addAllIncludeFilters(test_filters);
+
+ assertTrue(sut.mIncludeFilters.contains("filter_one"));
+ assertTrue(sut.mIncludeFilters.contains("filter_two"));
+ }
+
+ @Test
+ public void clearIncludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+ sut.addIncludeFilter("filter_test");
+
+ sut.clearIncludeFilters();
+
+ assertTrue(sut.mIncludeFilters.isEmpty());
+ }
+
+ @Test
+ public void getIncludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+ sut.addIncludeFilter("filter_test");
+
+ assertEquals(sut.mIncludeFilters, sut.getIncludeFilters());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addExcludeFilter_nullExcludeFilter_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addExcludeFilter_emptyExcludeFilter_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter("");
+ }
+
+ @Test
+ public void addExcludeFilter_validExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter("test_filter");
+
+ assertTrue(sut.mExcludeFilters.contains("test_filter"));
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void addAllExcludeFilters_nullExcludeFilters_throwsException() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addAllExcludeFilters(null);
+ }
+
+ @Test
+ public void addAllExcludeFilters_validExcludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+ Set<String> test_filters = new HashSet<>();
+ test_filters.add("filter_one");
+ test_filters.add("filter_two");
+
+ sut.addAllExcludeFilters(test_filters);
+
+ assertTrue(sut.mExcludeFilters.contains("filter_one"));
+ assertTrue(sut.mExcludeFilters.contains("filter_two"));
+ }
+
+ @Test
+ public void clearExcludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+ sut.addExcludeFilter("filter_test");
+
+ sut.clearExcludeFilters();
+
+ assertTrue(sut.mExcludeFilters.isEmpty());
+ }
+
+ @Test
+ public void getExcludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter("filter_test");
+
+ assertEquals(sut.mExcludeFilters, sut.getExcludeFilters());
+ }
+
+ @Test
+ public void inFilter_withEmptyFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ assertTrue(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withRelatedIncludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_one");
+
+ assertTrue(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withUnrelatedIncludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_two");
+
+ assertFalse(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withRelatedExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter("filter_one");
+
+ assertFalse(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withUnrelatedExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addExcludeFilter("filter_two");
+
+ assertTrue(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withSameIncludeAndExcludeFilters() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_one");
+ sut.addExcludeFilter("filter_one");
+
+ assertFalse(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withUnrelatedIncludeFilterAndRelatedExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_one");
+ sut.addExcludeFilter("filter_two");
+
+ assertFalse(sut.inFilter("filter_two"));
+ }
+
+ @Test
+ public void inFilter_withRelatedIncludeFilterAndUnrelatedExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_one");
+ sut.addExcludeFilter("filter_two");
+
+ assertTrue(sut.inFilter("filter_one"));
+ }
+
+ @Test
+ public void inFilter_withUnrelatedIncludeFilterAndUnrelatedExcludeFilter() {
+ AppLaunchTest sut = new AppLaunchTest();
+
+ sut.addIncludeFilter("filter_one");
+ sut.addExcludeFilter("filter_two");
+
+ assertFalse(sut.inFilter("filter_three"));
+ }
+
+ private InstrumentationTest createFailingInstrumentationTest() {
+ InstrumentationTest instrumentation =
+ new InstrumentationTest() {
+ @Override
+ public void run(
+ final TestInformation testInfo, final ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ listener.testFailed(new TestDescription("", ""), "test failed");
+ }
+ };
+ return instrumentation;
+ }
+
+ private InstrumentationTest createPassingInstrumentationTest() {
+ InstrumentationTest instrumentation =
+ new InstrumentationTest() {
+ @Override
+ public void run(
+ final TestInformation testInfo, final ITestInvocationListener listener)
+ throws DeviceNotAvailableException {}
+ };
+ return instrumentation;
+ }
+
+ private InstrumentationTest createPassingInstrumentationTestAfterFailing(int failedCount) {
+ InstrumentationTest instrumentation =
+ new InstrumentationTest() {
+ private int mRetryCount = 0;
+
+ @Override
+ public void run(
+ final TestInformation testInfo, final ITestInvocationListener listener)
+ throws DeviceNotAvailableException {
+ if (mRetryCount < failedCount) {
+ listener.testFailed(new TestDescription("", ""), "test failed");
+ }
+ mRetryCount++;
+ }
+ };
+ return instrumentation;
+ }
+
+ private AppLaunchTest createLaunchTestWithInstrumentation(InstrumentationTest instrumentation) {
+ AppLaunchTest appLaunchTest =
+ new AppLaunchTest(TEST_PACKAGE_NAME) {
+ @Override
+ protected InstrumentationTest createInstrumentationTest(
+ String packageBeingTested) {
+ return instrumentation;
+ }
+
+ @Override
+ protected CommandResult resetPackage() throws DeviceNotAvailableException {
+ return new CommandResult(CommandStatus.SUCCESS);
+ }
+ };
+ appLaunchTest.setDevice(mock(ITestDevice.class));
+ return appLaunchTest;
+ }
+
+ private AppLaunchTest createLaunchTestWithRetry(
+ InstrumentationTest instrumentation, int retryCount) {
+ AppLaunchTest appLaunchTest =
+ new AppLaunchTest(TEST_PACKAGE_NAME, retryCount) {
+ @Override
+ protected InstrumentationTest createInstrumentationTest(
+ String packageBeingTested) {
+ return instrumentation;
+ }
+
+ @Override
+ protected CommandResult resetPackage() throws DeviceNotAvailableException {
+ return new CommandResult(CommandStatus.SUCCESS);
+ }
+ };
+ appLaunchTest.setDevice(mock(ITestDevice.class));
+ return appLaunchTest;
+ }
+
+ private AppLaunchTest createLaunchTestWithMockDevice(ITestDevice device) {
+ AppLaunchTest appLaunchTest = new AppLaunchTest(TEST_PACKAGE_NAME);
+ appLaunchTest.setDevice(device);
+ return appLaunchTest;
+ }
+
+ private static void verifyFailedAndEndedCall(ITestInvocationListener listener) {
+ InOrder inOrder = inOrder(listener);
+ inOrder.verify(listener, times(1)).testRunStarted(anyString(), anyInt());
+ inOrder.verify(listener, times(1)).testStarted(anyObject(), anyLong());
+ inOrder.verify(listener, times(1)).testFailed(any(), anyString());
+ inOrder.verify(listener, times(1))
+ .testEnded(anyObject(), anyLong(), (Map<String, String>) any());
+ inOrder.verify(listener, times(1)).testRunEnded(anyLong(), (HashMap<String, Metric>) any());
+ }
+
+ private static void verifyPassedAndEndedCall(ITestInvocationListener listener) {
+ InOrder inOrder = inOrder(listener);
+ inOrder.verify(listener, times(1)).testRunStarted(anyString(), anyInt());
+ inOrder.verify(listener, times(1)).testStarted(anyObject(), anyLong());
+ inOrder.verify(listener, never()).testFailed(any(), anyString());
+ inOrder.verify(listener, times(1))
+ .testEnded(anyObject(), anyLong(), (Map<String, String>) any());
+ inOrder.verify(listener, times(1)).testRunEnded(anyLong(), (HashMap<String, Metric>) any());
+ }
+}
diff --git a/instrumentation/launch/Android.bp b/instrumentation/launch/Android.bp
new file mode 100644
index 0000000..22255f6
--- /dev/null
+++ b/instrumentation/launch/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 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.
+
+android_test {
+ name: "csuite-launch-instrumentation",
+ static_libs: ["androidx.test.rules"],
+ // Include all test java files.
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ manifest: "src/main/AndroidManifest.xml",
+ test_suites: [
+ "csuite",
+ ],
+}
diff --git a/instrumentation/launch/src/main/AndroidManifest.xml b/instrumentation/launch/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7d346b5
--- /dev/null
+++ b/instrumentation/launch/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.compatibilitytest">
+ <uses-sdk android:minSdkVersion="21"
+ android:targetSdkVersion="21" />
+ <application />
+ <uses-permission android:name="android.permission.READ_LOGS" />
+ <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+ <instrumentation
+ android:name=".AppCompatibilityRunner"
+ android:targetPackage="com.android.compatibilitytest"
+ android:label="App Compatibility Test Runner" />
+</manifest>
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
new file mode 100644
index 0000000..5918a62
--- /dev/null
+++ b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2012 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.compatibilitytest;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.ProcessErrorStateInfo;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.IActivityController;
+import android.app.IActivityManager;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.app.UiModeManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.DropBoxManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Application Compatibility Test that launches an application and detects crashes. */
+@RunWith(AndroidJUnit4.class)
+public final class AppCompatibility {
+
+ private static final String TAG = AppCompatibility.class.getSimpleName();
+ private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
+ private static final String APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms";
+ private static final String WORKSPACE_LAUNCH_TIMEOUT_MSECS = "workspace_launch_timeout_ms";
+ private static final Set<String> DROPBOX_TAGS = new HashSet<>();
+ private static final int MAX_CRASH_SNIPPET_LINES = 20;
+ private static final int MAX_NUM_CRASH_SNIPPET = 3;
+
+ // time waiting for app to launch
+ private int mAppLaunchTimeout = 7000;
+ // time waiting for launcher home screen to show up
+ private int mWorkspaceLaunchTimeout = 2000;
+
+ private Context mContext;
+ private ActivityManager mActivityManager;
+ private PackageManager mPackageManager;
+ private Bundle mArgs;
+ private Instrumentation mInstrumentation;
+ private String mLauncherPackageName;
+ private IActivityController mCrashSupressor = new CrashSuppressor();
+ private Map<String, List<String>> mAppErrors = new HashMap<>();
+
+ static {
+ DROPBOX_TAGS.add("SYSTEM_TOMBSTONE");
+ DROPBOX_TAGS.add("system_app_anr");
+ DROPBOX_TAGS.add("system_app_native_crash");
+ DROPBOX_TAGS.add("system_app_crash");
+ DROPBOX_TAGS.add("data_app_anr");
+ DROPBOX_TAGS.add("data_app_native_crash");
+ DROPBOX_TAGS.add("data_app_crash");
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+ // Get permissions for privileged device operations.
+ mInstrumentation.getUiAutomation().adoptShellPermissionIdentity();
+
+ mContext = InstrumentationRegistry.getTargetContext();
+ mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mPackageManager = mContext.getPackageManager();
+ mArgs = InstrumentationRegistry.getArguments();
+
+ // resolve launcher package name
+ Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
+ ResolveInfo resolveInfo =
+ mPackageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ mLauncherPackageName = resolveInfo.activityInfo.packageName;
+ Assert.assertNotNull("failed to resolve package name for launcher", mLauncherPackageName);
+ Log.v(TAG, "Using launcher package name: " + mLauncherPackageName);
+
+ // Parse optional inputs.
+ String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
+ if (appLaunchTimeoutMsecs != null) {
+ mAppLaunchTimeout = Integer.parseInt(appLaunchTimeoutMsecs);
+ }
+ String workspaceLaunchTimeoutMsecs = mArgs.getString(WORKSPACE_LAUNCH_TIMEOUT_MSECS);
+ if (workspaceLaunchTimeoutMsecs != null) {
+ mWorkspaceLaunchTimeout = Integer.parseInt(workspaceLaunchTimeoutMsecs);
+ }
+ mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
+
+ // set activity controller to suppress crash dialogs and collects them by process name
+ mAppErrors.clear();
+ IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
+ .setActivityController(mCrashSupressor, false);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ // unset activity controller
+ IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
+ .setActivityController(null, false);
+ mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
+ }
+
+ /**
+ * Actual test case that launches the package and throws an exception on the first error.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testAppStability() throws Exception {
+ String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
+ if (packageName != null) {
+ Log.d(TAG, "Launching app " + packageName);
+ Intent intent = getLaunchIntentForPackage(packageName);
+ if (intent == null) {
+ Log.w(TAG, String.format("Skipping %s; no launch intent", packageName));
+ return;
+ }
+ long startTime = System.currentTimeMillis();
+ launchActivity(packageName, intent);
+ try {
+ checkDropbox(startTime, packageName);
+ if (mAppErrors.containsKey(packageName)) {
+ StringBuilder message =
+ new StringBuilder("Error(s) detected for package: ")
+ .append(packageName);
+ List<String> errors = mAppErrors.get(packageName);
+ for (int i = 0; i < MAX_NUM_CRASH_SNIPPET && i < errors.size(); i++) {
+ String err = errors.get(i);
+ message.append("\n\n");
+ // limit the size of each crash snippet
+ message.append(truncate(err, MAX_CRASH_SNIPPET_LINES));
+ }
+ if (errors.size() > MAX_NUM_CRASH_SNIPPET) {
+ message.append(
+ String.format(
+ "\n... %d more errors omitted ...",
+ errors.size() - MAX_NUM_CRASH_SNIPPET));
+ }
+ Assert.fail(message.toString());
+ }
+ // last check: see if app process is still running
+ Assert.assertTrue(
+ "app package \""
+ + packageName
+ + "\" no longer found in running "
+ + "tasks, but no explicit crashes were detected; check logcat for "
+ + "details",
+ processStillUp(packageName));
+ } finally {
+ returnHome();
+ }
+ } else {
+ Log.d(
+ TAG,
+ "Missing argument, use "
+ + PACKAGE_TO_LAUNCH
+ + " to specify the package to launch");
+ }
+ }
+
+ /**
+ * Truncate the text to at most the specified number of lines, and append a marker at the end
+ * when truncated
+ *
+ * @param text
+ * @param maxLines
+ * @return
+ */
+ private static String truncate(String text, int maxLines) {
+ String[] lines = text.split("\\r?\\n");
+ StringBuilder ret = new StringBuilder();
+ for (int i = 0; i < maxLines && i < lines.length; i++) {
+ ret.append(lines[i]);
+ ret.append('\n');
+ }
+ if (lines.length > maxLines) {
+ ret.append("... ");
+ ret.append(lines.length - maxLines);
+ ret.append(" more lines truncated ...\n");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Check dropbox for entries of interest regarding the specified process
+ *
+ * @param startTime if not 0, only check entries with timestamp later than the start time
+ * @param processName the process name to check for
+ */
+ private void checkDropbox(long startTime, String processName) {
+ DropBoxManager dropbox =
+ (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE);
+ DropBoxManager.Entry entry = null;
+ while (null != (entry = dropbox.getNextEntry(null, startTime))) {
+ try {
+ // only check entries with tag that's of interest
+ String tag = entry.getTag();
+ if (DROPBOX_TAGS.contains(tag)) {
+ String content = entry.getText(4096);
+ if (content != null) {
+ if (content.contains(processName)) {
+ addProcessError(processName, "dropbox:" + tag, content);
+ }
+ }
+ }
+ startTime = entry.getTimeMillis();
+ } finally {
+ entry.close();
+ }
+ }
+ }
+
+ private void returnHome() {
+ Intent homeIntent = new Intent(Intent.ACTION_MAIN);
+ homeIntent.addCategory(Intent.CATEGORY_HOME);
+ homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Send the "home" intent and wait 2 seconds for us to get there
+ mContext.startActivity(homeIntent);
+ try {
+ Thread.sleep(mWorkspaceLaunchTimeout);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ private Intent getLaunchIntentForPackage(String packageName) {
+ UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
+ boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
+ Intent intent = null;
+ if (isLeanback) {
+ intent = mPackageManager.getLeanbackLaunchIntentForPackage(packageName);
+ } else {
+ intent = mPackageManager.getLaunchIntentForPackage(packageName);
+ }
+ return intent;
+ }
+
+ /**
+ * Launches and activity and queries for errors.
+ *
+ * @param packageName {@link String} the package name of the application to launch.
+ * @return {@link Collection} of {@link ProcessErrorStateInfo} detected during the app launch.
+ */
+ private void launchActivity(String packageName, Intent intent) {
+ Log.d(
+ TAG,
+ String.format(
+ "launching package \"%s\" with intent: %s",
+ packageName, intent.toString()));
+
+ // Launch Activity
+ mContext.startActivity(intent);
+
+ try {
+ // artificial delay: in case app crashes after doing some work during launch
+ Thread.sleep(mAppLaunchTimeout);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+
+ private void addProcessError(String processName, String errorType, String errorInfo) {
+ // parse out the package name if necessary, for apps with multiple processes
+ String pkgName = processName.split(":", 2)[0];
+ List<String> errors;
+ if (mAppErrors.containsKey(pkgName)) {
+ errors = mAppErrors.get(pkgName);
+ } else {
+ errors = new ArrayList<>();
+ }
+ errors.add(String.format("### Type: %s, Details:\n%s", errorType, errorInfo));
+ mAppErrors.put(pkgName, errors);
+ }
+
+ /**
+ * Determine if a given package is still running.
+ *
+ * @param packageName {@link String} package to look for
+ * @return True if package is running, false otherwise.
+ */
+ private boolean processStillUp(String packageName) {
+ @SuppressWarnings("deprecation")
+ List<RunningTaskInfo> infos = mActivityManager.getRunningTasks(100);
+ for (RunningTaskInfo info : infos) {
+ if (info.baseActivity.getPackageName().equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * An {@link IActivityController} that instructs framework to kill processes hitting crashes
+ * directly without showing crash dialogs
+ */
+ private class CrashSuppressor extends IActivityController.Stub {
+
+ @Override
+ public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+ Log.d(TAG, "activity starting: " + intent.getComponent().toShortString());
+ return true;
+ }
+
+ @Override
+ public boolean activityResuming(String pkg) throws RemoteException {
+ Log.d(TAG, "activity resuming: " + pkg);
+ return true;
+ }
+
+ @Override
+ public boolean appCrashed(
+ String processName,
+ int pid,
+ String shortMsg,
+ String longMsg,
+ long timeMillis,
+ String stackTrace)
+ throws RemoteException {
+ Log.d(TAG, "app crash: " + processName);
+ addProcessError(processName, "crash", stackTrace);
+ // don't show dialog
+ return false;
+ }
+
+ @Override
+ public int appEarlyNotResponding(String processName, int pid, String annotation)
+ throws RemoteException {
+ // ignore
+ return 0;
+ }
+
+ @Override
+ public int appNotResponding(String processName, int pid, String processStats)
+ throws RemoteException {
+ Log.d(TAG, "app ANR: " + processName);
+ addProcessError(processName, "ANR", processStats);
+ // don't show dialog
+ return -1;
+ }
+
+ @Override
+ public int systemNotResponding(String msg) throws RemoteException {
+ // ignore
+ return -1;
+ }
+ }
+}
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
new file mode 100644
index 0000000..943cea9
--- /dev/null
+++ b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2012 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.compatibilitytest;
+
+import androidx.test.runner.AndroidJUnitRunner;
+
+// empty subclass to maintain backwards compatibility on host-side harness
+public class AppCompatibilityRunner extends AndroidJUnitRunner {}
diff --git a/tools/build/config.mk b/tools/build/config.mk
new file mode 100644
index 0000000..f773d9b
--- /dev/null
+++ b/tools/build/config.mk
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2019 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.
+
+COMPATIBILITY_TESTCASES_OUT_csuite := $(HOST_OUT)/csuite/android-csuite/testcases
diff --git a/tools/csuite-tradefed/Android.bp b/tools/csuite-tradefed/Android.bp
new file mode 100644
index 0000000..82d959e
--- /dev/null
+++ b/tools/csuite-tradefed/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2018 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.
+
+tradefed_binary_host {
+ name: "csuite-tradefed",
+ wrapper: "src/scripts/csuite-tradefed",
+ short_name: "CSUITE",
+ full_name: "App Compatibility Test Suite",
+ version: "1.0",
+ static_libs: [
+ "cts-tradefed-harness",
+ "csuite-harness",
+ ],
+}
+
+java_test_host {
+ name: "csuite-tradefed-tests",
+ srcs: [
+ "src/test/java/**/*.java",
+ ],
+ static_libs: [
+ "tradefed",
+ "csuite-tradefed",
+ ],
+ test_suites: ["general-tests"],
+}
diff --git a/tools/csuite-tradefed/AndroidTest.xml b/tools/csuite-tradefed/AndroidTest.xml
new file mode 100644
index 0000000..850750e
--- /dev/null
+++ b/tools/csuite-tradefed/AndroidTest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="class" value="com.android.compatibility.tradefed.CSuiteTradefedTest" />
+ </test>
+</configuration>
diff --git a/tools/csuite-tradefed/README b/tools/csuite-tradefed/README
new file mode 100644
index 0000000..32cf936
--- /dev/null
+++ b/tools/csuite-tradefed/README
@@ -0,0 +1,45 @@
+C-Suite Trade Federation
+---------------------------
+
+App Compatibility Test Suite Trade Federation, csuite-tradefed for short, is built on
+top of the Android Trade Federation test harness to test app compatibility.
+
+Configuring csuite-tradefed
+---------------------------
+
+Ensure 'adb' is in your current PATH. adb can be found in the
+Android SDK available from http://developer.android.com
+
+Example:
+ PATH=$PATH:/home/myuser/android-sdk-linux_x86/platform-tools
+
+And ensure that the device is visible via 'adb devices'
+
+Using csuite-tradefed
+---------------------------
+
+To run a test plan on a single device:
+
+1. Make sure you have at least one device connected
+2. Launch the csuite-tradefed console by running the 'csuite-tradefed'. If you
+are working from the Android source tree and have run 'm csuite',
+the script can be found at
+ out/host/linux-x86/csuite/android-csuite/tools/csuite-tradefed
+
+C-Suite Tradefed Development
+---------------------------
+
+See http://source.android.com for instructions on obtaining the Android
+platform source code and setting up a build environment.
+
+The source for the tradefed framework can be found on the 'tradefed' branch.
+
+Perform these steps to build and run csuite from the development environment:
+
+cd <path to android source root>
+make csuite
+csuite-tradefed
+run <plan-name>
+
+More documentation and details on using and extending trade federation will
+be forthcoming in the near future.
diff --git a/tools/csuite-tradefed/TEST_MAPPING b/tools/csuite-tradefed/TEST_MAPPING
new file mode 100644
index 0000000..89c2072
--- /dev/null
+++ b/tools/csuite-tradefed/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+ "postsubmit": [
+ {
+ "name": "csuite-tradefed-tests",
+ "host": true
+ }
+ ]
+}
diff --git a/tools/csuite-tradefed/src/scripts/csuite-tradefed b/tools/csuite-tradefed/src/scripts/csuite-tradefed
new file mode 100644
index 0000000..4277884
--- /dev/null
+++ b/tools/csuite-tradefed/src/scripts/csuite-tradefed
@@ -0,0 +1,122 @@
+#!/bin/bash
+
+# Copyright (C) 2019 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.
+
+# The launcher script for running C-Suite.
+# This can be run from an Android build environment or a standalone C-Suite zip.
+
+checkFile() {
+ if [ ! -f "$1" ]; then
+ echo "Unable to locate $1"
+ exit
+ fi;
+}
+
+checkPath() {
+ if ! type -P $1 &> /dev/null; then
+ echo "Unable to find $1 in path."
+ exit
+ fi;
+}
+
+checkPath adb
+checkPath java
+
+# Check the Java version.
+JAVA_VERSION=$(java -version 2>&1 | head -n 1 | grep 'version [ "]\(1\.8\|9\|11\).*[ "]')
+if [ "${JAVA_VERSION}" == "" ]; then
+ echo "Wrong java version. 1.8, 9, or 11 is required."
+ exit
+fi
+
+# Check for the debug flag and set up remote debugging.
+if [ -n "${TF_DEBUG}" ]; then
+ if [ -z "${TF_DEBUG_PORT}" ]; then
+ TF_DEBUG_PORT=10088
+ fi
+ RDBG_FLAG=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}
+fi
+
+# Get OS.
+HOST=`uname`
+if [ "$HOST" == "Linux" ]; then
+ OS="linux-x86"
+elif [ "$HOST" == "Darwin" ]; then
+ OS="darwin-x86"
+else
+ echo "Unrecognized OS"
+ exit
+fi
+
+# Check if in Android build environment.
+if [ ! -z "${ANDROID_BUILD_TOP}" ]; then
+ if [ ! -z "${ANDROID_HOST_OUT}" ]; then
+ CSUITE_ROOT=${ANDROID_HOST_OUT}/csuite
+ else
+ CSUITE_ROOT=${ANDROID_BUILD_TOP}/${OUT_DIR:-out}/host/${OS}/csuite
+ fi
+ if [ ! -d ${CSUITE_ROOT} ]; then
+ echo "Could not find $CSUITE_ROOT in Android build environment. Try 'make csuite'"
+ exit
+ fi;
+fi;
+
+if [ -z ${CSUITE_ROOT} ]; then
+ # Assume we're in an extracted csuite install.
+ CSUITE_ROOT="$(dirname $0)/../.."
+fi;
+
+JAR_DIR=${CSUITE_ROOT}/android-csuite/tools
+
+TRADEFED_JAR="tradefed"
+
+JARS="tradefed
+ hosttestlib
+ compatibility-host-util
+ csuite-tradefed
+ csuite-tradefed-tests"
+
+for JAR in $JARS; do
+ checkFile ${JAR_DIR}/${JAR}.jar
+ JAR_PATH=${JAR_PATH}:${JAR_DIR}/${JAR}.jar
+done
+
+OPTIONAL_JARS="
+ google-tradefed
+ google-tradefed-tests
+ google-tf-prod-tests"
+
+for JAR in $OPTIONAL_JARS; do
+ if [ -f "$JAR.jar" ]; then
+ JAR_PATH=${JAR_PATH}:${JAR_DIR}/${JAR}.jar
+ fi;
+done
+
+# Load any shared libraries for host-side executables.
+LIB_DIR=${CSUITE_ROOT}/android-csuite/lib
+if [ "$HOST" == "Linux" ]; then
+ LD_LIBRARY_PATH=${LIB_DIR}:${LIB_DIR}64:${LD_LIBRARY_PATH}
+ export LD_LIBRARY_PATH
+elif [ "$HOST" == "Darwin" ]; then
+ DYLD_LIBRARY_PATH=${LIB_DIR}:${LIB_DIR}64:${DYLD_LIBRARY_PATH}
+ export DYLD_LIBRARY_PATH
+fi
+
+# Include any host-side test jars.
+for j in ${CSUITE_ROOT}/android-csuite/testcases/*.jar; do
+ JAR_PATH=${JAR_PATH}:$j
+done
+
+java $RDBG_FLAG -cp ${JAR_PATH} -DMTS_ROOT=${CSUITE_ROOT} com.android.compatibility.common.tradefed.command.CompatibilityConsole "$@"
diff --git a/tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java b/tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java
new file mode 100644
index 0000000..709798b
--- /dev/null
+++ b/tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 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.compatibility.tradefed;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider;
+
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class CSuiteTradefedTest {
+
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ private static final String ROOT_DIR_PROPERTY_NAME = "CSUITE_ROOT";
+ private static final String SUITE_FULL_NAME = "App Compatibility Test Suite";
+ private static final String SUITE_NAME = "C-Suite";
+
+ private CompatibilityBuildProvider mProvider;
+
+ @Before
+ public void setUp() throws Exception {
+ System.setProperty(ROOT_DIR_PROPERTY_NAME, tempFolder.getRoot().getAbsolutePath());
+ File base = tempFolder.newFolder("android-csuite");
+ File tests = tempFolder.newFolder("testcases");
+
+ mProvider =
+ new CompatibilityBuildProvider() {
+ @Override
+ protected String getSuiteInfoName() {
+ return SUITE_NAME;
+ }
+
+ @Override
+ protected String getSuiteInfoFullname() {
+ return SUITE_FULL_NAME;
+ }
+ };
+ }
+
+ @Test
+ public void testSuiteInfoLoad() throws Exception {
+ CompatibilityBuildHelper helper = new CompatibilityBuildHelper(mProvider.getBuild());
+ assertEquals("Incorrect suite full name", SUITE_FULL_NAME, helper.getSuiteFullName());
+ assertEquals("Incorrect suite name", SUITE_NAME, helper.getSuiteName());
+ }
+
+ @After
+ public void cleanUp() throws Exception {
+ System.clearProperty(ROOT_DIR_PROPERTY_NAME);
+ }
+}
diff --git a/tools/script/generate_module.py b/tools/script/generate_module.py
new file mode 100644
index 0000000..30ae7b4
--- /dev/null
+++ b/tools/script/generate_module.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2020 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.
+#
+# This script generates C-Suite configuration files for a list of apps.
+
+import argparse
+import glob
+import os
+import sys
+from xml.dom import minidom
+from xml.etree import cElementTree as ET
+from xml.sax import saxutils
+
+from typing import IO, List, Text
+
+_ANDROID_BP_FILE_NAME = 'Android.bp'
+_ANDROID_XML_FILE_NAME = 'AndroidTest.xml'
+
+_TF_TEST_APP_INSTALL_SETUP =\
+ 'com.android.tradefed.targetprep.TestAppInstallSetup'
+_CSUITE_APP_SETUP_PREPARER =\
+ 'com.android.compatibility.targetprep.AppSetupPreparer'
+_CSUITE_LAUNCH_TEST_CLASS =\
+ 'com.android.compatibility.testtype.AppLaunchTest'
+
+_CONFIG_TYPE_TARGET_PREPARER = 'target_preparer'
+_CONFIG_TYPE_TEST = 'test'
+
+
+def generate_all_modules_from_config(package_list_file_path, root_dir):
+ """Generate multiple test and build modules.
+
+ Args:
+ package_list_file_path: path of a file containing package names.
+ root_dir: root directory that modules will be generated in.
+ """
+ remove_existing_package_files(root_dir)
+
+ with open(package_list_file_path) as fp:
+ for line in parse_package_list(fp):
+ _generate_module_files(line.strip(), root_dir)
+
+
+def remove_existing_package_files(root_dir):
+ for filename in glob.iglob(root_dir + '**/AndroidTest.xml'):
+ if _is_auto_generated(filename):
+ os.remove(filename)
+
+ for filename in glob.iglob(root_dir + '**/Android.bp'):
+ if _is_auto_generated(filename):
+ os.remove(filename)
+
+ _remove_empty_dirs(root_dir)
+
+
+def _is_auto_generated(filename):
+ with open(filename, 'r') as f:
+ return 'auto-generated' in f.read()
+
+
+def _remove_empty_dirs(path):
+ for filename in os.listdir(path):
+ file_path = os.path.join(path, filename)
+ if os.path.isdir(file_path) and not os.listdir(file_path):
+ os.rmdir(file_path)
+
+
+def parse_package_list(package_list_file: IO[bytes]) -> List[bytes]:
+ return {
+ line.strip() for line in package_list_file.readlines() if line.strip()}
+
+
+def _generate_module_files(package_name, root_dir):
+ """Generate test and build modules for a single package.
+
+ Args:
+ package_name: package name of test and build modules.
+ root_dir: root directory that modules will be generated in.
+ """
+ package_dir = _create_package_dir(root_dir, package_name)
+
+ build_module_path = os.path.join(package_dir, _ANDROID_BP_FILE_NAME)
+ test_module_path = os.path.join(package_dir, _ANDROID_XML_FILE_NAME)
+
+ with open(build_module_path, 'w') as f:
+ write_build_module(package_name, f)
+
+ with open(test_module_path, 'w') as f:
+ write_test_module(package_name, f)
+
+
+def _create_package_dir(root_dir, package_name):
+ package_dir_path = os.path.join(root_dir, package_name)
+ os.mkdir(package_dir_path)
+
+ return package_dir_path
+
+
+def write_build_module(package_name: Text, out_file: IO[bytes]) -> Text:
+ build_module = _BUILD_MODULE_HEADER \
+ + _BUILD_MODULE_TEMPLATE.format(package_name=package_name)
+ out_file.write(build_module)
+
+
+def write_test_module(package_name: Text, out_file: IO[bytes]) -> Text:
+ configuration = ET.Element('configuration', {
+ 'description': 'Tests the compatibility of apps'
+ })
+ ET.SubElement(
+ configuration, 'option', {
+ 'name': 'config-descriptor:metadata',
+ 'key': 'plan',
+ 'value': 'csuite-launch'
+ }
+ )
+ ET.SubElement(
+ configuration, 'option', {
+ 'name': 'package-name',
+ 'value': package_name
+ }
+ )
+ test_file_name_option = {
+ 'name': 'test-file-name',
+ 'value': 'csuite-launch-instrumentation.apk'
+ }
+ _add_element_with_option(
+ configuration,
+ _CONFIG_TYPE_TARGET_PREPARER,
+ _TF_TEST_APP_INSTALL_SETUP,
+ options=[test_file_name_option]
+ )
+ _add_element_with_option(
+ configuration,
+ _CONFIG_TYPE_TARGET_PREPARER,
+ _CSUITE_APP_SETUP_PREPARER
+ )
+ _add_element_with_option(
+ configuration,
+ _CONFIG_TYPE_TEST,
+ _CSUITE_LAUNCH_TEST_CLASS
+ )
+
+ test_module = _TEST_MODULE_HEADER + _prettify(configuration)
+ out_file.write(test_module)
+
+
+def _add_element_with_option(elem, sub_elem, class_name, options=None):
+ if options is None:
+ options = []
+
+ new_elem = ET.SubElement(
+ elem, sub_elem, {
+ 'class': class_name,
+ }
+ )
+ for option in options:
+ ET.SubElement(
+ new_elem, 'option', option
+ )
+
+
+def _prettify(elem: ET.Element) -> Text:
+ declaration = minidom.Document().toxml()
+ parsed = minidom.parseString(ET.tostring(elem, 'utf-8'))
+
+ return saxutils.unescape(
+ parsed.toprettyxml(indent=' ')[len(declaration) + 1:])
+
+_BUILD_MODULE_HEADER = """// Copyright (C) 2020 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.
+
+// This file was auto-generated by test/app_compat/csuite/tools/script/generate_module.py.
+// Do not edit manually.
+
+"""
+
+_BUILD_MODULE_TEMPLATE = """csuite_config {{
+ name: "csuite_{package_name}",
+}}
+"""
+
+_TEST_MODULE_HEADER = """<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<!-- This file was auto-generated by test/app_compat/csuite/tools/script/generate_module.py.
+ Do not edit manually.
+-->
+
+"""
+
+
+def _file_path(path):
+ if os.path.isfile(path):
+ return path
+ raise argparse.ArgumentTypeError('%s is not a valid path' % path)
+
+
+def _dir_path(path):
+ if os.path.isdir(path):
+ return path
+ raise argparse.ArgumentTypeError('%s is not a valid path' % path)
+
+
+def parse_args(args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--package_list',
+ type=_file_path,
+ required=True,
+ help='path of the file containing package names')
+ parser.add_argument('--root_dir',
+ type=_dir_path,
+ required=True,
+ help='path of the root directory that' +
+ 'modules will be generated in')
+ return parser.parse_args(args)
+
+
+def main():
+ parser = parse_args(sys.argv[1:])
+ generate_all_modules_from_config(parser.package_list, parser.root_dir)
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/script/generate_module_unittest.py b/tools/script/generate_module_unittest.py
new file mode 100644
index 0000000..4d24162
--- /dev/null
+++ b/tools/script/generate_module_unittest.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2020 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.
+
+import io
+import os
+import unittest
+
+from lxml import etree
+from pyfakefs import fake_filesystem_unittest
+
+import generate_module
+
+
+class WriteTestModuleTest(unittest.TestCase):
+
+ def test_xml_is_valid(self):
+ package_name = 'package_name'
+ out = io.StringIO()
+
+ generate_module.write_test_module(package_name, out)
+
+ test_module_generated = out.getvalue()
+ self.assertTrue(self._contains_license(test_module_generated))
+ self.assertTrue(self._is_validate_xml(test_module_generated))
+
+ def _contains_license(self, generated_str: bytes) -> bool:
+ return 'Copyright' in generated_str and \
+ 'Android Open Source Project' in generated_str
+
+ def _is_validate_xml(self, xml_str: bytes) -> bool:
+ xmlschema_doc = etree.parse(
+ io.BytesIO('''<?xml version="1.0" encoding="UTF-8" ?>
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="configuration">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="option" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="target_preparer" minOccurs="0" maxOccurs="unbounded"/>
+ <xs:element name="test" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ <xs:attribute name="description"/>
+ </xs:complexType>
+ </xs:element>
+ </xs:schema>
+ '''.encode('utf8')))
+ xmlschema = etree.XMLSchema(xmlschema_doc)
+
+ xml_doc = etree.parse(io.BytesIO(xml_str.encode('utf8')))
+ result = xmlschema.validate(xml_doc)
+
+ return result
+
+
+class WriteBuildModuleTest(unittest.TestCase):
+
+ def test_build_file_is_valid(self):
+ package_name = 'package_name'
+ out = io.StringIO()
+
+ generate_module.write_build_module(package_name, out)
+
+ build_module_generated = out.getvalue()
+ self.assertTrue(self._contains_license(build_module_generated))
+ self.assertTrue(self._are_parentheses_balanced(build_module_generated))
+ self.assertIn('csuite_config', build_module_generated)
+ self.assertIn(package_name, build_module_generated)
+
+ def _contains_license(self, generated_str: bytes) -> bool:
+ return 'Copyright' in generated_str and \
+ 'Android Open Source Project' in generated_str
+
+ def _are_parentheses_balanced(self, generated_str: bytes) -> bool:
+ parenthese_count = 0
+
+ for elem in generated_str:
+ if elem == '{':
+ parenthese_count += 1
+ elif elem == '}':
+ parenthese_count -= 1
+
+ if parenthese_count < 0:
+ return False
+
+ return parenthese_count == 0
+
+
+class ParsePackageListTest(unittest.TestCase):
+
+ def test_accepts_empty_lines(self):
+ input = io.StringIO('\n\n\npackage_name\n\n')
+
+ package_list = generate_module.parse_package_list(input)
+
+ self.assertEqual(len(package_list), 1)
+ self.assertIn('package_name', package_list)
+ self.assertTrue(all(package_list))
+
+ def test_strips_trailing_whitespace(self):
+ input = io.StringIO(' package_name ')
+
+ package_list = generate_module.parse_package_list(input)
+
+ self.assertEqual(len(package_list), 1)
+ self.assertIn('package_name', package_list)
+ self.assertTrue(all(package_list))
+
+ def test_duplicate_package_name(self):
+ input = io.StringIO('\n\npackage_name\n\npackage_name\n')
+
+ package_list = generate_module.parse_package_list(input)
+
+ self.assertEqual(len(package_list), 1)
+ self.assertIn('package_name', package_list)
+ self.assertTrue(all(package_list))
+
+
+class ParseArgsTest(fake_filesystem_unittest.TestCase):
+
+ def setUp(self):
+ super(ParseArgsTest, self).setUp()
+ self.setUpPyfakefs()
+
+ def test_configuration_file_not_exist(self):
+ package_list_file_path = '/test/package_list.txt'
+ root_dir = '/test/modules'
+ os.makedirs(root_dir)
+
+ with self.assertRaises(SystemExit):
+ generate_module.parse_args(
+ ['--package_list', package_list_file_path,
+ '--root_dir', root_dir])
+
+ def test_module_dir_not_exist(self):
+ package_list_file_path = '/test/package_list.txt'
+ package_name1 = 'package_name_1'
+ package_name2 = 'package_name_2'
+ self.fs.create_file(package_list_file_path,
+ contents=(package_name1+'\n'+package_name2))
+ root_dir = '/test/modules'
+
+ with self.assertRaises(SystemExit):
+ generate_module.parse_args(
+ ['--package_list', package_list_file_path,
+ '--root_dir', root_dir])
+
+
+class GenerateAllModulesFromConfigTest(fake_filesystem_unittest.TestCase):
+
+ def setUp(self):
+ super(GenerateAllModulesFromConfigTest, self).setUp()
+ self.setUpPyfakefs()
+
+ def test_creates_package_files(self):
+ package_list_file_path = '/test/package_list.txt'
+ package_name1 = 'package_name_1'
+ package_name2 = 'package_name_2'
+ self.fs.create_file(package_list_file_path,
+ contents=(package_name1+'\n'+package_name2))
+ root_dir = '/test/modules'
+ self.fs.create_dir(root_dir)
+
+ generate_module.generate_all_modules_from_config(
+ package_list_file_path, root_dir)
+
+ self.assertTrue(os.path.exists(
+ os.path.join(root_dir, package_name1, 'Android.bp')))
+ self.assertTrue(os.path.exists(
+ os.path.join(root_dir, package_name1, 'AndroidTest.xml')))
+ self.assertTrue(os.path.exists(
+ os.path.join(root_dir, package_name2, 'Android.bp')))
+ self.assertTrue(os.path.exists(
+ os.path.join(root_dir, package_name2, 'AndroidTest.xml')))
+
+ def test_removes_all_existing_package_files(self):
+ root_dir = '/test/'
+ package_dir = '/test/existing_package/'
+ existing_package_file1 = 'test/existing_package/AndroidTest.xml'
+ existing_package_file2 = 'test/existing_package/Android.bp'
+ self.fs.create_file(existing_package_file1, contents='auto-generated')
+ self.fs.create_file(existing_package_file2, contents='auto-generated')
+
+ generate_module.remove_existing_package_files(root_dir)
+
+ self.assertFalse(os.path.exists(existing_package_file1))
+ self.assertFalse(os.path.exists(existing_package_file2))
+ self.assertFalse(os.path.exists(package_dir))
+
+
+if __name__ == '__main__':
+ unittest.main()