aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp29
-rw-r--r--OWNERS5
-rw-r--r--PREUPLOAD.cfg1
-rw-r--r--harness/Android.bp20
-rw-r--r--harness/AndroidTest.xml20
-rw-r--r--harness/TEST_MAPPING8
-rw-r--r--harness/src/main/java/com/android/compatibility/AppCompatibilityTest.java671
-rw-r--r--harness/src/main/java/com/android/compatibility/AppCrawlerCompatibilityTest.java82
-rw-r--r--harness/src/main/java/com/android/compatibility/AppLaunchCompatibilityTest.java55
-rw-r--r--harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java221
-rw-r--r--harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java56
-rw-r--r--harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java87
-rw-r--r--harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java57
-rw-r--r--harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java282
-rw-r--r--harness/src/main/java/com/android/csuite/config/ModuleGenerator.java257
-rw-r--r--harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java39
-rw-r--r--harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java60
-rw-r--r--harness/src/main/java/com/android/csuite/core/PackageNameProvider.java31
-rw-r--r--harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java296
-rw-r--r--harness/src/main/resources/META-INF/services/com.android.tradefed.config.remote.IRemoteFileResolver1
-rw-r--r--harness/src/main/resources/config/csuite-base.xml9
-rw-r--r--harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java334
-rw-r--r--harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java57
-rw-r--r--harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java483
-rw-r--r--harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java289
-rw-r--r--harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java68
-rw-r--r--harness/src/test/java/com/android/csuite/CSuiteUnitTests.java (renamed from harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java)20
-rw-r--r--harness/src/test/java/com/android/csuite/config/AppRemoteFileResolverTest.java279
-rw-r--r--harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java321
-rw-r--r--harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java45
-rw-r--r--harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java116
-rw-r--r--harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java425
-rw-r--r--harness/src/test/java/com/android/csuite/testing/Correspondences.java47
-rw-r--r--harness/src/test/java/com/android/csuite/testing/CorrespondencesTest.java50
-rw-r--r--harness/src/test/java/com/android/csuite/testing/MoreAsserts.java59
-rw-r--r--harness/src/test/java/com/android/csuite/testing/MoreAssertsTest.java86
-rw-r--r--instrumentation/launch/Android.bp9
-rw-r--r--instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java121
-rw-r--r--integration_tests/Android.bp116
-rw-r--r--integration_tests/TEST_MAPPING10
-rw-r--r--integration_tests/csuite_cli_test.py33
-rw-r--r--integration_tests/csuite_crash_detection_test.py104
-rw-r--r--integration_tests/csuite_crash_on_launch_test_app/Android.bp27
-rwxr-xr-xintegration_tests/csuite_crash_on_launch_test_app/AndroidManifest.xml27
-rw-r--r--integration_tests/csuite_crash_on_launch_test_app/TestAppActivity.java30
-rw-r--r--integration_tests/csuite_no_crash_test_app/Android.bp27
-rwxr-xr-xintegration_tests/csuite_no_crash_test_app/AndroidManifest.xml27
-rw-r--r--integration_tests/csuite_no_crash_test_app/TestAppActivity.java29
-rw-r--r--integration_tests/csuite_test_template.xml (renamed from harness/src/main/resources/config/launch.xml)15
-rw-r--r--integration_tests/csuite_test_utils.py283
-rw-r--r--pylib/Android.bp42
-rw-r--r--pylib/csuite_test.py107
-rw-r--r--pylintrc427
-rw-r--r--test_targets/csuite-app-launch/Android.bp22
-rw-r--r--test_targets/csuite-app-launch/template.xml28
-rw-r--r--test_targets/csuite-pre-installed-app-launch/Android.bp22
-rw-r--r--test_targets/csuite-pre-installed-app-launch/template.xml25
-rw-r--r--test_targets/csuite-system-app-launch/Android.bp22
-rw-r--r--test_targets/csuite-system-app-launch/template.xml28
-rw-r--r--test_targets/csuite-test-package-launch/Android.bp22
-rw-r--r--test_targets/csuite-test-package-launch/template.xml29
-rw-r--r--tools/csuite-tradefed/Android.bp8
-rw-r--r--tools/csuite-tradefed/AndroidTest.xml20
-rw-r--r--tools/csuite-tradefed/TEST_MAPPING8
-rw-r--r--tools/csuite-tradefed/src/scripts/csuite-tradefed5
-rw-r--r--tools/csuite_test/Android.bp20
-rw-r--r--tools/csuite_test/csuite_test.go132
-rw-r--r--tools/csuite_test/csuite_test_test.go286
-rw-r--r--tools/csuite_test/go.mod14
-rw-r--r--tools/script/Android.bp47
-rw-r--r--tools/script/csuite_test_template.xml23
-rw-r--r--tools/script/generate_module.py371
-rw-r--r--tools/script/generate_module_test.py245
-rw-r--r--tools/script/generate_module_unittest.py203
74 files changed, 6047 insertions, 1933 deletions
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..5295e32
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,29 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_defaults {
+ name: "csuite_python_defaults",
+ version: {
+ py2: {
+ enabled: false,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+}
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..b78e055
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,5 @@
+adirao@google.com
+fdeng@google.com
+hzalek@google.com
+yuexima@google.com
+zhuoyao@google.com
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 948fa79..91c7914 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,5 +1,6 @@
[Builtin Hooks]
bpfmt = true
+gofmt = true
google_java_format = true
pylint = true
xmllint = true
diff --git a/harness/Android.bp b/harness/Android.bp
index 2dec437..5111c1b 100644
--- a/harness/Android.bp
+++ b/harness/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
java_library_host {
name: "csuite-harness",
srcs: [
@@ -23,6 +27,9 @@ java_library_host {
libs: [
"tradefed",
],
+ static_libs: [
+ "compatibility-tradefed",
+ ]
}
java_test_host {
@@ -30,14 +37,17 @@ java_test_host {
srcs: [
"src/test/java/**/*.java",
],
- libs: [
- "csuite-harness",
- "tradefed",
- ],
static_libs: [
+ "tradefed",
+ "csuite-harness",
+ "compatibility-tradefed",
+ "guava-testlib",
+ "jimfs",
"mockito-host",
"objenesis",
"testng",
],
- test_suites: ["general-tests"],
+ test_options: {
+ unit_test: true,
+ },
}
diff --git a/harness/AndroidTest.xml b/harness/AndroidTest.xml
deleted file mode 100644
index f3fb637..0000000
--- a/harness/AndroidTest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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
deleted file mode 100644
index 50ed1cd..0000000
--- a/harness/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "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
deleted file mode 100644
index 74c4907..0000000
--- a/harness/src/main/java/com/android/compatibility/AppCompatibilityTest.java
+++ /dev/null
@@ -1,671 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 5c0fe11..0000000
--- a/harness/src/main/java/com/android/compatibility/AppCrawlerCompatibilityTest.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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
deleted file mode 100644
index dd1f648..0000000
--- a/harness/src/main/java/com/android/compatibility/AppLaunchCompatibilityTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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/targetprep/AppSetupPreparer.java b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
index b742c2f..12e30f7 100644
--- a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
+++ b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
@@ -17,99 +17,230 @@
package com.android.compatibility.targetprep;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
+import com.android.csuite.core.SystemPackageUninstaller;
import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
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.android.tradefed.util.AaptParser.AaptVersion;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.SimpleTimeLimiter;
+import com.google.common.util.concurrent.TimeLimiter;
+import com.google.common.util.concurrent.UncheckedTimeoutException;
import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
/** 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";
+ @VisibleForTesting
+ static final String OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS =
+ "wait-for-device-available-seconds";
+
+ @VisibleForTesting
+ static final String OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS =
+ "exponential-backoff-multiplier-seconds";
- @Option(name = "package-name", description = "Package name of the app being tested.")
+ @VisibleForTesting static final String OPTION_TEST_FILE_NAME = "test-file-name";
+ @VisibleForTesting static final String OPTION_INSTALL_ARG = "install-arg";
+ @VisibleForTesting static final String OPTION_SETUP_TIMEOUT_MILLIS = "setup-timeout-millis";
+ @VisibleForTesting static final String OPTION_MAX_RETRY = "max-retry";
+ @VisibleForTesting static final String OPTION_AAPT_VERSION = "aapt-version";
+ @VisibleForTesting static final String OPTION_INCREMENTAL_INSTALL = "incremental";
+
+ @Option(name = "package-name", description = "Package name of testing app.")
private String mPackageName;
- private final TestAppInstallSetup mAppInstallSetup;
+ @Option(
+ name = OPTION_TEST_FILE_NAME,
+ description = "the name of an apk file to be installed on device. Can be repeated.")
+ private final List<File> mTestFiles = new ArrayList<>();
+
+ @Option(name = OPTION_AAPT_VERSION, description = "The version of AAPT for APK parsing.")
+ private AaptVersion mAaptVersion = AaptVersion.AAPT2;
+
+ @Option(
+ name = OPTION_INSTALL_ARG,
+ description =
+ "Additional arguments to be passed to install command, "
+ + "including leading dash, e.g. \"-d\"")
+ private final List<String> mInstallArgs = new ArrayList<>();
+
+ @Option(
+ name = OPTION_INCREMENTAL_INSTALL,
+ description = "Enable packages to be installed incrementally.")
+ private boolean mIncrementalInstallation = false;
+
+ @Option(name = OPTION_MAX_RETRY, description = "Max number of retries upon TargetSetupError.")
+ private int mMaxRetry = 0;
+
+ @Option(
+ name = OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS,
+ description =
+ "The exponential backoff multiplier for retries in seconds. "
+ + "A value n means the preparer will wait for n^(retry_count) "
+ + "seconds between retries.")
+ private int mExponentialBackoffMultiplierSeconds = 0;
+
+ @Option(
+ name = OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS,
+ description =
+ "Timeout value for waiting for device available in seconds. "
+ + "A negative value means not to check device availability.")
+ private int mWaitForDeviceAvailableSeconds = -1;
+
+ @Option(
+ name = OPTION_SETUP_TIMEOUT_MILLIS,
+ description =
+ "Timeout value for a setUp operation. "
+ + "Note that the timeout is not a global timeout and will "
+ + "be applied to each retry attempt.")
+ private long mSetupOnceTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
+
+ private final TestAppInstallSetup mTestAppInstallSetup;
+ private final Sleeper mSleeper;
+ private final TimeLimiter mTimeLimiter =
+ SimpleTimeLimiter.create(Executors.newCachedThreadPool());
public AppSetupPreparer() {
- this(null, new TestAppInstallSetup());
+ this(new TestAppInstallSetup(), Sleepers.DefaultSleeper.INSTANCE);
}
@VisibleForTesting
- public AppSetupPreparer(String packageName, TestAppInstallSetup appInstallSetup) {
- this.mPackageName = packageName;
- this.mAppInstallSetup = appInstallSetup;
+ public AppSetupPreparer(TestAppInstallSetup testAppInstallSetup, Sleeper sleeper) {
+ mTestAppInstallSetup = testAppInstallSetup;
+ mSleeper = sleeper;
}
/** {@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));
+ checkArgumentNonNegative(mMaxRetry, OPTION_MAX_RETRY);
+ checkArgumentNonNegative(
+ mExponentialBackoffMultiplierSeconds,
+ OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS);
+ checkArgumentNonNegative(mSetupOnceTimeoutMillis, OPTION_SETUP_TIMEOUT_MILLIS);
+
+ int runCount = 0;
+ while (true) {
+ TargetSetupError currentException;
+ try {
+ runCount++;
+
+ ITargetPreparer handler =
+ mTimeLimiter.newProxy(
+ new ITargetPreparer() {
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws DeviceNotAvailableException, BuildError,
+ TargetSetupError {
+ setUpOnce(device, buildInfo);
+ }
+ },
+ ITargetPreparer.class,
+ mSetupOnceTimeoutMillis,
+ TimeUnit.MILLISECONDS);
+ handler.setUp(device, buildInfo);
+
+ break;
+ } catch (TargetSetupError e) {
+ currentException = e;
+ } catch (UncheckedTimeoutException e) {
+ currentException = new TargetSetupError(e.getMessage(), e);
+ }
+
+ waitForDeviceAvailable(device);
+ if (runCount > mMaxRetry) {
+ throw currentException;
+ }
+ CLog.w("setUp failed: %s. Run count: %d. Retrying...", currentException, runCount);
+
+ try {
+ mSleeper.sleep(
+ Duration.ofSeconds(
+ (int) Math.pow(mExponentialBackoffMultiplierSeconds, runCount)));
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new TargetSetupError(e.getMessage(), e);
+ }
+ }
+ }
- mAppInstallSetup.setAltDir(packageDir);
+ private void setUpOnce(ITestDevice device, IBuildInfo buildInfo)
+ throws DeviceNotAvailableException, BuildError, TargetSetupError {
+ mTestAppInstallSetup.setAaptVersion(mAaptVersion);
- List<String> apkFilePaths;
try {
- apkFilePaths = listApkFilePaths(packageDir);
- } catch (IOException e) {
- throw new TargetSetupError(
- String.format("Failed to access files in %s.", packageDir), e);
+ OptionSetter setter = new OptionSetter(mTestAppInstallSetup);
+ setter.setOptionValue("incremental", String.valueOf(mIncrementalInstallation));
+ } catch (ConfigurationException e) {
+ throw new TargetSetupError(e.getMessage(), e);
}
- if (apkFilePaths.isEmpty()) {
- throw new TargetSetupError(
- String.format("Failed to find apk files in %s.", packageDir));
+ if (mPackageName != null) {
+ SystemPackageUninstaller.uninstallPackage(mPackageName, device);
}
- if (apkFilePaths.size() == 1) {
- mAppInstallSetup.addTestFileName(apkFilePaths.get(0));
- } else {
- mAppInstallSetup.addSplitApkFileNames(String.join(",", apkFilePaths));
+ for (File testFile : mTestFiles) {
+ mTestAppInstallSetup.addTestFile(testFile);
}
- mAppInstallSetup.setUp(device, buildInfo);
+ for (String installArg : mInstallArgs) {
+ mTestAppInstallSetup.addInstallArg(installArg);
+ }
+
+ mTestAppInstallSetup.setUp(device, buildInfo);
}
/** {@inheritDoc} */
@Override
public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
- mAppInstallSetup.tearDown(testInfo, e);
+ mTestAppInstallSetup.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());
+ private void waitForDeviceAvailable(ITestDevice device) throws DeviceNotAvailableException {
+ if (mWaitForDeviceAvailableSeconds < 0) {
+ return;
+ }
+
+ device.waitForDeviceAvailable(1000L * mWaitForDeviceAvailableSeconds);
+ }
+
+ private void checkArgumentNonNegative(long val, String name) {
+ checkArgument(val >= 0, "%s (%s) must not be negative", name, val);
+ }
+
+ @VisibleForTesting
+ interface Sleeper {
+ void sleep(Duration duration) throws InterruptedException;
+ }
+
+ static class Sleepers {
+ enum DefaultSleeper implements Sleeper {
+ INSTANCE;
+
+ @Override
+ public void sleep(Duration duration) throws InterruptedException {
+ Thread.sleep(duration.toMillis());
+ }
+ }
+
+ private Sleepers() {}
}
}
diff --git a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java
deleted file mode 100644
index 7e50f3f..0000000
--- a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiver.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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/targetprep/CheckGmsPreparer.java b/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java
new file mode 100644
index 0000000..8d40d04
--- /dev/null
+++ b/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java
@@ -0,0 +1,87 @@
+/*
+ * 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.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Checks and recover GMS on a device.
+ *
+ * <p>This preparer checks whether the GMS process is running during setUp and tearDown. If GMS is
+ * not running before the test, a reboot will be attempted to recover.
+ */
+public final class CheckGmsPreparer implements ITargetPreparer {
+
+ private static final long WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS = 1000 * 60;
+ @VisibleForTesting static final String CHECK_GMS_COMMAND = "pidof com.google.android.gms";
+ @VisibleForTesting static final String OPTION_ENABLE = "enable";
+
+ @Option(name = OPTION_ENABLE, description = "Enable GMS checks.")
+ protected boolean mEnable = false;
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(TestInformation testInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ if (!mEnable || isGmsRunning(testInfo)) {
+ return;
+ }
+
+ CLog.e("Did not detect a running GMS process, rebooting device to recover");
+ testInfo.getDevice().reboot();
+ testInfo.getDevice().waitForBootComplete(WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS);
+
+ if (!isGmsRunning(testInfo)) {
+ CLog.e("GMS process still not running, throwing error");
+ mEnable = false;
+ throw new TargetSetupError(
+ "GMS required but did not detect a running GMS process after device reboot");
+ }
+ }
+
+ private static boolean isGmsRunning(TestInformation testInfo)
+ throws DeviceNotAvailableException {
+ CommandResult res = testInfo.getDevice().executeShellV2Command(CHECK_GMS_COMMAND);
+ if (res.getExitCode() == 0) {
+ CLog.d("Detected a running GMS process with PID: %s", res.getStdout());
+ return true;
+ }
+
+ CLog.e(
+ "Check GMS command returned non zero exit code. Command: %s, Result: %s",
+ CHECK_GMS_COMMAND, res.toString());
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+ if (!mEnable || isGmsRunning(testInfo)) {
+ return;
+ }
+
+ CLog.e("Did not detect a running GMS process on tearDown");
+ }
+}
diff --git a/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java b/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
index fb34dbf..7a52735 100644
--- a/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
+++ b/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
@@ -59,6 +59,12 @@ import java.util.Set;
/** A test that verifies that a single app can be successfully launched. */
public class AppLaunchTest
implements IDeviceTest, IRemoteTest, IConfigurationReceiver, ITestFilterReceiver {
+ @VisibleForTesting static final String SCREENSHOT_AFTER_LAUNCH = "screenshot-after-launch";
+
+ @Option(
+ name = SCREENSHOT_AFTER_LAUNCH,
+ description = "Whether to take a screenshost after a package is launched.")
+ private boolean mScreenshotAfterLaunch;
@Option(name = "package-name", description = "Package name of testing app.")
private String mPackageName;
@@ -79,6 +85,9 @@ public class AppLaunchTest
@Option(name = "exclude-filter", description = "The exclude filter of the test type.")
protected Set<String> mExcludeFilters = new HashSet<>();
+ @Option(name = "dismiss-dialog", description = "Attempt to dismiss dialog from apps.")
+ protected boolean mDismissDialog = false;
+
@Option(
name = "app-launch-timeout-ms",
description = "Time to wait for app to launch in msecs.")
@@ -88,18 +97,22 @@ public class AppLaunchTest
"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 ARG_DISMISS_DIALOG = "ARG_DISMISS_DIALOG";
private static final String APP_LAUNCH_TIMEOUT_LABEL = "app_launch_timeout_ms";
private static final int LOGCAT_SIZE_BYTES = 20 * 1024 * 1024;
+ private static final int BASE_INSTRUMENTATION_TEST_TIMEOUT_MS = 10 * 1000;
private ITestDevice mDevice;
private LogcatReceiver mLogcat;
private IConfiguration mConfiguration;
- public AppLaunchTest() {}
+ public AppLaunchTest() {
+ this(null);
+ }
@VisibleForTesting
public AppLaunchTest(String packageName) {
- mPackageName = packageName;
+ this(packageName, 0);
}
@VisibleForTesting
@@ -113,17 +126,23 @@ public class AppLaunchTest
* 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(
+ InstrumentationTest instrumentationTest = new InstrumentationTest();
+
+ instrumentationTest.setPackageName(LAUNCH_TEST_PACKAGE);
+ instrumentationTest.setConfiguration(mConfiguration);
+ instrumentationTest.addInstrumentationArg(PACKAGE_TO_LAUNCH, packageBeingTested);
+ instrumentationTest.setRunnerName(LAUNCH_TEST_RUNNER);
+ instrumentationTest.setDevice(mDevice);
+ instrumentationTest.addInstrumentationArg(
APP_LAUNCH_TIMEOUT_LABEL, Integer.toString(mAppLaunchTimeoutMs));
+ instrumentationTest.addInstrumentationArg(
+ ARG_DISMISS_DIALOG, Boolean.toString(mDismissDialog));
- return instrTest;
+ int testTimeoutMs = BASE_INSTRUMENTATION_TEST_TIMEOUT_MS + mAppLaunchTimeoutMs * 2;
+ instrumentationTest.setShellTimeout(testTimeoutMs);
+ instrumentationTest.setTestTimeout(testTimeoutMs);
+
+ return instrumentationTest;
}
/*
@@ -188,7 +207,21 @@ public class AppLaunchTest
// Clear test result between retries.
launchPackage(testInfo, result);
if (result.status == CompatibilityTestResult.STATUS_SUCCESS) {
- return;
+ break;
+ }
+ }
+
+ if (mScreenshotAfterLaunch) {
+ try (InputStreamSource screenSource = mDevice.getScreenshot()) {
+ listener.testLog(
+ mPackageName + "_screenshot_" + mDevice.getSerialNumber(),
+ LogDataType.PNG,
+ screenSource);
+ } catch (DeviceNotAvailableException e) {
+ CLog.e(
+ "Device %s became unavailable while capturing screenshot, %s",
+ mDevice.getSerialNumber(), e.toString());
+ throw e;
}
}
} finally {
diff --git a/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java b/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java
new file mode 100644
index 0000000..82fddb8
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/config/AppRemoteFileResolver.java
@@ -0,0 +1,282 @@
+/*
+ * 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.csuite.config;
+
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.config.remote.IRemoteFileResolver;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * An implementation of {@code IRemoteFileResolver} for downloading Android apps.
+ *
+ * <p>The scheme supported by this resolver allows Trade Federation test configs to abstract the
+ * actual service used to download Android app APK files. Note that this is a 'meta' resolver that
+ * resolves abstract 'app://' URIs into a URI with a different scheme using a custom template. The
+ * actual downloading of this resolved URI is then delegated to another registered {@link
+ * IRemoteFileResolver} implementation. Variable placeholders in the URI template string are
+ * expanded with corresponding values.
+ *
+ * <h2>Syntax and usage</h2>
+ *
+ * <p>References to apps in TradeFed test configs must have the following syntax:
+ *
+ * <blockquote>
+ *
+ * <b>{@code app://}</b><i>package-name</i>
+ *
+ * </blockquote>
+ *
+ * where <i>package-name</i> is the name of the application package such as:
+ *
+ * <blockquote>
+ *
+ * <table cellpadding=0 cellspacing=0 summary="layout">
+ * <tr><td>{@code app://com.example.myapp}<td></tr>
+ * </table>
+ *
+ * </blockquote>
+ *
+ * App APK files are downloaded to a directory and must be used in contexts that can handle File
+ * objects pointing to directories.
+ *
+ * <h2>Configuration</h2>
+ *
+ * <p>The URI template to use is specified using the {@code dynamic-download-args} TradeFed
+ * command-line argument:
+ *
+ * <blockquote>
+ *
+ * <pre>
+ * --dynamic-download-args app:uri-template=file:///app_files/{package}
+ * </pre>
+ *
+ * </blockquote>
+ *
+ * <p>Where {package} expands to the actual package name being downloaded. Any illegal URI
+ * characters must also be properly escaped as expected by {@link java.net.URI}.
+ *
+ * <p><span style="font-weight: bold; padding-right: 1em">Usage Note:</span> The {@code
+ * --enable-module-dynamic-download} flag must be set to {@code true} when used in test suites.
+ *
+ * @see com.android.tradefed.config.Option
+ */
+@NotThreadSafe
+@OptionClass(alias = "app")
+public final class AppRemoteFileResolver implements IRemoteFileResolver {
+
+ private static final String URI_SCHEME = "app";
+ private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\w+)\\}");
+
+ @VisibleForTesting static final String URI_TEMPLATE_OPTION = "uri-template";
+
+ @Option(name = URI_TEMPLATE_OPTION)
+ private String mUriTemplate;
+
+ @Nullable private ITestDevice mPrimaryDevice;
+
+ @Override
+ public String getSupportedProtocol() {
+ return URI_SCHEME;
+ }
+
+ @Override
+ public void setPrimaryDevice(@Nullable ITestDevice primaryDevice) {
+ this.mPrimaryDevice = primaryDevice;
+ }
+
+ @Override
+ public File resolveRemoteFiles(File uriSchemeAndPathAsFile) throws BuildRetrievalError {
+ // Note that this method is not really supported or even called by the framework. We
+ // only override it to simplify automated null pointer testing.
+ return resolveRemoteFiles(uriSchemeAndPathAsFile, ImmutableMap.of());
+ }
+
+ @Override
+ public File resolveRemoteFiles(
+ File uriSchemeAndPathAsFile, Map<String, String> uriQueryAndExtraParameters)
+ throws BuildRetrievalError {
+ URI appUri = checkAppUri(toUri(uriSchemeAndPathAsFile));
+ Objects.requireNonNull(uriQueryAndExtraParameters);
+
+ // TODO(hzalek): Remove this and make the corresponding option mandatory once test configs
+ // are using app URIs.
+ if (mUriTemplate == null) {
+ CLog.w("Resolver is not properly configured, skipping resolution of URI (%s)", appUri);
+ return null;
+ }
+
+ Preconditions.checkState(
+ !mUriTemplate.isEmpty(),
+ String.format("%s=%s is empty", URI_TEMPLATE_OPTION, mUriTemplate));
+
+ String packageName = appUri.getAuthority();
+ String expanded = expandVars(mUriTemplate, ImmutableMap.of("package", packageName));
+
+ URI uri;
+ try {
+ uri = new URI(expanded);
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException(
+ String.format(
+ "URI template (%s) did not expand to a a valid URI (%s)",
+ URI_TEMPLATE_OPTION, mUriTemplate, expanded),
+ e);
+ }
+
+ if (URI_SCHEME.equals(uri.getScheme())) {
+ throw new BuildRetrievalError(
+ String.format(
+ "Providers must return URIs with a scheme different than '%s': %s > %s",
+ URI_SCHEME, appUri, uri));
+ }
+
+ return resolveUriToFile(packageName, uri, uriQueryAndExtraParameters);
+ }
+
+ private static URI toUri(File uriSchemeAndPathAsFile) {
+ try {
+ // TradeFed forces a URI into a File instance which is lossy and forces us to attempt
+ // restoring the original format here so we don't have to use regular expressions. Be
+ // warned that using getAbsolutePath() will incorrectly strip the scheme.
+ String path = uriSchemeAndPathAsFile.getPath();
+ // Restore the original URI form since the first two forward slashes in the URI string
+ // get normalized into one when stored as a file.
+ path = path.replaceFirst(":/", "://");
+ return new URI(path);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Could not parse provided URI", e);
+ }
+ }
+
+ private static URI checkAppUri(URI uri) {
+ String uriScheme = uri.getScheme();
+ if (!URI_SCHEME.equals(uriScheme)) {
+ throw new IllegalArgumentException(
+ String.format("Unsupported scheme (%s) in provided URI (%s)", uriScheme, uri));
+ }
+
+ // Note that the below code accesses the 'authority' component of the URI and not 'path'
+ // like the dynamic resolver implementation. The latter has to do so because the authority
+ // component is no longer defined once the '//' gets converted to a single '/'.
+ String packageName = uri.getAuthority();
+ if (Strings.isNullOrEmpty(packageName)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid package name (%s) in provided URI (%s)", packageName, uri));
+ }
+
+ if (!Strings.isNullOrEmpty(uri.getPath())) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Path component (%s) incorrectly specified in provided URI (%s); "
+ + "app URIs must be of the form 'app://com.example.app'",
+ uri.getPath(), uri));
+ }
+
+ return uri;
+ }
+
+ private static String expandVars(CharSequence template, Map<String, String> vars) {
+ StringBuilder sb = new StringBuilder();
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(template);
+ int position = 0;
+
+ while (matcher.find()) {
+ sb.append(template.subSequence(position, matcher.start(0)));
+
+ String varName = matcher.group(1);
+ String varValue = vars.get(varName);
+
+ if (varValue == null) {
+ throw new IllegalStateException(
+ String.format(
+ "URI template (%s) contains a placeholder for undefined var (%s)",
+ template, varName));
+ }
+
+ sb.append(varValue);
+ position = matcher.end(0);
+ }
+
+ sb.append(template.subSequence(position, template.length()));
+ String expanded = sb.toString();
+
+ CLog.i("Template (%s) expanded (%s) using vars (%s)", template, expanded, vars);
+ return expanded;
+ }
+
+ private File resolveUriToFile(String packageName, URI uri, Map<String, String> params)
+ throws BuildRetrievalError {
+ DynamicRemoteFileResolver resolver = new DynamicRemoteFileResolver();
+ resolver.setDevice(mPrimaryDevice);
+ resolver.addExtraArgs(params);
+
+ FileOptionSource optionSource = new FileOptionSource();
+ Stopwatch stopwatch = Stopwatch.createStarted();
+
+ try {
+ OptionSetter setter = new OptionSetter(optionSource);
+ setter.setOptionValue(FileOptionSource.OPTION_NAME, uri.toString());
+ setter.validateRemoteFilePath(resolver);
+ CLog.i("Resolution of files took %d ms", stopwatch.elapsed().toMillis());
+ } catch (BuildRetrievalError e) {
+ throw new BuildRetrievalError(
+ String.format("Could not resolve URI (%s) for package '%s'", uri, packageName),
+ e);
+ } catch (ConfigurationException impossible) {
+ throw new AssertionError(impossible);
+ }
+
+ if (!optionSource.file.exists()) {
+ CLog.w("URI (%s) resolved to non-existent local file (%s)", uri, optionSource.file);
+ } else {
+ CLog.i("URI (%s) resolved to local file (%s)", uri, optionSource.file);
+ }
+
+ return optionSource.file;
+ }
+
+ /** This is required to resolve URIs since the remote resolver only deals with options. */
+ private static final class FileOptionSource {
+ static final String OPTION_NAME = "file";
+
+ @Option(name = OPTION_NAME, mandatory = true)
+ public File file;
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java b/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java
new file mode 100644
index 0000000..0760085
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java
@@ -0,0 +1,257 @@
+/*
+ * 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.csuite.config;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.csuite.core.PackageNameProvider;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.testtype.IBuildReceiver;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.IShardableTest;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A tool for generating TradeFed suite modules during runtime.
+ *
+ * <p>This class generates module config files into TradeFed's test directory at runtime using a
+ * template. Since the content of the test directory relies on what is being generated in a test
+ * run, there can only be one instance executing at a given time.
+ *
+ * <p>The intention of this class is to generate test modules at the beginning of a test run and
+ * cleans up after all tests finish, which resembles a target preparer. However, a target preparer
+ * is executed after the sharding process has finished. The only way to make the generated modules
+ * available for sharding without making changes to TradeFed's core code is to disguise this module
+ * generator as an instance of IShardableTest and declare it separately in test plan config. This is
+ * hacky, and in the long term a TradeFed centered solution is desired. For more details, see
+ * go/sharding-hack-for-module-gen. Note that since the generate step is executed as a test instance
+ * and cleanup step is executed as a target preparer, there should be no saved states between
+ * generating and cleaning up module files.
+ *
+ * <p>This module generator collects package names from all PackageNameProvider objects specified in
+ * the test configs.
+ *
+ * <h2>Syntax and usage</h2>
+ *
+ * <p>References to package name providers in TradeFed test configs must have the following syntax:
+ *
+ * <blockquote>
+ *
+ * <b>&lt;object type="PACKAGE_NAME_PROVIDER" class="</b><i>provider_class_name</i><b>"/&gt;</b>
+ *
+ * </blockquote>
+ *
+ * where <i>provider_class_name</i> is the fully-qualified class name of an PackageNameProvider
+ * implementation class.
+ */
+public final class ModuleGenerator
+ implements IRemoteTest,
+ IShardableTest,
+ IBuildReceiver,
+ ITargetPreparer,
+ IConfigurationReceiver {
+
+ @VisibleForTesting static final String MODULE_FILE_EXTENSION = ".config";
+ @VisibleForTesting static final String OPTION_TEMPLATE = "template";
+ @VisibleForTesting static final String PACKAGE_NAME_PROVIDER = "PACKAGE_NAME_PROVIDER";
+ private static final String TEMPLATE_PACKAGE_PATTERN = "\\{package\\}";
+ private static final Collection<IRemoteTest> NOT_SPLITABLE = null;
+
+ @Option(
+ name = OPTION_TEMPLATE,
+ description = "Module config template resource path.",
+ importance = Importance.ALWAYS)
+ private String mTemplate;
+
+ private final TestDirectoryProvider mTestDirectoryProvider;
+ private final ResourceLoader mResourceLoader;
+ private final FileSystem mFileSystem;
+ private IBuildInfo mBuildInfo;
+ private IConfiguration mConfiguration;
+
+ @Override
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
+ public ModuleGenerator() {
+ this(FileSystems.getDefault());
+ }
+
+ private ModuleGenerator(FileSystem fileSystem) {
+ this(
+ fileSystem,
+ new CompatibilityTestDirectoryProvider(fileSystem),
+ new ClassResourceLoader());
+ }
+
+ @VisibleForTesting
+ ModuleGenerator(
+ FileSystem fileSystem,
+ TestDirectoryProvider testDirectoryProvider,
+ ResourceLoader resourceLoader) {
+ mFileSystem = fileSystem;
+ mTestDirectoryProvider = testDirectoryProvider;
+ mResourceLoader = resourceLoader;
+ }
+
+ @Override
+ public void run(final TestInformation testInfo, final ITestInvocationListener listener) {
+ // Intentionally left blank since this class is not really a test.
+ }
+
+ @Override
+ public void setUp(TestInformation testInfo) {
+ // Intentionally left blank.
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mBuildInfo = buildInfo;
+ }
+
+ /**
+ * Generates test modules. Note that the implementation of this method is not related to
+ * sharding in any way.
+ */
+ @Override
+ public Collection<IRemoteTest> split() {
+ try {
+ // Executes the generate step.
+ generateModules();
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to generate modules", e);
+ }
+
+ return NOT_SPLITABLE;
+ }
+
+ /** Cleans up generated test modules. */
+ @Override
+ public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+ // Gets build info from test info as when the class is executed as a ITargetPreparer
+ // preparer, it is not considered as a IBuildReceiver instance.
+ mBuildInfo = testInfo.getBuildInfo();
+
+ try {
+ // Executes the clean up step.
+ cleanUpModules();
+ } catch (IOException ioException) {
+ throw new UncheckedIOException("Failed to clean up generated modules", ioException);
+ }
+ }
+
+ private Set<String> getPackageNames() throws IOException {
+ Set<String> packages = new HashSet<>();
+ for (Object provider : mConfiguration.getConfigurationObjectList(PACKAGE_NAME_PROVIDER)) {
+ packages.addAll(((PackageNameProvider) provider).get());
+ }
+ return packages;
+ }
+
+ private void generateModules() throws IOException {
+ String templateContent = mResourceLoader.load(mTemplate);
+
+ for (String packageName : getPackageNames()) {
+ validatePackageName(packageName);
+ Files.write(
+ getModulePath(packageName),
+ templateContent.replaceAll(TEMPLATE_PACKAGE_PATTERN, packageName).getBytes());
+ }
+ }
+
+ private void cleanUpModules() throws IOException {
+ getPackageNames()
+ .forEach(
+ packageName -> {
+ try {
+ Files.delete(getModulePath(packageName));
+ } catch (IOException ioException) {
+ CLog.e(
+ "Failed to delete the generated module for package "
+ + packageName,
+ ioException);
+ }
+ });
+ }
+
+ private Path getModulePath(String packageName) throws IOException {
+ Path testsDir = mTestDirectoryProvider.get(mBuildInfo);
+ return testsDir.resolve(packageName + MODULE_FILE_EXTENSION);
+ }
+
+ private static void validatePackageName(String packageName) {
+ if (packageName.isEmpty() || packageName.matches(".*" + TEMPLATE_PACKAGE_PATTERN + ".*")) {
+ throw new IllegalArgumentException(
+ "Package name cannot be empty or contains package placeholder: "
+ + TEMPLATE_PACKAGE_PATTERN);
+ }
+ }
+
+ @VisibleForTesting
+ interface ResourceLoader {
+ String load(String resourceName) throws IOException;
+ }
+
+ private static final class ClassResourceLoader implements ResourceLoader {
+ @Override
+ public String load(String resourceName) throws IOException {
+ return Resources.toString(
+ getClass().getClassLoader().getResource(resourceName), StandardCharsets.UTF_8);
+ }
+ }
+
+ @VisibleForTesting
+ interface TestDirectoryProvider {
+ Path get(IBuildInfo buildInfo) throws IOException;
+ }
+
+ private static final class CompatibilityTestDirectoryProvider implements TestDirectoryProvider {
+ private final FileSystem mFileSystem;
+
+ private CompatibilityTestDirectoryProvider(FileSystem fileSystem) {
+ mFileSystem = fileSystem;
+ }
+
+ @Override
+ public Path get(IBuildInfo buildInfo) throws IOException {
+ return mFileSystem.getPath(
+ new CompatibilityBuildHelper(buildInfo).getTestsDir().getPath());
+ }
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java
new file mode 100644
index 0000000..33426ed
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** A package name provider that accepts package names via a command line option. */
+@OptionClass(alias = "command-line-package-name-provider")
+public final class CommandLinePackageNameProvider implements PackageNameProvider {
+ @VisibleForTesting static final String PACKAGE = "package";
+
+ @Option(name = PACKAGE, description = "App package names.")
+ private final Set<String> mPackages = new HashSet<>();
+
+ @Override
+ public Set<String> get() {
+ return mPackages;
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java
new file mode 100644
index 0000000..6f6af06
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import com.android.tradefed.config.Option;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/** A package name provider that accepts files that contains package names. */
+public final class FileBasedPackageNameProvider implements PackageNameProvider {
+ @VisibleForTesting static final String PACKAGES_FILE = "packages-file";
+ @VisibleForTesting static final String COMMENT_LINE_PREFIX = "#";
+
+ @Option(
+ name = PACKAGES_FILE,
+ description =
+ "File paths that contain package names separated by newline characters."
+ + " Comment lines are supported only if the lines start with double slash."
+ + " Trailing comments are not supported. Empty lines are ignored.")
+ private final Set<File> mPackagesFiles = new HashSet<>();
+
+ @Override
+ public Set<String> get() throws IOException {
+ Set<String> packages = new HashSet<>();
+ for (File packagesFile : mPackagesFiles) {
+ packages.addAll(
+ Files.readAllLines(packagesFile.toPath()).parallelStream()
+ .map(String::trim)
+ .filter(this::isPackageName)
+ .collect(Collectors.toSet()));
+ }
+ return packages;
+ }
+
+ private boolean isPackageName(String text) {
+ // Check the text is not an empty string and not a comment line.
+ return !text.isEmpty() && !text.startsWith(COMMENT_LINE_PREFIX);
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/PackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/PackageNameProvider.java
new file mode 100644
index 0000000..05709a1
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/PackageNameProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import java.io.IOException;
+import java.util.Set;
+
+/** Provides a list of package names. */
+public interface PackageNameProvider {
+ /**
+ * Returns a set of package names.
+ *
+ * @return the package names. An empty set is returned if no package names are to be provided.
+ * @throws IOException if failed to get package names.
+ */
+ Set<String> get() throws IOException;
+}
diff --git a/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java b/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java
new file mode 100644
index 0000000..4ed6efe
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java
@@ -0,0 +1,296 @@
+/*
+ * 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.csuite.core;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+/**
+ * Uninstalls a system app.
+ *
+ * <p>This utility class may not restore the uninstalled system app after test completes.
+ *
+ * <p>The class may disable dm verity on some devices, and it does not re-enable it after
+ * uninstalling a system app.
+ */
+public final class SystemPackageUninstaller {
+ @VisibleForTesting static final String OPTION_PACKAGE_NAME = "package-name";
+ static final String SYSPROP_DEV_BOOTCOMPLETE = "dev.bootcomplete";
+ static final String SYSPROP_SYS_BOOT_COMPLETED = "sys.boot_completed";
+ static final long WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS = 1000 * 60;
+ @VisibleForTesting static final int MAX_NUMBER_OF_UPDATES = 100;
+ @VisibleForTesting static final String PM_CHECK_COMMAND = "pm path android";
+
+ public static void uninstallPackage(String packageName, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ checkNotNull(packageName);
+
+ if (!isPackageManagerRunning(device)) {
+ CLog.w(
+ "Package manager is not available on the device."
+ + " Attempting to recover it by restarting the framework.");
+ runAsRoot(
+ device,
+ () -> {
+ stopFramework(device);
+ startFramework(device);
+ });
+ if (!isPackageManagerRunning(device)) {
+ throw new TargetSetupError("The package manager failed to start.");
+ }
+ }
+
+ if (!isPackageInstalled(packageName, device)) {
+ CLog.i("Package %s is not installed.", packageName);
+ return;
+ }
+
+ // Attempts to uninstall the package/updates from user partition.
+ // This method should be called before the other methods and requires
+ // the framework to be running.
+ removePackageUpdates(packageName, device);
+
+ if (!isPackageInstalled(packageName, device)) {
+ CLog.i("Package %s has been removed.", packageName);
+ return;
+ }
+
+ String packageInstallDirectory = getPackageInstallDirectory(packageName, device);
+ CLog.d("Install directory for package %s is %s", packageName, packageInstallDirectory);
+
+ if (!isPackagePathSystemApp(packageInstallDirectory)) {
+ CLog.w("%s is not a system app, skipping", packageName);
+ return;
+ }
+
+ CLog.i("Uninstalling system app %s", packageName);
+
+ runWithWritableFilesystem(
+ device,
+ () ->
+ runWithFrameworkOff(
+ device,
+ () -> {
+ removePackageInstallDirectory(packageInstallDirectory, device);
+ removePackageData(packageName, device);
+ }));
+ }
+
+ private interface PreparerTask {
+ void run() throws TargetSetupError, DeviceNotAvailableException;
+ }
+
+ private static void runWithFrameworkOff(ITestDevice device, PreparerTask action)
+ throws TargetSetupError, DeviceNotAvailableException {
+ stopFramework(device);
+
+ try {
+ action.run();
+ } finally {
+ startFramework(device);
+ }
+ }
+
+ private static void runWithWritableFilesystem(ITestDevice device, PreparerTask action)
+ throws TargetSetupError, DeviceNotAvailableException {
+ runAsRoot(
+ device,
+ () -> {
+ // TODO(yuexima): The remountSystemWritable method may internally disable dm
+ // verity on some devices. Consider restoring verity which would require a
+ // reboot.
+ device.remountSystemWritable();
+
+ try {
+ action.run();
+ } finally {
+ remountSystemReadOnly(device);
+ }
+ });
+ }
+
+ private static void runAsRoot(ITestDevice device, PreparerTask action)
+ throws TargetSetupError, DeviceNotAvailableException {
+ boolean disableRootAfterUninstall = false;
+
+ if (!device.isAdbRoot()) {
+ if (!device.enableAdbRoot()) {
+ throw new TargetSetupError("Failed to enable adb root");
+ }
+
+ disableRootAfterUninstall = true;
+ }
+
+ try {
+ action.run();
+ } finally {
+ if (disableRootAfterUninstall && !device.disableAdbRoot()) {
+ throw new TargetSetupError("Failed to disable adb root");
+ }
+ }
+ }
+
+ private static void stopFramework(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ // 'stop' is a blocking command.
+ executeShellCommandOrThrow(device, "stop", "Failed to stop framework");
+ // Set the boot complete flags to false. When the framework is started again, both flags
+ // will be set to true by the system upon the completion of restarting. This allows
+ // ITestDevice#waitForBootComplete to wait for framework start, and it only works
+ // when adb is rooted.
+ device.setProperty(SYSPROP_SYS_BOOT_COMPLETED, "0");
+ device.setProperty(SYSPROP_DEV_BOOTCOMPLETE, "0");
+ }
+
+ private static void startFramework(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ // 'start' is a non-blocking command.
+ executeShellCommandOrThrow(device, "start", "Failed to start framework");
+ // This wait only blocks if the boot completed flags are set to 0.
+ device.waitForBootComplete(WAIT_FOR_BOOT_COMPLETE_TIMEOUT_MILLIS);
+ }
+
+ private static CommandResult executeShellCommandOrThrow(
+ ITestDevice device, String command, String failureMessage)
+ throws TargetSetupError, DeviceNotAvailableException {
+ CommandResult commandResult = device.executeShellV2Command(command);
+
+ if (commandResult.getStatus() != CommandStatus.SUCCESS) {
+ throw new TargetSetupError(
+ String.format("%s; Command result: %s", failureMessage, commandResult));
+ }
+
+ return commandResult;
+ }
+
+ private static CommandResult executeShellCommandOrLog(
+ ITestDevice device, String command, String failureMessage)
+ throws DeviceNotAvailableException {
+ CommandResult commandResult = device.executeShellV2Command(command);
+ if (commandResult.getStatus() != CommandStatus.SUCCESS) {
+ CLog.e("%s. Command result: %s", failureMessage, commandResult);
+ }
+
+ return commandResult;
+ }
+
+ private static void remountSystemReadOnly(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ executeShellCommandOrThrow(
+ device,
+ "mount -o ro,remount /system",
+ "Failed to remount system partition as read only");
+ }
+
+ private static boolean isPackagePathSystemApp(String packagePath) {
+ return packagePath.startsWith("/system/") || packagePath.startsWith("/product/");
+ }
+
+ private static void removePackageInstallDirectory(
+ String packageInstallDirectory, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ CLog.i("Removing package install directory %s", packageInstallDirectory);
+ executeShellCommandOrThrow(
+ device,
+ String.format("rm -r %s", packageInstallDirectory),
+ String.format(
+ "Failed to remove system app package path %s", packageInstallDirectory));
+ }
+
+ private static void removePackageUpdates(String packageName, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ CLog.i("Removing package updates for %s", packageName);
+
+ // A system package may have update packages. If so, each `adb uninstall` call
+ // only uninstalls the latest update. To remove all update packages we can
+ // call uninstall repeatedly until the command fails.
+ for (int i = 0; i < MAX_NUMBER_OF_UPDATES; i++) {
+ String errMsg = device.uninstallPackage(packageName);
+ if (errMsg != null) {
+ CLog.d("Completed removing updates as the uninstall command returned: %s", errMsg);
+ return;
+ }
+ CLog.i("Removed an update package for %s", packageName);
+ }
+
+ throw new TargetSetupError("Too many updates were uninstalled. Something must be wrong.");
+ }
+
+ private static void removePackageData(String packageName, ITestDevice device)
+ throws DeviceNotAvailableException {
+ String dataPath = String.format("/data/data/%s", packageName);
+ CLog.i("Removing package data directory for %s", dataPath);
+ executeShellCommandOrLog(
+ device,
+ String.format("rm -r %s", dataPath),
+ String.format(
+ "Failed to remove system app data %s from %s", packageName, dataPath));
+ }
+
+ private static boolean isPackageManagerRunning(ITestDevice device)
+ throws DeviceNotAvailableException {
+ return device.executeShellV2Command(PM_CHECK_COMMAND).getStatus() == CommandStatus.SUCCESS;
+ }
+
+ private static boolean isPackageInstalled(String packageName, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ CommandResult commandResult =
+ executeShellCommandOrThrow(
+ device,
+ String.format("pm list packages %s", packageName),
+ "Failed to execute pm command");
+
+ if (commandResult.getStdout() == null) {
+ throw new TargetSetupError(
+ String.format(
+ "Failed to get pm command output: %s", commandResult.getStdout()));
+ }
+
+ return Arrays.asList(commandResult.getStdout().split("\\r?\\n"))
+ .contains(String.format("package:%s", packageName));
+ }
+
+ private static String getPackageInstallDirectory(String packageName, ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ CommandResult commandResult =
+ executeShellCommandOrThrow(
+ device,
+ String.format("pm path %s", packageName),
+ "Failed to execute pm command");
+
+ if (commandResult.getStdout() == null
+ || !commandResult.getStdout().startsWith("package:")) {
+ throw new TargetSetupError(
+ String.format(
+ "Failed to get pm path command output %s", commandResult.getStdout()));
+ }
+
+ String packageInstallPath = commandResult.getStdout().substring("package:".length());
+ return Paths.get(packageInstallPath).getParent().toString();
+ }
+}
diff --git a/harness/src/main/resources/META-INF/services/com.android.tradefed.config.remote.IRemoteFileResolver b/harness/src/main/resources/META-INF/services/com.android.tradefed.config.remote.IRemoteFileResolver
new file mode 100644
index 0000000..be67f9b
--- /dev/null
+++ b/harness/src/main/resources/META-INF/services/com.android.tradefed.config.remote.IRemoteFileResolver
@@ -0,0 +1 @@
+com.android.csuite.config.AppRemoteFileResolver
diff --git a/harness/src/main/resources/config/csuite-base.xml b/harness/src/main/resources/config/csuite-base.xml
index 6431154..03e418b 100644
--- a/harness/src/main/resources/config/csuite-base.xml
+++ b/harness/src/main/resources/config/csuite-base.xml
@@ -28,5 +28,12 @@
<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" />
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparer">
+ <option name="test-file-name" value="csuite-launch-instrumentation.apk"/>
+ </target_preparer>
+ <!-- Cleans generated module files after test -->
+ <target_preparer class="com.android.csuite.config.ModuleGenerator" />
+
+ <object type="PACKAGE_NAME_PROVIDER" class="com.android.csuite.core.CommandLinePackageNameProvider" />
+ <object type="PACKAGE_NAME_PROVIDER" class="com.android.csuite.core.FileBasedPackageNameProvider" />
</configuration>
diff --git a/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java b/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java
deleted file mode 100644
index 18f3368..0000000
--- a/harness/src/test/java/com/android/compatibility/AppCompatibilityTestTest.java
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * 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/targetprep/AppSetupPreparerConfigurationReceiverTest.java b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java
deleted file mode 100644
index e708a67..0000000
--- a/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerConfigurationReceiverTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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
index 7614f05..e2e4352 100644
--- a/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
+++ b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
@@ -15,125 +15,492 @@
*/
package com.android.compatibility.targetprep;
-import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.ArgsOptionParser;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
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 com.android.tradefed.util.AaptParser.AaptVersion;
+
+import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.internal.stubbing.answers.AnswersWithDelay;
+import org.mockito.stubbing.Answer;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Map;
@RunWith(JUnit4.class)
-public class AppSetupPreparerTest {
-
- private static final String OPTION_GCS_APK_DIR = "gcs-apk-dir";
- public static final ITestDevice NULL_DEVICE = null;
+public final class AppSetupPreparerTest {
+ private static final ITestDevice NULL_DEVICE = null;
+ private static final IBuildInfo NULL_BUILD_INFO = null;
+ private static final String TEST_PACKAGE_NAME = "test.package.name";
+ private static final Answer<Object> EMPTY_ANSWER = (i) -> 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_unresolvedAppUri_installs() throws Exception {
+ String appUri = "app://com.example.app";
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, appUri)
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+
+ verify(installer).addTestFile(new File(appUri));
+ }
+
+ @Test
+ public void tearDown_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ AppSetupPreparer preparer = new PreparerBuilder().setInstaller(installer).build();
+ TestInformation testInfo = TestInformation.newBuilder().build();
+
+ preparer.tearDown(testInfo, null);
+
+ verify(installer).tearDown(testInfo, null);
+ }
+
+ @Test
+ public void setUp_withinRetryLimit_doesNotThrowException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doThrow(new TargetSetupError("Still failing"))
+ .doNothing()
+ .when(installer)
+ .setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ }
+
+ @Test
+ public void setUp_exceedsRetryLimit_throwsException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doThrow(new TargetSetupError("Still failing"))
+ .doThrow(new TargetSetupError("Still failing"))
+ .doNothing()
+ .when(installer)
+ .setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_negativeTimeout_throwsException() throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "-1")
+ .build();
+
+ assertThrows(
+ IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_withinTimeout_doesNotThrowException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "1000")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ }
+
+ @Test
+ public void setUp_exceedsTimeout_throwsException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "5")
+ .build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_timesOutWithoutExceedingRetryLimit_doesNotThrowException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER))
+ .doNothing()
+ .when(installer)
+ .setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "5")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ }
+
+ @Test
+ public void setUp_timesOutAndExceedsRetryLimit_doesNotThrowException() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "5")
+ .build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_zeroMaxRetry_runsOnce() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doNothing().when(installer).setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "0")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+
+ verify(installer).setUp(any(), any());
+ }
+
+ @Test
+ public void setUp_positiveMaxRetryButNoException_runsOnlyOnce() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doNothing().when(installer).setUp(any(), any());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+
+ verify(installer).setUp(any(), any());
+ }
@Test
- public void setUp_gcsApkDirIsNull_throwsException()
- throws DeviceNotAvailableException, TargetSetupError {
- mBuildInfo.addBuildAttribute(OPTION_GCS_APK_DIR, null);
+ public void setUp_negativeMaxRetry_throwsException() throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder().setOption(AppSetupPreparer.OPTION_MAX_RETRY, "-1").build();
- assertThrows(NullPointerException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ assertThrows(
+ IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
}
@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());
+ public void setUp_deviceNotAvailableAndWaitEnabled_throwsDeviceNotAvailableException()
+ throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Connection reset by peer.")))
+ .setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "1")
+ .build();
+ ITestDevice device = createUnavailableDevice();
assertThrows(
- IllegalArgumentException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ DeviceNotAvailableException.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
}
@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());
+ public void setUp_deviceAvailableAndWaitEnabled_doesNotChangeException() throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Connection reset by peer.")))
+ .setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "1")
+ .build();
+ ITestDevice device = createAvailableDevice();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_deviceNotAvailableAndWaitDisabled_doesNotChangeException() throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Connection reset by peer.")))
+ .setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "-1")
+ .build();
+ ITestDevice device = createUnavailableDevice();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
+ }
+
+ @Test
+ public void setUp_negativeExponentialBackoffMultiplier_throwsIllegalArgumentException()
+ throws Exception {
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setOption(
+ AppSetupPreparer.OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS,
+ "-1")
+ .build();
assertThrows(
- IllegalArgumentException.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
}
@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());
+ public void setUp_testFileNameOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ ArgumentCaptor<File> captor = ArgumentCaptor.forClass(File.class);
+ doNothing().when(installer).addTestFile(captor.capture());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, "additional1.apk")
+ .setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, "additional2.apk")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
- assertThrows(TargetSetupError.class, () -> mPreparer.setUp(NULL_DEVICE, mBuildInfo));
+ assertThat(captor.getAllValues())
+ .containsAtLeast(new File("additional1.apk"), new File("additional2.apk"));
}
@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());
+ public void setUp_installArgOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
+ doNothing().when(installer).addInstallArg(captor.capture());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_INSTALL_ARG, "-arg1")
+ .setOption(AppSetupPreparer.OPTION_INSTALL_ARG, "-arg2")
+ .build();
- mPreparer.setUp(NULL_DEVICE, mBuildInfo);
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
- verify(mMockAppInstallSetup).setAltDir(packageDir);
- verify(mMockAppInstallSetup).addSplitApkFileNames("apk_name_2.apk,apk_name_1.apk");
- verify(mMockAppInstallSetup).setUp(any(), any());
+ assertThat(captor.getAllValues()).containsExactly("-arg1", "-arg2");
}
@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());
+ public void setUp_installIncrementalOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- mPreparer.setUp(NULL_DEVICE, mBuildInfo);
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_INCREMENTAL_INSTALL, "true")
+ .build();
- verify(mMockAppInstallSetup).setAltDir(packageDir);
- verify(mMockAppInstallSetup).addTestFileName("apk_name_1.apk");
- verify(mMockAppInstallSetup).setUp(any(), any());
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ String result = ArgsOptionParser.getOptionHelp(false, installer);
+ System.out.println(result);
+
+ assertThat(result).contains("incremental");
}
@Test
- public void tearDown() throws Exception {
- TestInformation testInfo = TestInformation.newBuilder().build();
+ public void setUp_aaptVersionOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ ArgumentCaptor<AaptVersion> captor = ArgumentCaptor.forClass(AaptVersion.class);
+ doNothing().when(installer).setAaptVersion(captor.capture());
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_AAPT_VERSION, "AAPT2")
+ .build();
+
+ preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+
+ assertThat(captor.getValue()).isEqualTo(AaptVersion.AAPT2);
+ }
+
+ @Test
+ public void setUp_zeroExponentialBackoffMultiplier_noSleepBetweenRetries() throws Exception {
+ FakeSleeper fakeSleeper = new FakeSleeper();
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setSleeper(fakeSleeper)
+ .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setOption(
+ AppSetupPreparer.OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS, "0")
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
+ .build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThat(fakeSleeper.getSleepHistory().get(0)).isEqualTo(Duration.ofSeconds(0));
+ }
- mPreparer.tearDown(testInfo, null);
+ @Test
+ public void setUp_positiveExponentialBackoffMultiplier_sleepsBetweenRetries() throws Exception {
+ FakeSleeper fakeSleeper = new FakeSleeper();
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setSleeper(fakeSleeper)
+ .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setOption(
+ AppSetupPreparer.OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS, "3")
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "3")
+ .build();
- verify(mMockAppInstallSetup, times(1)).tearDown(testInfo, null);
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThat(fakeSleeper.getSleepHistory().get(0)).isEqualTo(Duration.ofSeconds(3));
+ assertThat(fakeSleeper.getSleepHistory().get(1)).isEqualTo(Duration.ofSeconds(9));
+ assertThat(fakeSleeper.getSleepHistory().get(2)).isEqualTo(Duration.ofSeconds(27));
}
- private File createPackageFile(File parentDir, String packageName, String apkName)
- throws IOException {
- File packageDir =
- Files.createDirectories(Paths.get(parentDir.getAbsolutePath(), packageName))
- .toFile();
+ @Test
+ public void setUp_interruptedDuringBackoff_throwsException() throws Exception {
+ FakeSleeper fakeSleeper = new FakeInterruptedSleeper();
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setSleeper(fakeSleeper)
+ .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setOption(
+ AppSetupPreparer.OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS, "3")
+ .setOption(AppSetupPreparer.OPTION_MAX_RETRY, "3")
+ .build();
+
+ try {
+ assertThrows(
+ TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThat(Thread.currentThread().isInterrupted()).isTrue();
+ assertThat(fakeSleeper.getSleepHistory().size()).isEqualTo(1);
+ } finally {
+ // Clear interrupt to not interfere with future tests.
+ Thread.interrupted();
+ }
+ }
+
+ private TestAppInstallSetup mockInstallerThatThrows(Exception e) throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+ doThrow(e).when(installer).setUp(any(), any());
+ return installer;
+ }
+
+ private File createPackageFile(String packageName, String apkName) throws IOException {
+ Path packageDir =
+ Files.createDirectories(
+ Paths.get(tempFolder.newFolder("any").getAbsolutePath(), packageName));
+ Files.createFile(packageDir.resolve(apkName));
+
+ return packageDir.toFile();
+ }
+
+ private static ITestDevice createUnavailableDevice() throws Exception {
+ ITestDevice device = mock(ITestDevice.class);
+ when(device.getProperty(any())).thenReturn(null);
+ doThrow(new DeviceNotAvailableException("_", "serial"))
+ .when(device)
+ .waitForDeviceAvailable(anyLong());
+ return device;
+ }
+
+ private static ITestDevice createAvailableDevice() throws Exception {
+ ITestDevice device = mock(ITestDevice.class);
+ when(device.getProperty(any())).thenReturn("");
+ when(device.waitForDeviceShell(anyLong())).thenReturn(true);
+ doNothing().when(device).waitForDeviceAvailable(anyLong());
+
+ return device;
+ }
+
+ private static class FakeSleeper implements AppSetupPreparer.Sleeper {
+ private ArrayList<Duration> mSleepHistory = new ArrayList<>();
+
+ @Override
+ public void sleep(Duration duration) throws InterruptedException {
+ mSleepHistory.add(duration);
+ }
+
+ ArrayList<Duration> getSleepHistory() {
+ return mSleepHistory;
+ }
+ }
+
+ private static class FakeInterruptedSleeper extends FakeSleeper {
+ @Override
+ public void sleep(Duration duration) throws InterruptedException {
+ super.sleep(duration);
+ throw new InterruptedException("_");
+ }
+ }
+
+ private static final class PreparerBuilder {
+
+ private AppSetupPreparer.Sleeper mSleeper = new FakeSleeper();
+ private TestAppInstallSetup mInstaller = mock(TestAppInstallSetup.class);
+ private final ListMultimap<String, String> mOptions = ArrayListMultimap.create();
+
+ PreparerBuilder setSleeper(AppSetupPreparer.Sleeper sleeper) {
+ this.mSleeper = sleeper;
+ return this;
+ }
+
+ PreparerBuilder setInstaller(TestAppInstallSetup installer) {
+ this.mInstaller = installer;
+ return this;
+ }
+
+ PreparerBuilder setOption(String key, String value) {
+ mOptions.put(key, value);
+ return this;
+ }
+
+ AppSetupPreparer build() throws ConfigurationException {
+ AppSetupPreparer preparer = new AppSetupPreparer(mInstaller, mSleeper);
+ OptionSetter optionSetter = new OptionSetter(preparer);
+
+ for (Map.Entry<String, String> e : mOptions.entries()) {
+ optionSetter.setOptionValue(e.getKey(), e.getValue());
+ }
- return Files.createFile(Paths.get(packageDir.getAbsolutePath(), apkName)).toFile();
+ return preparer;
+ }
}
}
diff --git a/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java b/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java
new file mode 100644
index 0000000..09f2141
--- /dev/null
+++ b/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java
@@ -0,0 +1,289 @@
+/*
+ * 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.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.ddmlib.Log;
+import com.android.ddmlib.Log.ILogOutput;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import com.google.common.truth.Correspondence;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+@RunWith(JUnit4.class)
+public final class CheckGmsPreparerTest {
+
+ private CheckGmsPreparer mPreparer;
+ private LogCaptor mLogCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ mPreparer = new CheckGmsPreparer();
+ new OptionSetter(mPreparer).setOptionValue(CheckGmsPreparer.OPTION_ENABLE, "true");
+
+ mLogCaptor = new LogCaptor();
+ Log.addLogger(mLogCaptor);
+ }
+
+ @After
+ public void tearDown() {
+ Log.removeLogger(mLogCaptor);
+ }
+
+ @Test
+ public void setUp_checkDisabledAndGmsAbsent_doesNotReboot() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsent();
+ disablePreparer(mPreparer);
+
+ mPreparer.setUp(createTestInfo(device));
+
+ Mockito.verify(device, Mockito.never()).reboot();
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .doesNotContain("GMS");
+ }
+
+ @Test
+ public void tearDown_checkDisabledAndGmsAbsent_doesNotLog() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsent();
+ disablePreparer(mPreparer);
+
+ mPreparer.tearDown(createTestInfo(device), null);
+
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .doesNotContain("GMS");
+ }
+
+ @Test
+ public void tearDown_setUpThrows_doesNotCheck() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsent();
+ TestInformation testInfo = createTestInfo(device);
+ assertThrows(TargetSetupError.class, () -> mPreparer.setUp(testInfo));
+ mLogCaptor.reset();
+ Mockito.reset(device);
+ Mockito.when(device.executeShellV2Command(Mockito.any()))
+ .thenReturn(createFailedCommandResult());
+
+ mPreparer.tearDown(testInfo, null);
+
+ Mockito.verify(device, Mockito.never()).executeShellV2Command(Mockito.any());
+ }
+
+ @Test
+ public void tearDown_setUpRecoveredGms_checksGms() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsentAndRecoverable();
+ TestInformation testInfo = createTestInfo(device);
+ mPreparer.setUp(testInfo);
+ mLogCaptor.reset();
+ Mockito.reset(device);
+ Mockito.when(device.executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND))
+ .thenReturn(createSuccessfulCommandResult());
+
+ mPreparer.tearDown(testInfo, null);
+
+ Mockito.verify(device, Mockito.atLeast(1))
+ .executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND);
+ }
+
+ @Test
+ public void tearDown_setUpFoundGms_checksGms() throws Exception {
+ ITestDevice device = createDeviceWithGmsPresent();
+ TestInformation testInfo = createTestInfo(device);
+ mPreparer.setUp(testInfo);
+ Mockito.reset(device);
+ mLogCaptor.reset();
+ Mockito.when(device.executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND))
+ .thenReturn(createSuccessfulCommandResult());
+
+ mPreparer.tearDown(testInfo, null);
+
+ Mockito.verify(device, Mockito.atLeast(1))
+ .executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND);
+ }
+
+ @Test
+ public void setUp_gmsPresent_doesNotReboot() throws Exception {
+ ITestDevice device = createDeviceWithGmsPresent();
+
+ mPreparer.setUp(createTestInfo(device));
+
+ Mockito.verify(device, Mockito.never()).reboot();
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .doesNotContain("GMS");
+ }
+
+ @Test
+ public void setUp_gmsProcessRecoveredAfterReboot_doesNotThrow() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsentAndRecoverable();
+
+ mPreparer.setUp(createTestInfo(device));
+
+ Mockito.verify(device, Mockito.times(1)).reboot();
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .contains("GMS");
+ }
+
+ @Test
+ public void setUp_gmsProcessNotRecoveredAfterReboot_throwsException() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsent();
+
+ assertThrows(TargetSetupError.class, () -> mPreparer.setUp(createTestInfo(device)));
+ Mockito.verify(device, Mockito.times(1)).reboot();
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .contains("GMS");
+ }
+
+ @Test
+ public void tearDown_gmsProcessPresent_doesNotLog() throws Exception {
+ ITestDevice device = createDeviceWithGmsPresent();
+
+ mPreparer.tearDown(createTestInfo(device), null);
+
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .doesNotContain("GMS");
+ }
+
+ @Test
+ public void tearDown_gmsProcessAbsent_logsError() throws Exception {
+ ITestDevice device = createDeviceWithGmsAbsent();
+
+ mPreparer.tearDown(createTestInfo(device), null);
+
+ assertThat(mLogCaptor.getLogItems())
+ .comparingElementsUsing(createContainsErrorLogCorrespondence())
+ .contains("GMS");
+ }
+
+ private static void disablePreparer(CheckGmsPreparer preparer) throws Exception {
+ new OptionSetter(preparer).setOptionValue(CheckGmsPreparer.OPTION_ENABLE, "false");
+ }
+
+ private static Correspondence<LogItem, String> createContainsErrorLogCorrespondence() {
+ return Correspondence.from(
+ (LogItem actual, String expected) -> {
+ return actual.getLogLevel() == LogLevel.ERROR
+ && actual.getMessage().contains(expected);
+ },
+ "has an error log that contains");
+ }
+
+ private static ITestDevice createDeviceWithGmsAbsentAndRecoverable() throws Exception {
+ ITestDevice device = Mockito.mock(ITestDevice.class);
+ Mockito.doReturn(createFailedCommandResult())
+ .doReturn(createSuccessfulCommandResult())
+ .when(device)
+ .executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND);
+ return device;
+ }
+
+ private static ITestDevice createDeviceWithGmsPresent() throws Exception {
+ ITestDevice device = Mockito.mock(ITestDevice.class);
+ Mockito.when(device.executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND))
+ .thenReturn(createSuccessfulCommandResult());
+ return device;
+ }
+
+ private static ITestDevice createDeviceWithGmsAbsent() throws Exception {
+ ITestDevice device = Mockito.mock(ITestDevice.class);
+ Mockito.when(device.executeShellV2Command(CheckGmsPreparer.CHECK_GMS_COMMAND))
+ .thenReturn(createFailedCommandResult());
+ return device;
+ }
+
+ private static final class LogCaptor implements ILogOutput {
+ private ArrayList<LogItem> mLogItems = new ArrayList<>();
+
+ void reset() {
+ mLogItems.clear();
+ }
+
+ ArrayList<LogItem> getLogItems() {
+ return mLogItems;
+ }
+
+ @Override
+ public void printLog(LogLevel logLevel, String tag, String message) {
+ mLogItems.add(new LogItem(logLevel, tag, message));
+ }
+
+ @Override
+ public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
+ printLog(logLevel, tag, message);
+ }
+ }
+
+ private static final class LogItem {
+ private LogLevel mLogLevel;
+ private String mMessage;
+
+ LogLevel getLogLevel() {
+ return mLogLevel;
+ }
+
+ String getMessage() {
+ return mMessage;
+ }
+
+ LogItem(LogLevel logLevel, String tag, String message) {
+ mLogLevel = logLevel;
+ mMessage = message;
+ }
+ }
+
+ private static TestInformation createTestInfo(ITestDevice device) {
+ IInvocationContext context = new InvocationContext();
+ context.addAllocatedDevice("device1", device);
+ context.addDeviceBuildInfo("device1", new BuildInfo());
+ return TestInformation.newBuilder().setInvocationContext(context).build();
+ }
+
+ private static CommandResult createSuccessfulCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ return commandResult;
+ }
+
+ private static CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java b/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
index 4f9aa35..0c23cc3 100644
--- a/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
+++ b/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
@@ -15,16 +15,20 @@
*/
package com.android.compatibility.testtype;
+import com.android.tradefed.config.OptionSetter;
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.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
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;
@@ -39,10 +43,13 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.InOrder;
+import org.mockito.Mockito;
import java.util.HashMap;
import java.util.HashSet;
@@ -55,6 +62,7 @@ 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;
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void run_testFailed() throws DeviceNotAvailableException {
@@ -77,10 +85,28 @@ public final class AppLaunchTestTest {
}
@Test
+ public void run_takeScreenShot_savesToTestLog() throws Exception {
+ InstrumentationTest instrumentationTest = createPassingInstrumentationTest();
+ AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest);
+ new OptionSetter(appLaunchTest)
+ .setOptionValue(AppLaunchTest.SCREENSHOT_AFTER_LAUNCH, "true");
+ ITestDevice mMockDevice = mock(ITestDevice.class);
+ appLaunchTest.setDevice(mMockDevice);
+ InputStreamSource screenshotData = new FileInputStreamSource(tempFolder.newFile());
+ when(mMockDevice.getScreenshot()).thenReturn(screenshotData);
+ when(mMockDevice.getSerialNumber()).thenReturn("SERIAL");
+
+ appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
+
+ Mockito.verify(mMockListener, times(1))
+ .testLog(Mockito.contains("screenshot"), Mockito.any(), Mockito.eq(screenshotData));
+ }
+
+ @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));
+ .thenReturn(createSuccessfulCommandResult());
AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
@@ -92,7 +118,7 @@ public final class AppLaunchTestTest {
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));
+ .thenReturn(createFailedCommandResult());
AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
@@ -103,7 +129,7 @@ public final class AppLaunchTestTest {
@Test
public void run_testRetry_passedAfterTwoFailings() throws Exception {
InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(2);
- AppLaunchTest appLaunchTest = createLaunchTestWithRetry(instrumentationTest, 2);
+ AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest, 2);
appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
@@ -113,7 +139,7 @@ public final class AppLaunchTestTest {
@Test
public void run_testRetry_failedAfterThreeFailings() throws Exception {
InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(3);
- AppLaunchTest appLaunchTest = createLaunchTestWithRetry(instrumentationTest, 2);
+ AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest, 2);
appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
@@ -369,24 +395,10 @@ public final class AppLaunchTestTest {
}
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;
+ return createLaunchTestWithInstrumentation(instrumentation, 0);
}
- private AppLaunchTest createLaunchTestWithRetry(
+ private AppLaunchTest createLaunchTestWithInstrumentation(
InstrumentationTest instrumentation, int retryCount) {
AppLaunchTest appLaunchTest =
new AppLaunchTest(TEST_PACKAGE_NAME, retryCount) {
@@ -398,7 +410,7 @@ public final class AppLaunchTestTest {
@Override
protected CommandResult resetPackage() throws DeviceNotAvailableException {
- return new CommandResult(CommandStatus.SUCCESS);
+ return createSuccessfulCommandResult();
}
};
appLaunchTest.setDevice(mock(ITestDevice.class));
@@ -406,7 +418,7 @@ public final class AppLaunchTestTest {
}
private AppLaunchTest createLaunchTestWithMockDevice(ITestDevice device) {
- AppLaunchTest appLaunchTest = new AppLaunchTest(TEST_PACKAGE_NAME);
+ AppLaunchTest appLaunchTest = new AppLaunchTest(TEST_PACKAGE_NAME, 0);
appLaunchTest.setDevice(device);
return appLaunchTest;
}
@@ -430,4 +442,16 @@ public final class AppLaunchTestTest {
.testEnded(anyObject(), anyLong(), (Map<String, String>) any());
inOrder.verify(listener, times(1)).testRunEnded(anyLong(), (HashMap<String, Metric>) any());
}
+
+ private CommandResult createSuccessfulCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ return commandResult;
+ }
+
+ private CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ return commandResult;
+ }
}
diff --git a/harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java b/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
index b87402e..6eb1103 100644
--- a/harness/src/test/java/com/android/compatibility/CSuiteUnitTests.java
+++ b/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
@@ -13,11 +13,7 @@
* 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;
+package com.android.csuite;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -25,10 +21,16 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({
- AppCompatibilityTestTest.class,
- AppLaunchTestTest.class,
- AppSetupPreparerTest.class,
- AppSetupPreparerConfigurationReceiverTest.class,
+ com.android.compatibility.targetprep.AppSetupPreparerTest.class,
+ com.android.compatibility.targetprep.CheckGmsPreparerTest.class,
+ com.android.compatibility.testtype.AppLaunchTestTest.class,
+ com.android.csuite.config.AppRemoteFileResolverTest.class,
+ com.android.csuite.config.ModuleGeneratorTest.class,
+ com.android.csuite.core.CommandLinePackageNameProviderTest.class,
+ com.android.csuite.core.FileBasedPackageNameProviderTest.class,
+ com.android.csuite.core.SystemAppUninstallerTest.class,
+ com.android.csuite.testing.CorrespondencesTest.class,
+ com.android.csuite.testing.MoreAssertsTest.class,
})
public final class CSuiteUnitTests {
// Intentionally empty.
diff --git a/harness/src/test/java/com/android/csuite/config/AppRemoteFileResolverTest.java b/harness/src/test/java/com/android/csuite/config/AppRemoteFileResolverTest.java
new file mode 100644
index 0000000..01c6ecf
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/config/AppRemoteFileResolverTest.java
@@ -0,0 +1,279 @@
+/*
+ * 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.csuite.config;
+
+import static com.android.csuite.testing.Correspondences.instanceOf;
+import static com.android.csuite.testing.MoreAsserts.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.tradefed.build.BuildRetrievalError;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.config.remote.IRemoteFileResolver;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.testing.NullPointerTester;
+
+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.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ServiceLoader;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+@RunWith(JUnit4.class)
+public final class AppRemoteFileResolverTest {
+
+ private static final String PACKAGE_NAME = "com.example.app";
+ private static final File APP_URI_FILE = uriToFile("app://" + PACKAGE_NAME);
+ private static final ImmutableMap<String, String> EMPTY_PARAMS = ImmutableMap.of();
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ // Class sanity tests.
+
+ @Test
+ public void isServiceLoadable() throws Exception {
+ ClassLoader classLoader = classLoaderWithProviders(AppRemoteFileResolver.class.getName());
+
+ ServiceLoader<IRemoteFileResolver> serviceLoader =
+ ServiceLoader.load(IRemoteFileResolver.class, classLoader);
+
+ // Copy the list to provide better failure error messages since ServiceLoader's string
+ // representation is not very informative.
+ assertThat(ImmutableList.copyOf(serviceLoader))
+ .comparingElementsUsing(instanceOf())
+ .contains(AppRemoteFileResolver.class);
+ }
+
+ @Test
+ public void nullPointers() {
+ NullPointerTester tester = new NullPointerTester();
+ tester.setDefault(File.class, APP_URI_FILE);
+ tester.testAllPublicConstructors(AppRemoteFileResolver.class);
+ tester.testAllPublicInstanceMethods(new AppRemoteFileResolver());
+ }
+
+ // URI validation tests.
+
+ @Test
+ public void unsupportedUriScheme_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
+ String uri = "gs://" + PACKAGE_NAME;
+ File f = uriToFile(uri);
+
+ Throwable thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> resolver.resolveRemoteFiles(f, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains("(gs)");
+ assertThat(thrown).hasMessageThat().contains(uri);
+ }
+
+ @Test
+ public void opaqueUri_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
+ File uri = uriToFile("app:" + PACKAGE_NAME);
+
+ Throwable thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains("package name");
+ }
+
+ @Test
+ public void uriHasPathComponent_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithAnyTemplate();
+ File uri = uriToFile("app://" + PACKAGE_NAME + "/invalid");
+
+ Throwable thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains("invalid");
+ }
+
+ // Template validation and expansion tests.
+
+ @Test
+ public void templateNotSet_returnsNull() throws Exception {
+ AppRemoteFileResolver resolver = new AppRemoteFileResolver();
+
+ File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
+
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ public void emptyTemplate_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithTemplate("");
+
+ Throwable thrown =
+ assertThrows(
+ IllegalStateException.class,
+ () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION);
+ }
+
+ @Test
+ public void templateHasNoPlaceholders_returnsFileWithoutExpansion() throws Exception {
+ File expected = temporaryFolder.newFolder();
+ AppRemoteFileResolver resolver = newResolverWithTemplate(expected.toURI().toString());
+
+ File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void templateContainsPlaceholderForUndefinedVar_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithTemplate("file://{undefined}");
+
+ Throwable thrown =
+ assertThrows(
+ IllegalStateException.class,
+ () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains("undefined");
+ }
+
+ @Test
+ public void templateExpandsToInvalidUri_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithTemplate("file:\\{package}");
+
+ Throwable thrown =
+ assertThrows(
+ IllegalStateException.class,
+ () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION);
+ }
+
+ @Test
+ public void templateContainsPlaceholder_resolvesUriToFile() throws Exception {
+ File parent = temporaryFolder.newFolder();
+ File expected = new File(parent, PACKAGE_NAME);
+ String template = new File(parent, "{package}").toString();
+ AppRemoteFileResolver resolver = newResolverWithTemplate(template);
+ File uri = uriToFile("app://" + PACKAGE_NAME);
+
+ File actual = resolver.resolveRemoteFiles(uri, EMPTY_PARAMS);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void templateExpandsToAppUri_throwsException() throws Exception {
+ AppRemoteFileResolver resolver = newResolverWithTemplate("app://{package}");
+
+ Throwable thrown =
+ assertThrows(
+ BuildRetrievalError.class,
+ () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS));
+
+ assertThat(thrown).hasMessageThat().contains("'app'");
+ }
+
+ @Test
+ public void templateExpandsToUriWithUnsupportedScheme_returnsExpandedUri() throws Exception {
+ String uri = "unsupported://" + PACKAGE_NAME;
+ AppRemoteFileResolver resolver = newResolverWithTemplate(uri);
+ File expected = uriToFile(uri);
+
+ File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS);
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ // Utility classes and methods.
+
+ /**
+ * Constructs a File from a URI string using the same logic TradeFed uses since it's tricky and
+ * has some gotchas such as stripping slashes.
+ */
+ private static File uriToFile(String str) {
+ FileOptionSource optionSource = new FileOptionSource();
+
+ try {
+ OptionSetter setter = new OptionSetter(optionSource);
+ setter.setOptionValue(FileOptionSource.OPTION_NAME, str);
+ } catch (ConfigurationException e) {
+ throw new RuntimeException(e);
+ }
+
+ return optionSource.file;
+ }
+
+ private static final class FileOptionSource {
+ static final String OPTION_NAME = "file";
+
+ @Option(name = OPTION_NAME)
+ public File file;
+ }
+
+ private static AppRemoteFileResolver newResolverWithAnyTemplate()
+ throws ConfigurationException {
+ return newResolverWithTemplate("file:///tmp/{package}");
+ }
+
+ private static AppRemoteFileResolver newResolverWithTemplate(String uriTemplate)
+ throws ConfigurationException {
+ AppRemoteFileResolver resolver = new AppRemoteFileResolver();
+ OptionSetter setter = new OptionSetter(resolver);
+ setter.setOptionValue("app:" + AppRemoteFileResolver.URI_TEMPLATE_OPTION, uriTemplate);
+ return resolver;
+ }
+
+ private ClassLoader classLoaderWithProviders(String... lines) throws IOException {
+ String service = IRemoteFileResolver.class.getName();
+ File jar = temporaryFolder.newFile();
+
+ try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) {
+ JarEntry jarEntry = new JarEntry("META-INF/services/" + service);
+
+ out.putNextEntry(jarEntry);
+ PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, UTF_8));
+
+ for (String line : lines) {
+ writer.println(line);
+ }
+
+ writer.flush();
+ }
+
+ return new URLClassLoader(new URL[] {jar.toURI().toURL()});
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java b/harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java
new file mode 100644
index 0000000..a679828
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.csuite.config;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.csuite.core.PackageNameProvider;
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.TestInformation;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.jimfs.Jimfs;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StringSubject;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public final class ModuleGeneratorTest {
+ private static final String TEST_PACKAGE_NAME1 = "test.package.name1";
+ private static final String TEST_PACKAGE_NAME2 = "test.package.name2";
+ private static final String PACKAGE_PLACEHOLDER = "{package}";
+ private static final Exception NO_EXCEPTION = null;
+
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+
+ @Test
+ public void tearDown_packageNamesProvided_deletesGeneratedModules() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator1 =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .build();
+ generator1.split();
+ ModuleGenerator generator2 =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .build();
+
+ generator2.tearDown(createTestInfo(), NO_EXCEPTION);
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ @Test
+ public void tearDown_packageNamesNotProvided_doesNotThrowError() throws Exception {
+ ModuleGenerator generator = createGeneratorBuilder().setTestsDir(createTestsDir()).build();
+ generator.split();
+
+ generator.tearDown(createTestInfo(), NO_EXCEPTION);
+ }
+
+ @Test
+ public void split_packageNameIsEmptyString_throwsError() throws Exception {
+ ModuleGenerator generator = createGeneratorBuilder().addPackage("").build();
+
+ assertThrows(IllegalArgumentException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_packageNameContainsPlaceholder_throwsError() throws Exception {
+ ModuleGenerator generator =
+ createGeneratorBuilder().addPackage("a" + PACKAGE_PLACEHOLDER + "b").build();
+
+ assertThrows(IllegalArgumentException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_multiplePackageNameProviders_generateModulesForAll() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackageNameProvider(() -> ImmutableSet.of(TEST_PACKAGE_NAME1))
+ // Simulates package providers providing duplicated package names.
+ .addPackageNameProvider(() -> ImmutableSet.of(TEST_PACKAGE_NAME1))
+ .addPackageNameProvider(() -> ImmutableSet.of(TEST_PACKAGE_NAME2))
+ .build();
+
+ generator.split();
+
+ assertThatListDirectory(testsDir)
+ .containsExactly(
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME1),
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME2));
+ }
+
+ @Test
+ public void split_packageNameProviderThrowsException_throwsException() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackageNameProvider(
+ () -> {
+ throw new IOException();
+ })
+ .build();
+
+ assertThrows(UncheckedIOException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_packageNamesNotProvided_doesNotGenerate() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator = createGeneratorBuilder().setTestsDir(testsDir).build();
+
+ generator.split();
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ @Test
+ public void split_templateContainsPlaceholders_replacesPlaceholdersInOutput() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "hello placeholder%s%s world";
+ ModuleGenerator generator =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(
+ String.format(content, PACKAGE_PLACEHOLDER, PACKAGE_PLACEHOLDER))
+ .build();
+
+ generator.split();
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1)
+ .isEqualTo(String.format(content, TEST_PACKAGE_NAME1, TEST_PACKAGE_NAME1));
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2)
+ .isEqualTo(String.format(content, TEST_PACKAGE_NAME2, TEST_PACKAGE_NAME2));
+ }
+
+ @Test
+ public void split_templateDoesNotContainPlaceholder_outputsTemplateContent() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "no placeholder";
+ ModuleGenerator generator =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(content)
+ .build();
+
+ generator.split();
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1).isEqualTo(content);
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2).isEqualTo(content);
+ }
+
+ @Test
+ public void split_templateContentIsEmpty_outputsTemplateContent() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "";
+ ModuleGenerator generator =
+ createGeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(content)
+ .build();
+
+ generator.split();
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1).isEqualTo(content);
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2).isEqualTo(content);
+ }
+
+ private static StringSubject assertThatModuleConfigFileContent(
+ Path testsDir, String packageName) throws IOException {
+ return assertThat(
+ new String(Files.readAllBytes(getModuleConfigFile(testsDir, packageName))));
+ }
+
+ private static IterableSubject assertThatListDirectory(Path dir) throws IOException {
+ // Convert stream to list because com.google.common.truth.Truth8 is not available.
+ return assertThat(
+ Files.walk(dir)
+ .filter(p -> !p.equals(dir))
+ .collect(ImmutableList.toImmutableList()));
+ }
+
+ private static Path getModuleConfigFile(Path baseDir, String packageName) {
+ return baseDir.resolve(packageName + ".config");
+ }
+
+ private Path createTestsDir() throws IOException {
+ Path rootPath = mFileSystem.getPath("csuite");
+ Files.createDirectories(rootPath);
+ return Files.createTempDirectory(rootPath, "testDir");
+ }
+
+ private static TestInformation createTestInfo() {
+ IInvocationContext context = new InvocationContext();
+ context.addAllocatedDevice("device1", Mockito.mock(ITestDevice.class));
+ context.addDeviceBuildInfo("device1", new BuildInfo());
+ return TestInformation.newBuilder().setInvocationContext(context).build();
+ }
+
+ private GeneratorBuilder createGeneratorBuilder() throws IOException {
+ return new GeneratorBuilder()
+ .setFileSystem(mFileSystem)
+ .setTemplateContent(MODULE_TEMPLATE_CONTENT)
+ .setOption(ModuleGenerator.OPTION_TEMPLATE, "empty_path");
+ }
+
+ private static final class GeneratorBuilder {
+ private final ListMultimap<String, String> mOptions = ArrayListMultimap.create();
+ private final Set<String> mPackages = new HashSet<>();
+ private final List<PackageNameProvider> mPackageNameProviders = new ArrayList<>();
+ private Path mTestsDir;
+ private String mTemplateContent;
+ private FileSystem mFileSystem;
+
+ GeneratorBuilder addPackage(String packageName) {
+ mPackages.add(packageName);
+ return this;
+ }
+
+ GeneratorBuilder addPackageNameProvider(PackageNameProvider packageNameProvider) {
+ mPackageNameProviders.add(packageNameProvider);
+ return this;
+ }
+
+ GeneratorBuilder setFileSystem(FileSystem fileSystem) {
+ mFileSystem = fileSystem;
+ return this;
+ }
+
+ GeneratorBuilder setTemplateContent(String templateContent) {
+ mTemplateContent = templateContent;
+ return this;
+ }
+
+ GeneratorBuilder setTestsDir(Path testsDir) {
+ mTestsDir = testsDir;
+ return this;
+ }
+
+ GeneratorBuilder setOption(String key, String value) {
+ mOptions.put(key, value);
+ return this;
+ }
+
+ ModuleGenerator build() throws Exception {
+ ModuleGenerator generator =
+ new ModuleGenerator(
+ mFileSystem, buildInfo -> mTestsDir, resourcePath -> mTemplateContent);
+
+ OptionSetter optionSetter = new OptionSetter(generator);
+ for (Map.Entry<String, String> entry : mOptions.entries()) {
+ optionSetter.setOptionValue(entry.getKey(), entry.getValue());
+ }
+
+ List<PackageNameProvider> packageNameProviders = new ArrayList<>(mPackageNameProviders);
+ packageNameProviders.add(() -> mPackages);
+
+ IConfiguration configuration = new Configuration("name", "description");
+ configuration.setConfigurationObjectList(
+ ModuleGenerator.PACKAGE_NAME_PROVIDER, packageNameProviders);
+ generator.setConfiguration(configuration);
+
+ return generator;
+ }
+ }
+
+ private static final String MODULE_TEMPLATE_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<configuration description=\"description\">\n"
+ + " <option name=\"package-name\" value=\"{package}\"/>\n"
+ + " <target_generator class=\"some.generator.class\">\n"
+ + " <option name=\"test-file-name\" value=\"app://{package}\"/>\n"
+ + " </target_generator>\n"
+ + " <test class=\"some.test.class\"/>\n"
+ + "</configuration>";
+}
diff --git a/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java b/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java
new file mode 100644
index 0000000..41e6dc1
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.config.OptionSetter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public final class CommandLinePackageNameProviderTest {
+
+ @Test
+ public void get_packageNamesProvided_returnsPackageNames() throws Exception {
+ CommandLinePackageNameProvider provider = new CommandLinePackageNameProvider();
+ String package1 = "package.name1";
+ String package2 = "package.name2";
+ OptionSetter optionSetter = new OptionSetter(provider);
+ optionSetter.setOptionValue(CommandLinePackageNameProvider.PACKAGE, package1);
+ optionSetter.setOptionValue(CommandLinePackageNameProvider.PACKAGE, package2);
+
+ Set<String> packageNames = provider.get();
+
+ assertThat(packageNames).containsExactly(package1, package2);
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java b/harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java
new file mode 100644
index 0000000..ee429b2
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.csuite.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+
+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.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public final class FileBasedPackageNameProviderTest {
+ private static final String TEST_PACKAGE_NAME1 = "test.package.name1";
+ private static final String TEST_PACKAGE_NAME2 = "test.package.name2";
+ private static final String PACKAGE_PLACEHOLDER = "{package}";
+ private static final Exception NO_EXCEPTION = null;
+
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void get_fileNotSpecified_returnsEmptySet() throws Exception {
+ FileBasedPackageNameProvider provider = createProvider();
+
+ Set<String> packageNames = provider.get();
+
+ assertThat(packageNames).isEmpty();
+ }
+
+ @Test
+ public void get_multipleFileSpecified_returnsAllEntries() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ String packageName3 = "c";
+ String packageName4 = "d";
+ FileBasedPackageNameProvider provider =
+ createProvider(
+ createPackagesFile(packageName1 + "\n" + packageName2),
+ createPackagesFile(packageName3 + "\n" + packageName4));
+
+ Set<String> packageNames = provider.get();
+
+ assertThat(packageNames)
+ .containsExactly(packageName1, packageName2, packageName3, packageName4);
+ }
+
+ @Test
+ public void get_fileContainsEmptyLines_ignoresEmptyLines() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ FileBasedPackageNameProvider provider =
+ createProvider(createPackagesFile(packageName1 + "\n \n\n" + packageName2));
+
+ Set<String> packageNames = provider.get();
+
+ assertThat(packageNames).containsExactly(packageName1, packageName2);
+ }
+
+ @Test
+ public void get_fileContainsCommentLines_ignoresCommentLines() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ FileBasedPackageNameProvider provider =
+ createProvider(
+ createPackagesFile(
+ packageName1
+ + "\n"
+ + FileBasedPackageNameProvider.COMMENT_LINE_PREFIX
+ + " Some comments\n"
+ + packageName2));
+
+ Set<String> packageNames = provider.get();
+
+ assertThat(packageNames).containsExactly(packageName1, packageName2);
+ }
+
+ private FileBasedPackageNameProvider createProvider(Path... packagesFiles)
+ throws IOException, ConfigurationException {
+ FileBasedPackageNameProvider provider = new FileBasedPackageNameProvider();
+ OptionSetter optionSetter = new OptionSetter(provider);
+ for (Path packagesFile : packagesFiles) {
+ optionSetter.setOptionValue(
+ FileBasedPackageNameProvider.PACKAGES_FILE, packagesFile.toString());
+ }
+ return provider;
+ }
+
+ private Path createPackagesFile(String content) throws IOException {
+ Path tempFile = Files.createTempFile(tempFolder.getRoot().toPath(), "packages", ".txt");
+ Files.write(tempFile, content.getBytes());
+ return tempFile;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java b/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java
new file mode 100644
index 0000000..30b77db
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java
@@ -0,0 +1,425 @@
+/*
+ * 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.csuite.core;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+@RunWith(JUnit4.class)
+public final class SystemAppUninstallerTest {
+ private static final ITestDevice NULL_DEVICE = null;
+ private static final String TEST_PACKAGE_NAME = "test.package.name";
+ private static final String SYSTEM_APP_INSTALL_DIRECTORY = "/system/app";
+ private static final String CHECK_PACKAGE_INSTALLED_COMMAND_PREFIX = "pm list packages ";
+ private static final String GET_PACKAGE_INSTALL_PATH_COMMAND_PREFIX = "pm path ";
+ private static final String REMOVE_SYSTEM_APP_COMMAND_PREFIX =
+ "rm -r " + SYSTEM_APP_INSTALL_DIRECTORY;
+ private static final String REMOVE_APP_DATA_COMMAND_PREFIX = "rm -r /data/data";
+ private static final String MOUNT_COMMAND_PREFIX = "mount";
+
+ @Test
+ public void uninstallPackage_packageNameIsNull_throws() throws Exception {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ SystemPackageUninstaller.uninstallPackage(
+ null, createGoodDeviceWithAppNotInstalled()));
+ }
+
+ @Test
+ public void uninstallPackage_frameworkNotRunning_startsFrameworkOrThrows() throws Exception {
+ ITestDevice device = createGoodDeviceWithAppNotInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.eq(SystemPackageUninstaller.PM_CHECK_COMMAND)))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ Mockito.verify(device, Mockito.times(1)).executeShellV2Command(Mockito.eq("start"));
+ }
+
+ @Test
+ public void uninstallPackage_packageIsNotInstalled_doesNotRemove() throws Exception {
+ ITestDevice device = createGoodDeviceWithAppNotInstalled();
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(0)).executeShellV2Command(Mockito.startsWith("rm"));
+ }
+
+ @Test
+ public void uninstallPackage_differentPackageWithSameNamePrefixInstalled_doesNotRemove()
+ throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ // Mock the device as if the test package does not exist on device
+ CommandResult commandResult = createSuccessfulCommandResult();
+ commandResult.setStdout(String.format("package:%s_some_more_chars", TEST_PACKAGE_NAME));
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ CHECK_PACKAGE_INSTALLED_COMMAND_PREFIX)))
+ .thenReturn(commandResult);
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(0)).executeShellV2Command(Mockito.startsWith("rm"));
+ }
+
+ @Test
+ public void uninstallPackage_checkPackageInstalledCommandFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ CHECK_PACKAGE_INSTALLED_COMMAND_PREFIX)))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_getInstallDirectoryCommandFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ GET_PACKAGE_INSTALL_PATH_COMMAND_PREFIX)))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_packageIsNotSystemApp_doesNotRemove() throws Exception {
+ ITestDevice device = createGoodDeviceWithUserAppInstalled();
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(0)).executeShellV2Command(Mockito.startsWith("rm"));
+ }
+
+ @Test
+ public void uninstallPackage_adbAlreadyRooted_doesNotRootAgain() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.isAdbRoot()).thenReturn(true);
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(0)).enableAdbRoot();
+ }
+
+ @Test
+ public void uninstallPackage_adbNotAlreadyRooted_rootAdbAndThenUnroot() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.isAdbRoot()).thenReturn(false);
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(1)).enableAdbRoot();
+ Mockito.verify(device, Mockito.times(1)).disableAdbRoot();
+ }
+
+ @Test
+ public void uninstallPackage_adbRootCommandFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.enableAdbRoot()).thenThrow(new DeviceNotAvailableException());
+
+ assertThrows(
+ DeviceNotAvailableException.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_adbRootFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.enableAdbRoot()).thenReturn(false);
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_adbDisableRootCommandFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.disableAdbRoot()).thenThrow(new DeviceNotAvailableException());
+
+ assertThrows(
+ DeviceNotAvailableException.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_adbDisableRootFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(device.disableAdbRoot()).thenReturn(false);
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_adbRemountFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.doThrow(new DeviceNotAvailableException()).when(device).remountSystemWritable();
+
+ assertThrows(
+ DeviceNotAvailableException.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_adbRemounted_mountReadOnlyAfterwards() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.doNothing().when(device).remountSystemWritable();
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(1)).remountSystemWritable();
+ Mockito.verify(device, Mockito.times(1))
+ .executeShellV2Command(Mockito.startsWith(MOUNT_COMMAND_PREFIX));
+ }
+
+ @Test
+ public void uninstallPackage_mountReadOnlyFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(MOUNT_COMMAND_PREFIX)))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_removePackageInstallDirectoryFailed_throws() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(REMOVE_SYSTEM_APP_COMMAND_PREFIX)))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ @Test
+ public void uninstallPackage_removePackageDataDirectoryFailed_doesNotThrow() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(REMOVE_APP_DATA_COMMAND_PREFIX)))
+ .thenReturn(createFailedCommandResult());
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+ }
+
+ @Test
+ public void uninstallPackage_packageIsSystemApp_appRemoved() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(1))
+ .executeShellV2Command(Mockito.startsWith(REMOVE_SYSTEM_APP_COMMAND_PREFIX));
+ Mockito.verify(device, Mockito.times(1))
+ .executeShellV2Command(Mockito.startsWith(REMOVE_APP_DATA_COMMAND_PREFIX));
+ }
+
+ @Test
+ public void uninstallPackage_noUpdatePackagePresent_appRemoved() throws Exception {
+ int numberOfUpdates = 0;
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled(numberOfUpdates);
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(numberOfUpdates + 1))
+ .uninstallPackage(TEST_PACKAGE_NAME);
+ Mockito.verify(device, Mockito.times(1))
+ .executeShellV2Command(Mockito.startsWith(REMOVE_SYSTEM_APP_COMMAND_PREFIX));
+ }
+
+ @Test
+ public void uninstallPackage_someUpdatePackagesPresent_appRemoved() throws Exception {
+ int numberOfUpdates = 2;
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled(numberOfUpdates);
+
+ SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device);
+
+ Mockito.verify(device, Mockito.times(numberOfUpdates + 1))
+ .uninstallPackage(TEST_PACKAGE_NAME);
+ Mockito.verify(device, Mockito.times(1))
+ .executeShellV2Command(Mockito.startsWith(REMOVE_SYSTEM_APP_COMMAND_PREFIX));
+ }
+
+ @Test
+ public void uninstallPackage_tooManyUpdatePackagesPresent_throwsException() throws Exception {
+ ITestDevice device =
+ createGoodDeviceWithSystemAppInstalled(
+ SystemPackageUninstaller.MAX_NUMBER_OF_UPDATES + 1);
+
+ assertThrows(
+ TargetSetupError.class,
+ () -> SystemPackageUninstaller.uninstallPackage(TEST_PACKAGE_NAME, device));
+ }
+
+ private ITestDevice createGoodDeviceWithUserAppInstalled() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+
+ CommandResult commandResult = createSuccessfulCommandResult();
+ commandResult.setStdout(
+ String.format("package:/data/app/%s/%s.apk", TEST_PACKAGE_NAME, TEST_PACKAGE_NAME));
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ GET_PACKAGE_INSTALL_PATH_COMMAND_PREFIX)))
+ .thenReturn(commandResult);
+
+ return device;
+ }
+
+ private ITestDevice createGoodDeviceWithAppNotInstalled() throws Exception {
+ ITestDevice device = createGoodDeviceWithSystemAppInstalled();
+ CommandResult commandResult = createSuccessfulCommandResult();
+ commandResult.setStdout("");
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ CHECK_PACKAGE_INSTALLED_COMMAND_PREFIX)))
+ .thenReturn(commandResult);
+ return device;
+ }
+
+ private ITestDevice createGoodDeviceWithSystemAppInstalled() throws Exception {
+ return createGoodDeviceWithSystemAppInstalled(1);
+ }
+
+ private ITestDevice createGoodDeviceWithSystemAppInstalled(int numberOfUpdatesInstalled)
+ throws Exception {
+ ITestDevice device = Mockito.mock(ITestDevice.class);
+ CommandResult commandResult;
+
+ // Is framework running
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.eq(SystemPackageUninstaller.PM_CHECK_COMMAND)))
+ .thenReturn(createSuccessfulCommandResult());
+
+ // Uninstall updates
+ String uninstallFailureMessage = "Failure [DELETE_FAILED_INTERNAL_ERROR]";
+ if (numberOfUpdatesInstalled == 0) {
+ Mockito.when(device.uninstallPackage(TEST_PACKAGE_NAME))
+ .thenReturn(uninstallFailureMessage);
+ } else {
+ String[] uninstallResults = new String[numberOfUpdatesInstalled];
+ uninstallResults[numberOfUpdatesInstalled - 1] = uninstallFailureMessage;
+ Mockito.when(device.uninstallPackage(TEST_PACKAGE_NAME))
+ .thenReturn(null, uninstallResults);
+ }
+
+ // List package
+ commandResult = createSuccessfulCommandResult();
+ commandResult.setStdout("package:" + TEST_PACKAGE_NAME);
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ CHECK_PACKAGE_INSTALLED_COMMAND_PREFIX)))
+ .thenReturn(commandResult);
+
+ // Get package path
+ commandResult = createSuccessfulCommandResult();
+ commandResult.setStdout(
+ String.format(
+ "package:%s/%s/%s.apk",
+ SYSTEM_APP_INSTALL_DIRECTORY, TEST_PACKAGE_NAME, TEST_PACKAGE_NAME));
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(
+ GET_PACKAGE_INSTALL_PATH_COMMAND_PREFIX)))
+ .thenReturn(commandResult);
+
+ // Adb root
+ Mockito.when(device.isAdbRoot()).thenReturn(false);
+ Mockito.when(device.enableAdbRoot()).thenReturn(true);
+
+ // Adb remount
+ Mockito.doNothing().when(device).remountSystemWritable();
+
+ // Remove package install directory
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(REMOVE_SYSTEM_APP_COMMAND_PREFIX)))
+ .thenReturn(createSuccessfulCommandResult());
+
+ // Remove package data directory
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(REMOVE_APP_DATA_COMMAND_PREFIX)))
+ .thenReturn(createSuccessfulCommandResult());
+
+ // Restart framework
+ Mockito.when(device.executeShellV2Command(ArgumentMatchers.eq("start")))
+ .thenReturn(createSuccessfulCommandResult());
+ Mockito.when(device.executeShellV2Command(ArgumentMatchers.eq("stop")))
+ .thenReturn(createSuccessfulCommandResult());
+
+ // Disable adb root
+ Mockito.when(device.disableAdbRoot()).thenReturn(true);
+
+ // Remount read only
+ Mockito.when(
+ device.executeShellV2Command(
+ ArgumentMatchers.startsWith(MOUNT_COMMAND_PREFIX)))
+ .thenReturn(createSuccessfulCommandResult());
+
+ return device;
+ }
+
+ private static CommandResult createSuccessfulCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ return commandResult;
+ }
+
+ private static CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/testing/Correspondences.java b/harness/src/test/java/com/android/csuite/testing/Correspondences.java
new file mode 100644
index 0000000..c388e23
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/testing/Correspondences.java
@@ -0,0 +1,47 @@
+/*
+ * 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.csuite.testing;
+
+import com.google.common.truth.Correspondence;
+
+/**
+ * Useful implementations of {@link Correspondence}.
+ *
+ * <p>Correspondences are used with {@code IterableSubject.comparingElementsUsing(Correspondence)}
+ * and allow for making assertions on elements besides simple equality. See {@link Correspondence}
+ * for more information.
+ */
+// TODO(hzalek): Move this into a dedicated testing library.
+public final class Correspondences {
+
+ private static final Correspondence<Object, Class<?>> INSTANCE_OF =
+ Correspondence.from(
+ (Object obj, Class<?> clazz) -> {
+ return clazz.isInstance(obj);
+ },
+ "is an instance of");
+
+ /**
+ * Returns a {@link Correspondence} that determines whether elements are instances of the class
+ * they are compared to.
+ */
+ public static Correspondence<Object, Class<?>> instanceOf() {
+ return INSTANCE_OF;
+ }
+
+ private Correspondences() {}
+}
diff --git a/harness/src/test/java/com/android/csuite/testing/CorrespondencesTest.java b/harness/src/test/java/com/android/csuite/testing/CorrespondencesTest.java
new file mode 100644
index 0000000..5a4dbcf
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/testing/CorrespondencesTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.csuite.testing;
+
+import static com.android.csuite.testing.Correspondences.instanceOf;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class CorrespondencesTest {
+
+ @Test(expected = AssertionError.class)
+ public void instanceOf_comparedToDifferentClass_fails() {
+ assertThat(stringList()).comparingElementsUsing(instanceOf()).contains(Integer.class);
+ }
+
+ @Test
+ public void instanceOf_comparedToSuperType_succeeds() {
+ assertThat(stringList()).comparingElementsUsing(instanceOf()).contains(Object.class);
+ }
+
+ @Test
+ public void instanceOf_comparedToSameClass_succeeds() {
+ assertThat(stringList()).comparingElementsUsing(instanceOf()).contains(String.class);
+ }
+
+ private static ImmutableList<String> stringList() {
+ return ImmutableList.of("A", "B", "C");
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java b/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java
new file mode 100644
index 0000000..306dad4
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java
@@ -0,0 +1,59 @@
+/*
+ * 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.csuite.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/** An additional set of assertion methods useful for writing tests. */
+// TODO(hzalek): Move this into a dedicated testing library.
+public final class MoreAsserts {
+
+ /**
+ * Facilitates the use of assertThrows from Java 8 by allowing method references to void methods
+ * that declare checked exceptions and is not meant to be directly implemented.
+ */
+ public interface ThrowingRunnable {
+ void run() throws Exception;
+ }
+
+ /**
+ * Asserts that {@code runnable} throws an exception of type {@code expectedThrowable} when
+ * executed. If it does, the exception object is returned. Otherwise, if it does not throw an
+ * exception or does but of the unexpected type, an {@link AssertionError} is thrown.
+ *
+ * <p>This is mostly intended as a drop-in replacement of assertThrows that is only available in
+ * JUnit 4.13 and later.
+ *
+ * @param message the identifying message for the {@link AssertionError}
+ * @param expectedThrowable the expected type of the exception
+ * @param runnable a function that is expected to throw an exception when executed
+ * @return the exception thrown by {@code runnable}
+ */
+ public static <T extends Throwable> T assertThrows(
+ Class<T> expectedClass, ThrowingRunnable runnable) {
+ try {
+ runnable.run();
+ } catch (Throwable e) {
+ assertThat(e).isInstanceOf(expectedClass);
+ return expectedClass.cast(e);
+ }
+ throw new AssertionError(
+ "Did not throw any when expected instance of: " + expectedClass.getName());
+ }
+
+ private MoreAsserts() {}
+}
diff --git a/harness/src/test/java/com/android/csuite/testing/MoreAssertsTest.java b/harness/src/test/java/com/android/csuite/testing/MoreAssertsTest.java
new file mode 100644
index 0000000..e0a968f
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/testing/MoreAssertsTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.csuite.testing;
+
+import static com.android.csuite.testing.MoreAsserts.assertThrows;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class MoreAssertsTest {
+
+ @Test(expected = AssertionError.class)
+ public void assertThrows_noExceptionThrown_fails() {
+ assertThrows(
+ Throwable.class,
+ () -> {
+ /* This comment works around a presubmit hook warning that complains about a
+ * missing whitespace after the '{' which if added results in another warning
+ * about the file being badly formatted. */
+ });
+ }
+
+ @Test(expected = AssertionError.class)
+ public void assertThrows_differentExceptionTypeThrown_fails() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ throw new IllegalStateException();
+ });
+ }
+
+ @Test(expected = AssertionError.class)
+ public void assertThrows_superTypeOfExpectedExceptionTypeThrown_fails() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ throw new RuntimeException();
+ });
+ }
+
+ @Test
+ public void assertThrows_expectedExceptionTypeThrown_returnsSameObject() {
+ IllegalArgumentException expected = new IllegalArgumentException();
+
+ IllegalArgumentException actual =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ throw expected;
+ });
+
+ assertThat(actual).isSameInstanceAs(expected);
+ }
+
+ @Test
+ public void assertThrows_subTypeOfExpectedExceptionTypeThrown_returnsSameObject() {
+ IllegalArgumentException expected = new IllegalArgumentException();
+
+ RuntimeException actual =
+ assertThrows(
+ RuntimeException.class,
+ () -> {
+ throw expected;
+ });
+
+ assertThat(actual).isSameInstanceAs(expected);
+ }
+}
diff --git a/instrumentation/launch/Android.bp b/instrumentation/launch/Android.bp
index 22255f6..1b45c1a 100644
--- a/instrumentation/launch/Android.bp
+++ b/instrumentation/launch/Android.bp
@@ -12,11 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-android_test {
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
name: "csuite-launch-instrumentation",
static_libs: ["androidx.test.rules"],
// Include all test java files.
srcs: ["src/**/*.java"],
+ // The value of min sdk version chosen here is the oldest sdk version we
+ // have tested. Lower version numbers should also work.
+ min_sdk_version: "29",
platform_apis: true,
manifest: "src/main/AndroidManifest.xml",
test_suites: [
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
index 5918a62..870076c 100644
--- a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
+++ b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
@@ -34,10 +34,13 @@ import android.os.DropBoxManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
+import android.view.KeyEvent;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.Preconditions;
+
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -51,6 +54,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.IntStream;
/** Application Compatibility Test that launches an application and detects crashes. */
@RunWith(AndroidJUnit4.class)
@@ -59,15 +63,14 @@ 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 String ARG_DISMISS_DIALOG = "ARG_DISMISS_DIALOG";
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;
+ private static final int DELAY_AFTER_KEYEVENT_MILLIS = 500;
// 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;
@@ -113,10 +116,6 @@ public final class AppCompatibility {
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
@@ -141,54 +140,59 @@ public final class AppCompatibility {
@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;
+ Preconditions.checkStringNotEmpty(
+ packageName,
+ String.format(
+ "Missing argument, use %s to specify the package to launch",
+ PACKAGE_TO_LAUNCH));
+
+ 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);
+
+ if (mArgs.getString(ARG_DISMISS_DIALOG, "false").equals("true")) {
+ // Attempt to dismiss any dialogs which some apps display to 'gracefully' handle
+ // errors. The dialog prevents the app from crashing thereby hiding issues. The
+ // first key event is to select a default button on the error dialog if any while
+ // the second event pushes the button.
+ IntStream.range(0, 2)
+ .forEach(i -> mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_ENTER));
+ // Give the app process enough time to terminate after dismissing the error.
+ Thread.sleep(DELAY_AFTER_KEYEVENT_MILLIS);
+ }
+
+ 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));
}
- 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();
+ if (errors.size() > MAX_NUM_CRASH_SNIPPET) {
+ message.append(
+ String.format(
+ "\n... %d more errors omitted ...",
+ errors.size() - MAX_NUM_CRASH_SNIPPET));
}
- } else {
- Log.d(
- TAG,
- "Missing argument, use "
- + PACKAGE_TO_LAUNCH
- + " to specify the package to launch");
+ 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));
}
/**
@@ -243,19 +247,6 @@ public final class AppCompatibility {
}
}
- 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;
diff --git a/integration_tests/Android.bp b/integration_tests/Android.bp
new file mode 100644
index 0000000..84eaa2e
--- /dev/null
+++ b/integration_tests/Android.bp
@@ -0,0 +1,116 @@
+// 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.
+
+// The below module creates a standalone zip that end-to-end tests can depend
+// on for running the suite. This is a workaround since we can't use csuite.zip
+// which is defined in an external Makefile that Soong can't depend on.
+//
+// Besides listing jars we know the launcher script depends on which is
+// brittle, this is a hack for several reasons. First, we're listing our
+// dependencies in the tools attribute when we should be using the 'srcs'
+// attribute. Second, we're accessing jars using a path relative to a known
+// artifact location instead of using the Soong 'location' feature.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_genrule_host {
+ name: "csuite_standalone_zip",
+ cmd: "ANDROID_CSUITE=$(genDir)/android-csuite && " +
+ "CSUITE_TOOLS=$${ANDROID_CSUITE}/tools && " +
+ "CSUITE_TESTCASES=$${ANDROID_CSUITE}/testcases && " +
+ "ANDROID_HOST_OUT=$$(dirname $(location :csuite-tradefed))/.. && " +
+ "rm -rf $${CSUITE_TOOLS} && mkdir -p $${CSUITE_TOOLS} && " +
+ "rm -rf $${CSUITE_TESTCASES} && mkdir -p $${CSUITE_TESTCASES} && " +
+ "cp $(location :csuite-tradefed) $${CSUITE_TOOLS} && " +
+ "cp $${ANDROID_HOST_OUT}/framework/csuite-tradefed.jar $${CSUITE_TOOLS} && " +
+ "cp $(location :tradefed) $${CSUITE_TOOLS} && " +
+ "cp $(location :compatibility-host-util) $${CSUITE_TOOLS} && " +
+ // We skip copying the csuite-tradefed-tests jar since its location is
+ // not straight-forward to deduce and not really necessary.
+ "touch $${CSUITE_TOOLS}/csuite-tradefed-tests.jar && " +
+ "cp $(location :csuite_generate_module) $${CSUITE_TOOLS} && " +
+ "cp $(location :csuite-launch-instrumentation) $${CSUITE_TESTCASES} && " +
+ "chmod a+x $${CSUITE_TOOLS}/csuite-tradefed && " +
+ "$(location soong_zip) -o $(out) -d -C $(genDir) -D $${ANDROID_CSUITE}",
+ out: ["csuite-standalone.zip"],
+ srcs: [
+ ":csuite-launch-instrumentation",
+ ":tradefed",
+ ":compatibility-host-util",
+ ],
+ tools: [
+ "soong_zip",
+ ":csuite-tradefed",
+ ":csuite_generate_module",
+ ],
+}
+
+python_library_host {
+ name: "csuite_test_utils",
+ srcs: [
+ "csuite_test_utils.py",
+ ],
+ defaults: [
+ "csuite_python_defaults",
+ ],
+ java_data: [
+ "csuite_standalone_zip",
+ ],
+ libs: [
+ "csuite_test",
+ ],
+}
+
+python_test_host {
+ name: "csuite_cli_test",
+ srcs: [
+ "csuite_cli_test.py",
+ ],
+ test_config_template: "csuite_test_template.xml",
+ test_suites: [
+ "general-tests",
+ ],
+ libs: [
+ "csuite_test_utils",
+ ],
+ defaults: [
+ "csuite_python_defaults",
+ ],
+}
+
+python_test_host {
+ name: "csuite_crash_detection_test",
+ srcs: [
+ "csuite_crash_detection_test.py",
+ ],
+ test_config_template: "csuite_test_template.xml",
+ test_suites: [
+ "general-tests",
+ ],
+ libs: [
+ "csuite_test_utils",
+ ],
+ data: [
+ ":csuite_crash_on_launch_test_app",
+ ":csuite_no_crash_test_app",
+ ],
+ defaults: [
+ "csuite_python_defaults",
+ ],
+ test_options: {
+ unit_test: false,
+ },
+}
diff --git a/integration_tests/TEST_MAPPING b/integration_tests/TEST_MAPPING
new file mode 100644
index 0000000..9d98a72
--- /dev/null
+++ b/integration_tests/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+ "postsubmit": [
+ {
+ "name": "csuite_cli_test"
+ },
+ {
+ "name": "csuite_crash_detection_test"
+ }
+ ]
+}
diff --git a/integration_tests/csuite_cli_test.py b/integration_tests/csuite_cli_test.py
new file mode 100644
index 0000000..84925e2
--- /dev/null
+++ b/integration_tests/csuite_cli_test.py
@@ -0,0 +1,33 @@
+# Lint as: python3
+#
+# Copyright 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.
+"""Tests the C-Suite command line interface."""
+
+import csuite_test_utils
+
+
+class CSuiteCliTest(csuite_test_utils.TestCase):
+
+ def test_prints_suite_name(self):
+ with csuite_test_utils.CSuiteHarness() as harness:
+ completed_process = harness.run_and_wait(
+ ['run', 'commandAndExit', 'version'])
+
+ self.assertEqual(0, completed_process.returncode)
+ self.assertIn('App Compatibility Test Suite', completed_process.stdout)
+
+
+if __name__ == '__main__':
+ csuite_test_utils.main()
diff --git a/integration_tests/csuite_crash_detection_test.py b/integration_tests/csuite_crash_detection_test.py
new file mode 100644
index 0000000..9dd8a00
--- /dev/null
+++ b/integration_tests/csuite_crash_detection_test.py
@@ -0,0 +1,104 @@
+# Lint as: python3
+#
+# Copyright 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.
+"""Tests C-Suite's crash detection behavior."""
+
+import csuite_test_utils
+
+
+class CrashDetectionTest(csuite_test_utils.TestCase):
+
+ def setUp(self):
+ super(CrashDetectionTest, self).setUp()
+ self.adb = csuite_test_utils.Adb()
+ self.repo = csuite_test_utils.PackageRepository()
+ self.harness = csuite_test_utils.CSuiteHarness()
+
+ def tearDown(self):
+ super(CrashDetectionTest, self).tearDown()
+ self.harness.cleanup()
+ self.repo.cleanup()
+
+ def test_no_crash_test_passes(self):
+ test_app_package = 'android.csuite.nocrashtestapp'
+ self.adb.run(['logcat', '-c'])
+
+ completed_process = self.run_test(
+ test_app_package=test_app_package,
+ test_app_module='csuite_no_crash_test_app')
+
+ self.expect_regex(completed_process.stdout, r"""PASSED\s*:\s*1""")
+ self.expect_app_launched(test_app_package)
+ self.expect_package_not_installed(test_app_package)
+
+ def test_crash_on_launch_test_fails(self):
+ test_app_package = 'android.csuite.crashonlaunchtestapp'
+ self.adb.run(['logcat', '-c'])
+
+ completed_process = self.run_test(
+ test_app_package=test_app_package,
+ test_app_module='csuite_crash_on_launch_test_app')
+
+ self.expect_regex(completed_process.stdout, r"""FAILED\s*:\s*1""")
+ self.expect_app_launched(test_app_package)
+ self.expect_package_not_installed(test_app_package)
+
+ def run_test(self, test_app_package, test_app_module):
+ """Set up and run the launcher for a given test app."""
+
+ # We don't check the return code since adb returns non-zero exit code if
+ # the package does not exist.
+ self.adb.uninstall(test_app_package, check=False)
+ self.assert_package_not_installed(test_app_package)
+
+ module_name = self.harness.add_module(test_app_package)
+ self.repo.add_package_apks(
+ test_app_package, csuite_test_utils.get_test_app_apks(test_app_module))
+
+ file_resolver_class = 'com.android.csuite.config.AppRemoteFileResolver'
+
+ return self.harness.run_and_wait([
+ '--serial',
+ csuite_test_utils.get_device_serial(),
+ 'run',
+ 'commandAndExit',
+ 'launch',
+ '-m',
+ module_name,
+ '--enable-module-dynamic-download',
+ '--dynamic-download-args',
+ '%s:uri-template=file://%s/{package}' %
+ (file_resolver_class, self.repo.get_path())
+ ])
+
+ def expect_regex(self, s, regex):
+ with self.subTest():
+ self.assertRegex(s, regex)
+
+ def assert_package_not_installed(self, package_name):
+ self.assertNotIn(package_name, self.adb.list_packages())
+
+ def expect_package_not_installed(self, package_name):
+ with self.subTest():
+ self.assert_package_not_installed(package_name)
+
+ def expect_app_launched(self, tag):
+ logcat_process = self.adb.run(['logcat', '-d', '-v', 'brief', '-s', tag])
+ with self.subTest():
+ self.assertIn('App launched', logcat_process.stdout)
+
+
+if __name__ == '__main__':
+ csuite_test_utils.main()
diff --git a/integration_tests/csuite_crash_on_launch_test_app/Android.bp b/integration_tests/csuite_crash_on_launch_test_app/Android.bp
new file mode 100644
index 0000000..ec0022b
--- /dev/null
+++ b/integration_tests/csuite_crash_on_launch_test_app/Android.bp
@@ -0,0 +1,27 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "csuite_crash_on_launch_test_app",
+ srcs: [
+ "*.java",
+ ],
+ sdk_version: "test_current",
+ min_sdk_version: "29",
+ target_sdk_version: "29",
+}
diff --git a/integration_tests/csuite_crash_on_launch_test_app/AndroidManifest.xml b/integration_tests/csuite_crash_on_launch_test_app/AndroidManifest.xml
new file mode 100755
index 0000000..496aefb
--- /dev/null
+++ b/integration_tests/csuite_crash_on_launch_test_app/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package='android.csuite.crashonlaunchtestapp' >
+ <application>
+ <activity android:name="android.csuite.TestAppActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/integration_tests/csuite_crash_on_launch_test_app/TestAppActivity.java b/integration_tests/csuite_crash_on_launch_test_app/TestAppActivity.java
new file mode 100644
index 0000000..2838a18
--- /dev/null
+++ b/integration_tests/csuite_crash_on_launch_test_app/TestAppActivity.java
@@ -0,0 +1,30 @@
+/*
+ * 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 android.csuite;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+public final class TestAppActivity extends Activity {
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ Log.i(getApplicationContext().getPackageName(), "App launched");
+ throw new RuntimeException("Expected exception");
+ }
+}
diff --git a/integration_tests/csuite_no_crash_test_app/Android.bp b/integration_tests/csuite_no_crash_test_app/Android.bp
new file mode 100644
index 0000000..e9ee60a
--- /dev/null
+++ b/integration_tests/csuite_no_crash_test_app/Android.bp
@@ -0,0 +1,27 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "csuite_no_crash_test_app",
+ srcs: [
+ "*.java",
+ ],
+ sdk_version: "test_current",
+ min_sdk_version: "29",
+ target_sdk_version: "29",
+}
diff --git a/integration_tests/csuite_no_crash_test_app/AndroidManifest.xml b/integration_tests/csuite_no_crash_test_app/AndroidManifest.xml
new file mode 100755
index 0000000..bd76721
--- /dev/null
+++ b/integration_tests/csuite_no_crash_test_app/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package='android.csuite.nocrashtestapp' >
+ <application>
+ <activity android:name="android.csuite.TestAppActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/integration_tests/csuite_no_crash_test_app/TestAppActivity.java b/integration_tests/csuite_no_crash_test_app/TestAppActivity.java
new file mode 100644
index 0000000..e61f484
--- /dev/null
+++ b/integration_tests/csuite_no_crash_test_app/TestAppActivity.java
@@ -0,0 +1,29 @@
+/*
+ * 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 android.csuite;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+
+public final class TestAppActivity extends Activity {
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ Log.i(getApplicationContext().getPackageName(), "App launched");
+ }
+}
diff --git a/harness/src/main/resources/config/launch.xml b/integration_tests/csuite_test_template.xml
index a1a158d..837716a 100644
--- a/harness/src/main/resources/config/launch.xml
+++ b/integration_tests/csuite_test_template.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<!-- 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.
@@ -13,10 +13,11 @@
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>
+ <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest">
+ <option name="par-file-name" value="{MODULE}"/>
+ <option name="inject-serial-option" value="true"/>
+ <option name="use-test-output-file" value="true"/>
+ <option name="test-timeout" value="10m"/>
+ </test>
</configuration>
diff --git a/integration_tests/csuite_test_utils.py b/integration_tests/csuite_test_utils.py
new file mode 100644
index 0000000..e72535f
--- /dev/null
+++ b/integration_tests/csuite_test_utils.py
@@ -0,0 +1,283 @@
+# Lint as: python3
+#
+# Copyright 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.
+"""Utilities for C-Suite integration tests."""
+
+import argparse
+import contextlib
+import logging
+import os
+import pathlib
+import shlex
+import shutil
+import stat
+import subprocess
+import sys
+import tempfile
+from typing import Sequence, Text
+import zipfile
+import csuite_test
+
+# Export symbols to reduce the number of imports tests have to list.
+TestCase = csuite_test.TestCase # pylint: disable=invalid-name
+get_device_serial = csuite_test.get_device_serial
+
+# Keep any created temporary directories for debugging test failures. The
+# directories do not need explicit removal since they are created using the
+# system's temporary-file facility.
+_KEEP_TEMP_DIRS = False
+
+
+class CSuiteHarness(contextlib.AbstractContextManager):
+ """Interface class for interacting with the C-Suite harness.
+
+ WARNING: Explicitly clean up created instances or use as a context manager.
+ Not doing so will result in a ResourceWarning for the implicit cleanup which
+ confuses the TradeFed Python test output parser.
+ """
+
+ def __init__(self):
+ self._suite_dir = pathlib.Path(tempfile.mkdtemp(prefix='csuite'))
+ logging.debug('Created harness directory: %s', self._suite_dir)
+
+ with zipfile.ZipFile(_get_standalone_zip_path(), 'r') as f:
+ f.extractall(self._suite_dir)
+
+ # Add owner-execute permission on scripts since zip does not preserve them.
+ self._launcher_binary = self._suite_dir.joinpath(
+ 'android-csuite/tools/csuite-tradefed')
+ _add_owner_exec_permission(self._launcher_binary)
+
+ self._generate_module_binary = self._suite_dir.joinpath(
+ 'android-csuite/tools/csuite_generate_module')
+ _add_owner_exec_permission(self._generate_module_binary)
+
+ self._testcases_dir = self._suite_dir.joinpath('android-csuite/testcases')
+
+ def __exit__(self, unused_type, unused_value, unused_traceback):
+ self.cleanup()
+
+ def cleanup(self):
+ if _KEEP_TEMP_DIRS:
+ return
+ shutil.rmtree(self._suite_dir, ignore_errors=True)
+
+ def add_module(self, package_name: Text) -> Text:
+ """Generates and adds a test module for the provided package."""
+ module_name = 'csuite_%s' % package_name
+
+ with tempfile.TemporaryDirectory() as o:
+ out_dir = pathlib.Path(o)
+ package_list_path = out_dir.joinpath('packages.list')
+
+ package_list_path.write_text(package_name + '\n')
+
+ flags = ['--package-list', package_list_path, '--root-dir', out_dir]
+
+ _run_command([self._generate_module_binary] + flags)
+
+ out_file_path = self._testcases_dir.joinpath(module_name + '.config')
+ shutil.copy(
+ out_dir.joinpath(package_name, 'AndroidTest.xml'), out_file_path)
+
+ return module_name
+
+ def run_and_wait(self, flags: Sequence[Text]) -> subprocess.CompletedProcess:
+ """Starts the Tradefed launcher and waits for it to complete."""
+
+ env = os.environ.copy()
+
+ # Unset environment variables that would cause the script to think it's in a
+ # build tree.
+ env.pop('ANDROID_BUILD_TOP', None)
+ env.pop('ANDROID_HOST_OUT', None)
+
+ # Unset environment variables that would cause TradeFed to find test configs
+ # other than the ones created by the test.
+ env.pop('ANDROID_HOST_OUT_TESTCASES', None)
+ env.pop('ANDROID_TARGET_OUT_TESTCASES', None)
+
+ # Unset environment variables that might cause the suite to pick up a
+ # connected device that wasn't explicitly specified.
+ env.pop('ANDROID_SERIAL', None)
+
+ # Set the environment variable that TradeFed requires to find test modules.
+ env['ANDROID_TARGET_OUT_TESTCASES'] = self._testcases_dir
+
+ return _run_command([self._launcher_binary] + flags, env=env)
+
+
+class PackageRepository(contextlib.AbstractContextManager):
+ """A file-system based APK repository for use in tests.
+
+ WARNING: Explicitly clean up created instances or use as a context manager.
+ Not doing so will result in a ResourceWarning for the implicit cleanup which
+ confuses the TradeFed Python test output parser.
+ """
+
+ def __init__(self):
+ self._root_dir = pathlib.Path(tempfile.mkdtemp(prefix='csuite_apk_dir'))
+ logging.info('Created repository directory: %s', self._root_dir)
+
+ def __exit__(self, unused_type, unused_value, unused_traceback):
+ self.cleanup()
+
+ def cleanup(self):
+ if _KEEP_TEMP_DIRS:
+ return
+ shutil.rmtree(self._root_dir, ignore_errors=True)
+
+ def get_path(self) -> pathlib.Path:
+ """Returns the path to the repository's root directory."""
+ return self._root_dir
+
+ def add_package_apks(self, package_name: Text,
+ apk_paths: Sequence[pathlib.Path]):
+ """Adds the provided package APKs to the repository."""
+ apk_dir = self._root_dir.joinpath(package_name)
+
+ # Raises if the directory already exists.
+ apk_dir.mkdir()
+ for f in apk_paths:
+ shutil.copy(f, apk_dir)
+
+
+class Adb:
+ """Encapsulates adb functionality to simplify usage in tests.
+
+ Most methods in this class raise an exception if they fail to execute. This
+ behavior can be overridden by using the check parameter.
+ """
+
+ def __init__(self,
+ adb_binary_path: pathlib.Path = None,
+ device_serial: Text = None):
+ self._args = [adb_binary_path or 'adb']
+
+ device_serial = device_serial or get_device_serial()
+ if device_serial:
+ self._args.extend(['-s', device_serial])
+
+ def shell(self,
+ args: Sequence[Text],
+ check: bool = None) -> subprocess.CompletedProcess:
+ """Runs an adb shell command and waits for it to complete.
+
+ Note that the exit code of the returned object corresponds to that of
+ the adb command and not the command executed in the shell.
+
+ Args:
+ args: a sequence of program arguments to pass to the shell.
+ check: whether to raise if the process terminates with a non-zero exit
+ code.
+
+ Returns:
+ An object representing a process that has finished and that can be
+ queried.
+ """
+ return self.run(['shell'] + args, check)
+
+ def run(self,
+ args: Sequence[Text],
+ check: bool = None) -> subprocess.CompletedProcess:
+ """Runs an adb command and waits for it to complete."""
+ return _run_command(self._args + args, check=check)
+
+ def uninstall(self, package_name: Text, check: bool = None):
+ """Uninstalls the specified package."""
+ self.run(['uninstall', package_name], check=check)
+
+ def list_packages(self) -> Sequence[Text]:
+ """Lists packages installed on the device."""
+ p = self.shell(['pm', 'list', 'packages'])
+ return [l.split(':')[1] for l in p.stdout.splitlines()]
+
+
+def _run_command(args, check=True, **kwargs) -> subprocess.CompletedProcess:
+ """A wrapper for subprocess.run that overrides defaults and adds logging."""
+ env = kwargs.get('env', {})
+
+ # Log the command-line for debugging failed tests. Note that we convert
+ # tokens to strings for _shlex_join.
+ env_str = ['env', '-i'] + ['%s=%s' % (k, v) for k, v in env.items()]
+ args_str = [str(t) for t in args]
+
+ # Override some defaults. Note that 'check' deviates from this pattern to
+ # avoid getting warnings about using subprocess.run without an explicitly set
+ # `check` parameter.
+ kwargs.setdefault('capture_output', True)
+ kwargs.setdefault('universal_newlines', True)
+
+ logging.debug('Running command: %s', _shlex_join(env_str + args_str))
+
+ return subprocess.run(args, check=check, **kwargs)
+
+
+def _add_owner_exec_permission(path: pathlib.Path):
+ path.chmod(path.stat().st_mode | stat.S_IEXEC)
+
+
+def get_test_app_apks(app_module_name: Text) -> Sequence[pathlib.Path]:
+ """Returns a test app's apk file paths."""
+ return [_get_test_file(app_module_name + '.apk')]
+
+
+def _get_standalone_zip_path():
+ """Returns the suite standalone zip file's path."""
+ return _get_test_file('csuite-standalone.zip')
+
+
+def _get_test_file(name: Text) -> pathlib.Path:
+ test_dir = _get_test_dir()
+ test_file = test_dir.joinpath(name)
+
+ if not test_file.exists():
+ raise RuntimeError('Unable to find the file `%s` in the test execution dir '
+ '`%s`; are you missing a data dependency in the build '
+ 'module?' % (name, test_dir))
+
+ return test_file
+
+
+def _shlex_join(split_command: Sequence[Text]) -> Text:
+ """Concatenate tokens and return a shell-escaped string."""
+ # This is an alternative to shlex.join that doesn't exist in Python versions
+ # < 3.8.
+ return ' '.join(shlex.quote(t) for t in split_command)
+
+
+def _get_test_dir() -> pathlib.Path:
+ return pathlib.Path(__file__).parent
+
+
+def main():
+ global _KEEP_TEMP_DIRS
+
+ parser = argparse.ArgumentParser(parents=[csuite_test.create_arg_parser()])
+ parser.add_argument(
+ '--log-level',
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
+ default='WARNING',
+ help='sets the logging level threshold')
+ parser.add_argument(
+ '--keep-temp-dirs',
+ type=bool,
+ help='keeps any created temporary directories for debugging failures')
+ args, unittest_argv = parser.parse_known_args(sys.argv)
+
+ _KEEP_TEMP_DIRS = args.keep_temp_dirs
+ logging.basicConfig(level=getattr(logging, args.log_level))
+
+ csuite_test.run_tests(args, unittest_argv)
diff --git a/pylib/Android.bp b/pylib/Android.bp
new file mode 100644
index 0000000..319060f
--- /dev/null
+++ b/pylib/Android.bp
@@ -0,0 +1,42 @@
+// 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.
+
+// The below module creates a standalone zip that end-to-end tests can depend
+// on for running the suite. This is a workaround since we can't use csuite.zip
+// which is defined in an external Makefile that Soong can't depend on.
+//
+// Besides listing jars we know the launcher script depends on which is
+// brittle, this is a hack for several reasons. First, we're listing our
+// dependencies in the tools attribute when we should be using the 'srcs'
+// attribute. Second, we're accessing jars using a path relative to a known
+// artifact location instead of using the Soong 'location' feature.
+//
+// Normally we would just use java_genrule_host to avoid these hacks but can't
+// do that since Soong currently complains when a python_host_test depends on
+// that target since, although compatible, the arch variants (x86_64 and
+// common) don't exactly match.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_library_host {
+ name: "csuite_test",
+ srcs: [
+ "csuite_test.py",
+ ],
+ defaults: [
+ "csuite_python_defaults",
+ ],
+}
diff --git a/pylib/csuite_test.py b/pylib/csuite_test.py
new file mode 100644
index 0000000..fa06ce3
--- /dev/null
+++ b/pylib/csuite_test.py
@@ -0,0 +1,107 @@
+# Lint as: python3
+#
+# Copyright 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.
+"""CSuite-specific automated unit test functionality."""
+
+import argparse
+import sys
+from typing import Any, Sequence, Text
+import unittest
+
+# Export the TestCase class to reduce the number of imports tests have to list.
+TestCase = unittest.TestCase
+
+_DEVICE_SERIAL = None
+
+
+def get_device_serial() -> Text:
+ """Returns the serial of the connected device."""
+ if not _DEVICE_SERIAL:
+ raise RuntimeError(
+ 'Device serial is unset, did you call main in your test?')
+ return _DEVICE_SERIAL
+
+
+def create_arg_parser(add_help: bool = False) -> argparse.ArgumentParser:
+ """Creates a new parser that can handle the default command-line flags.
+
+ The object returned by this function can be used by other modules that want to
+ add their own command-line flags. The returned parser is intended to be passed
+ to the 'parents' argument of ArgumentParser and extend the set of default
+ flags with additional ones.
+
+ Args:
+ add_help: whether to add an option which simply displays the parser’s help
+ message; this is typically false when used from other modules that want to
+ use the returned parser as a parent argument parser.
+
+ Returns:
+ A new arg parser that can handle the default flags expected by this module.
+ """
+
+ # The below flags are passed in by the TF Python test runner.
+ parser = argparse.ArgumentParser(add_help=add_help)
+
+ parser.add_argument('-s', '--serial', help='the device serial')
+ parser.add_argument(
+ '--test-output-file',
+ help='the file in which to store the test results',
+ required=True)
+
+ return parser
+
+
+def run_tests(args: Any, unittest_argv: Sequence[Text]) -> None:
+ """Executes a set of Python unit tests.
+
+ This function is typically used by modules that extend command-line flags.
+ Callers create their own argument parser with this module's parser as a parent
+ and parse the command-line. The resulting object is will contain the
+ attributes expected by this module and is used to call this method.
+
+ Args:
+ args: an object that contains at least the set of attributes defined in
+ objects returned when using the default argument parser.
+ unittest_argv: the list of command-line arguments to forward to
+ unittest.main.
+ """
+ global _DEVICE_SERIAL
+
+ _DEVICE_SERIAL = args.serial
+
+ with open(args.test_output_file, 'w') as test_output_file:
+
+ # Note that we use a type and not an instance for 'testRunner' since
+ # TestProgram forwards its constructor arguments when creating an instance
+ # of the runner type. Not doing so would require us to make sure that the
+ # parameters passed to TestProgram are aligned with those for creating a
+ # runner instance.
+ class TestRunner(unittest.TextTestRunner):
+ """A test runner that writes test results to the TF-provided file."""
+
+ def __init__(self, *args, **kwargs):
+ super(TestRunner, self).__init__(
+ stream=test_output_file, *args, **kwargs)
+
+ # Setting verbosity is required to generate output that the TradeFed test
+ # runner can parse.
+ unittest.TestProgram(verbosity=3, testRunner=TestRunner, argv=unittest_argv)
+
+
+def main():
+ """Executes a set of Python unit tests."""
+ args, unittest_argv = create_arg_parser(add_help=True).parse_known_args(
+ sys.argv)
+ run_tests(args, unittest_argv)
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..c8c911f
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,427 @@
+# This Pylint rcfile contains a best-effort configuration to uphold the
+# best-practices and style described in the Google Python style guide:
+# https://google.github.io/styleguide/pyguide.html
+#
+# Its canonical open-source location is:
+# https://google.github.io/styleguide/pylintrc
+
+[MASTER]
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=third_party
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Pickle collected data for later comparisons.
+persistent=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Use multiple processes to speed up Pylint.
+jobs=4
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=apply-builtin,
+ backtick,
+ bad-option-value,
+ basestring-builtin,
+ buffer-builtin,
+ c-extension-no-member,
+ cmp-builtin,
+ cmp-method,
+ coerce-builtin,
+ coerce-method,
+ delslice-method,
+ div-method,
+ duplicate-code,
+ eq-without-hash,
+ execfile-builtin,
+ file-builtin,
+ filter-builtin-not-iterating,
+ fixme,
+ getslice-method,
+ global-statement,
+ hex-method,
+ idiv-method,
+ implicit-str-concat-in-sequence,
+ import-error,
+ import-self,
+ import-star-module-level,
+ input-builtin,
+ intern-builtin,
+ invalid-str-codec,
+ locally-disabled,
+ long-builtin,
+ long-suffix,
+ map-builtin-not-iterating,
+ metaclass-assignment,
+ next-method-called,
+ next-method-defined,
+ no-absolute-import,
+ no-else-break,
+ no-else-continue,
+ no-else-raise,
+ no-else-return,
+ no-member,
+ no-self-use,
+ nonzero-method,
+ oct-method,
+ old-division,
+ old-ne-operator,
+ old-octal-literal,
+ old-raise-syntax,
+ parameter-unpacking,
+ print-statement,
+ raising-string,
+ range-builtin-not-iterating,
+ raw_input-builtin,
+ rdiv-method,
+ reduce-builtin,
+ relative-import,
+ reload-builtin,
+ round-builtin,
+ setslice-method,
+ signature-differs,
+ standarderror-builtin,
+ suppressed-message,
+ sys-max-int,
+ too-few-public-methods,
+ too-many-ancestors,
+ too-many-arguments,
+ too-many-boolean-expressions,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-locals,
+ too-many-public-methods,
+ too-many-return-statements,
+ too-many-statements,
+ trailing-newlines,
+ unichr-builtin,
+ unicode-builtin,
+ unpacking-in-except,
+ useless-else-on-loop,
+ useless-suppression,
+ using-cmp-argument,
+ xrange-builtin,
+ zip-builtin-not-iterating,
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]". This option is deprecated
+# and it will be removed in Pylint 2.0.
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=main,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
+
+# Regular expression matching correct function names
+function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct constant names
+const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct attribute names
+attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
+
+# Regular expression matching correct argument names
+argument-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=^_?[A-Z][a-zA-Z0-9]*$
+
+# Regular expression matching correct module names
+module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
+
+# Regular expression matching correct method names
+method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
+# lines made too long by directives to pytype.
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=(?x)(
+ ^\s*(\#\ )?<?https?://\S+>?$|
+ ^\s*(from\s+\S+\s+)?import\s+.+$)
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=yes
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=
+
+# Maximum number of lines in a module
+max-module-lines=99999
+
+# String used as indentation unit. The internal Google style guide mandates 2
+# spaces. Google's externaly-published style guide says 4, consistent with
+# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
+# projects (like TensorFlow).
+indent-string=' '
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=TODO
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging,absl.logging,tensorflow.google.logging
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+ TERMIOS,
+ Bastion,
+ rexec,
+ sets
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant, absl
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls,
+ class_
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=StandardError,
+ Exception,
+ BaseException
diff --git a/test_targets/csuite-app-launch/Android.bp b/test_targets/csuite-app-launch/Android.bp
new file mode 100644
index 0000000..256d9f4
--- /dev/null
+++ b/test_targets/csuite-app-launch/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "csuite-app-launch",
+ test_config_template: "template.xml"
+}
diff --git a/test_targets/csuite-app-launch/template.xml b/test_targets/csuite-app-launch/template.xml
new file mode 100644
index 0000000..52c4611
--- /dev/null
+++ b/test_targets/csuite-app-launch/template.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<configuration description="Launches an app and check for crashes">
+ <option name="package-name" value="{package}"/>
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparer">
+ <option name="test-file-name" value="app://{package}"/>
+ </target_preparer>
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.compatibility.testtype.AppLaunchTest"/>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-pre-installed-app-launch/Android.bp b/test_targets/csuite-pre-installed-app-launch/Android.bp
new file mode 100644
index 0000000..539306a
--- /dev/null
+++ b/test_targets/csuite-pre-installed-app-launch/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "csuite-pre-installed-app-launch",
+ test_config_template: "template.xml"
+}
diff --git a/test_targets/csuite-pre-installed-app-launch/template.xml b/test_targets/csuite-pre-installed-app-launch/template.xml
new file mode 100644
index 0000000..2d20306
--- /dev/null
+++ b/test_targets/csuite-pre-installed-app-launch/template.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Launches an app that exists on the device and check for crashes">
+ <option name="package-name" value="{package}"/>
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.compatibility.testtype.AppLaunchTest"/>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-system-app-launch/Android.bp b/test_targets/csuite-system-app-launch/Android.bp
new file mode 100644
index 0000000..2514740
--- /dev/null
+++ b/test_targets/csuite-system-app-launch/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "csuite-system-app-launch",
+ test_config_template: "template.xml"
+}
diff --git a/test_targets/csuite-system-app-launch/template.xml b/test_targets/csuite-system-app-launch/template.xml
new file mode 100644
index 0000000..4d1181b
--- /dev/null
+++ b/test_targets/csuite-system-app-launch/template.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<configuration description="Reinstalls a system app and check for launch crashes.">
+ <option name="package-name" value="{package}"/>
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparer">
+ <option name="test-file-name" value="app://{package}"/>
+ </target_preparer>
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.compatibility.testtype.AppLaunchTest"/>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-test-package-launch/Android.bp b/test_targets/csuite-test-package-launch/Android.bp
new file mode 100644
index 0000000..6cdd899
--- /dev/null
+++ b/test_targets/csuite-test-package-launch/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "csuite-test-package-launch",
+ test_config_template: "template.xml"
+}
diff --git a/test_targets/csuite-test-package-launch/template.xml b/test_targets/csuite-test-package-launch/template.xml
new file mode 100644
index 0000000..9c97fd3
--- /dev/null
+++ b/test_targets/csuite-test-package-launch/template.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+<configuration description="Installs a test package with -t arg and check for launch crashes">
+ <option name="package-name" value="{package}"/>
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparer">
+ <option name="test-file-name" value="app://{package}"/>
+ <option name="install-arg" value="-t"/>
+ </target_preparer>
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.compatibility.testtype.AppLaunchTest"/>
+</configuration> \ No newline at end of file
diff --git a/tools/csuite-tradefed/Android.bp b/tools/csuite-tradefed/Android.bp
index 82d959e..a441726 100644
--- a/tools/csuite-tradefed/Android.bp
+++ b/tools/csuite-tradefed/Android.bp
@@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
tradefed_binary_host {
name: "csuite-tradefed",
wrapper: "src/scripts/csuite-tradefed",
@@ -33,5 +37,7 @@ java_test_host {
"tradefed",
"csuite-tradefed",
],
- test_suites: ["general-tests"],
+ test_options: {
+ unit_test: true,
+ },
}
diff --git a/tools/csuite-tradefed/AndroidTest.xml b/tools/csuite-tradefed/AndroidTest.xml
deleted file mode 100644
index 850750e..0000000
--- a/tools/csuite-tradefed/AndroidTest.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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/TEST_MAPPING b/tools/csuite-tradefed/TEST_MAPPING
deleted file mode 100644
index 89c2072..0000000
--- a/tools/csuite-tradefed/TEST_MAPPING
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "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
index 4277884..f3b887a 100644
--- a/tools/csuite-tradefed/src/scripts/csuite-tradefed
+++ b/tools/csuite-tradefed/src/scripts/csuite-tradefed
@@ -83,7 +83,6 @@ JAR_DIR=${CSUITE_ROOT}/android-csuite/tools
TRADEFED_JAR="tradefed"
JARS="tradefed
- hosttestlib
compatibility-host-util
csuite-tradefed
csuite-tradefed-tests"
@@ -99,7 +98,7 @@ OPTIONAL_JARS="
google-tf-prod-tests"
for JAR in $OPTIONAL_JARS; do
- if [ -f "$JAR.jar" ]; then
+ if [ -f "${JAR_DIR}/${JAR}.jar" ]; then
JAR_PATH=${JAR_PATH}:${JAR_DIR}/${JAR}.jar
fi;
done
@@ -119,4 +118,4 @@ 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 "$@"
+java $RDBG_FLAG -cp ${JAR_PATH} -DCSUITE_ROOT=${CSUITE_ROOT} com.android.compatibility.common.tradefed.command.CompatibilityConsole "$@"
diff --git a/tools/csuite_test/Android.bp b/tools/csuite_test/Android.bp
new file mode 100644
index 0000000..a1103dc
--- /dev/null
+++ b/tools/csuite_test/Android.bp
@@ -0,0 +1,20 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+ name: "soong-csuite",
+ pkgPath: "android/soong/csuite",
+ deps: [
+ "blueprint",
+ "soong-android",
+ "soong-java",
+ ],
+ srcs: [
+ "csuite_test.go",
+ ],
+ testSrcs: [
+ "csuite_test_test.go",
+ ],
+ pluginFor: ["soong_build"],
+}
diff --git a/tools/csuite_test/csuite_test.go b/tools/csuite_test/csuite_test.go
new file mode 100644
index 0000000..74373f2
--- /dev/null
+++ b/tools/csuite_test/csuite_test.go
@@ -0,0 +1,132 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// 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 csuite
+
+import (
+ "android/soong/android"
+ "android/soong/java"
+ "strings"
+)
+
+var (
+ pctx = android.NewPackageContext("android/soong/csuite")
+)
+
+func init() {
+ android.RegisterModuleType("csuite_test", CSuiteTestFactory)
+}
+
+type csuiteTestProperties struct {
+ // Local path to a module template xml file.
+ // The content of the template will be used to generate test modules at runtime.
+ Test_config_template *string `android:"path"`
+
+ // Local path to a test plan config xml to be included in the generated plan.
+ Test_plan_include *string `android:"path"`
+}
+
+type CSuiteTest struct {
+ // Java TestHost.
+ java.TestHost
+
+ // C-Suite test properties struct.
+ csuiteTestProperties csuiteTestProperties
+}
+
+func (cSuiteTest *CSuiteTest) buildCopyConfigTemplateCommand(ctx android.ModuleContext, rule *android.RuleBuilder) string {
+ if cSuiteTest.csuiteTestProperties.Test_config_template == nil {
+ ctx.ModuleErrorf(`'test_config_template' is missing.`)
+ }
+ inputPath := android.PathForModuleSrc(ctx, *cSuiteTest.csuiteTestProperties.Test_config_template)
+ genPath := android.PathForModuleGen(ctx, planConfigDirName, ctx.ModuleName()+configTemplateFileExtension)
+ rule.Command().Textf("cp").Input(inputPath).Output(genPath)
+ cSuiteTest.AddExtraResource(genPath)
+ return genPath.Rel()
+}
+
+func (cSuiteTest *CSuiteTest) buildCopyPlanIncludeCommand(ctx android.ModuleContext, rule *android.RuleBuilder) string {
+ if cSuiteTest.csuiteTestProperties.Test_plan_include == nil {
+ return emptyPlanIncludePath
+ }
+ inputPath := android.PathForModuleSrc(ctx, *cSuiteTest.csuiteTestProperties.Test_plan_include)
+ genPath := android.PathForModuleGen(ctx, planConfigDirName, "includes", ctx.ModuleName()+".xml")
+ rule.Command().Textf("cp").Input(inputPath).Output(genPath)
+ cSuiteTest.AddExtraResource(genPath)
+ return strings.Replace(genPath.Rel(), "config/", "", -1)
+}
+
+func (cSuiteTest *CSuiteTest) buildWritePlanConfigRule(ctx android.ModuleContext, configTemplatePath string, planIncludePath string) {
+ planName := ctx.ModuleName()
+ content := strings.Replace(planTemplate, "{planName}", planName, -1)
+ content = strings.Replace(content, "{templatePath}", configTemplatePath, -1)
+ content = strings.Replace(content, "{planInclude}", planIncludePath, -1)
+ genPath := android.PathForModuleGen(ctx, planConfigDirName, planName+planFileExtension)
+ android.WriteFileRule(ctx, genPath, content)
+ cSuiteTest.AddExtraResource(genPath)
+}
+
+func (cSuiteTest *CSuiteTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+ rule := android.NewRuleBuilder(pctx, ctx)
+
+ configTemplatePath := cSuiteTest.buildCopyConfigTemplateCommand(ctx, rule)
+ planIncludePath := cSuiteTest.buildCopyPlanIncludeCommand(ctx, rule)
+ cSuiteTest.buildWritePlanConfigRule(ctx, configTemplatePath, planIncludePath)
+
+ rule.Build("CSuite", "generate C-Suite config files")
+ cSuiteTest.TestHost.GenerateAndroidBuildActions(ctx)
+}
+
+func CSuiteTestFactory() android.Module {
+ module := &CSuiteTest{}
+ module.AddProperties(&module.csuiteTestProperties)
+ installable := true
+ autoGenConfig := false
+ java.InitTestHost(&module.TestHost, &installable, []string{"csuite"}, &autoGenConfig)
+
+ java.InitJavaModuleMultiTargets(module, android.HostSupported)
+
+ return module
+}
+
+const (
+ emptyPlanIncludePath = `empty`
+ planConfigDirName = `config`
+ configTemplateFileExtension = `.xml.template`
+ planFileExtension = `.xml`
+ planTemplate = `<?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.
+-->
+<configuration>
+ <test class="com.android.csuite.config.ModuleGenerator">
+ <option name="template" value="{templatePath}" />
+ </test>
+ <include name="csuite-base" />
+ <include name="{planInclude}" />
+ <option name="plan" value="{planName}" />
+</configuration>
+`
+)
diff --git a/tools/csuite_test/csuite_test_test.go b/tools/csuite_test/csuite_test_test.go
new file mode 100644
index 0000000..daf07b0
--- /dev/null
+++ b/tools/csuite_test/csuite_test_test.go
@@ -0,0 +1,286 @@
+// Copyright 2020 Google Inc. All rights reserved.
+//
+// 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 csuite
+
+import (
+ "android/soong/android"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+)
+
+var buildDir string
+
+func TestBpContainsTestHostPropsThrowsError(t *testing.T) {
+ ctx, _ := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template",
+ data_native_bins: "bin"
+ }
+ `)
+
+ _, errs := ctx.ParseBlueprintsFiles("Android.bp")
+
+ android.FailIfNoMatchingErrors(t, `unrecognized property`, errs)
+}
+
+func TestBpContainsManifestThrowsError(t *testing.T) {
+ ctx, _ := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template",
+ test_config: "AndroidTest.xml"
+ }
+ `)
+
+ _, errs := ctx.ParseBlueprintsFiles("Android.bp")
+
+ android.FailIfNoMatchingErrors(t, `unrecognized property`, errs)
+}
+
+func TestBpMissingNameThrowsError(t *testing.T) {
+ ctx, _ := createContextAndConfig(t, `
+ csuite_test {
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ _, errs := ctx.ParseBlueprintsFiles("Android.bp")
+
+ android.FailIfNoMatchingErrors(t, `'name' is missing`, errs)
+}
+
+func TestBpMissingTemplatePathThrowsError(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ }
+ `)
+
+ ctx.ParseBlueprintsFiles("Android.bp")
+ _, errs := ctx.PrepareBuildActions(config)
+
+ android.FailIfNoMatchingErrors(t, `'test_config_template' is missing`, errs)
+}
+
+func TestValidBpMissingPlanIncludeDoesNotThrowError(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+}
+
+func TestValidBpMissingPlanIncludeGeneratesPlanXmlWithoutPlaceholders(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if strings.Contains(content, "{") || strings.Contains(content, "}") {
+ t.Errorf("The generated plan name contains a placeholder: %s", content)
+ }
+}
+
+func TestGeneratedTestPlanContainsPlanName(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if !strings.Contains(content, "plan_name") {
+ t.Errorf("The plan name is missing from the generated plan: %s", content)
+ }
+}
+
+func TestGeneratedTestPlanContainsTemplatePath(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if !strings.Contains(content, "config/plan_name.xml.template") {
+ t.Errorf("The template path is missing from the generated plan: %s", content)
+ }
+}
+
+func TestTemplateFileCopyRuleExists(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ params := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common").Rule("CSuite")
+ assertFileCopyRuleExists(t, params, "test_config.xml.template", "config/plan_name.xml.template")
+}
+
+func TestGeneratedTestPlanContainsPlanInclude(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template",
+ test_plan_include: "include.xml"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if !strings.Contains(content, `"includes/plan_name.xml"`) {
+ t.Errorf("The plan include path is missing from the generated plan: %s", content)
+ }
+}
+
+func TestPlanIncludeFileCopyRuleExists(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "test_config.xml.template",
+ test_plan_include: "include.xml"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ params := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common").Rule("CSuite")
+ assertFileCopyRuleExists(t, params, "include.xml", "config/includes/plan_name.xml")
+}
+
+func TestMain(m *testing.M) {
+ run := func() int {
+ setUp()
+ defer tearDown()
+
+ return m.Run()
+ }
+
+ os.Exit(run())
+}
+
+func parseBpAndBuild(t *testing.T, ctx *android.TestContext, config android.Config) {
+ _, parsingErrs := ctx.ParseBlueprintsFiles("Android.bp")
+ _, buildErrs := ctx.PrepareBuildActions(config)
+
+ android.FailIfErrored(t, parsingErrs)
+ android.FailIfErrored(t, buildErrs)
+}
+
+func assertFileCopyRuleExists(t *testing.T, params android.TestingBuildParams, src string, dst string) {
+ assertPathsContains(t, getAllInputPaths(params), src)
+ assertWritablePathsContainsRel(t, getAllOutputPaths(params), dst)
+ if !strings.HasPrefix(params.RuleParams.Command, "cp") {
+ t.Errorf("'cp' command is missing.")
+ }
+}
+
+func assertPathsContains(t *testing.T, paths android.Paths, path string) {
+ for _, p := range paths {
+ if p.String() == path {
+ return
+ }
+ }
+ t.Errorf("Cannot find expected path %s", path)
+}
+
+func assertWritablePathsContainsRel(t *testing.T, paths android.WritablePaths, relPath string) {
+ for _, path := range paths {
+ if path.Rel() == relPath {
+ return
+ }
+ }
+ t.Errorf("Cannot find expected relative path %s", relPath)
+}
+
+func getAllOutputPaths(params android.TestingBuildParams) android.WritablePaths {
+ var paths []android.WritablePath
+ if params.Output != nil {
+ paths = append(paths, params.Output)
+ }
+ if params.ImplicitOutput != nil {
+ paths = append(paths, params.ImplicitOutput)
+ }
+ if params.SymlinkOutput != nil {
+ paths = append(paths, params.SymlinkOutput)
+ }
+ paths = append(paths, params.Outputs...)
+ paths = append(paths, params.ImplicitOutputs...)
+ paths = append(paths, params.SymlinkOutputs...)
+
+ return paths
+}
+
+func getAllInputPaths(params android.TestingBuildParams) android.Paths {
+ var paths []android.Path
+ if params.Input != nil {
+ paths = append(paths, params.Input)
+ }
+ if params.Implicit != nil {
+ paths = append(paths, params.Implicit)
+ }
+ paths = append(paths, params.Inputs...)
+ paths = append(paths, params.Implicits...)
+
+ return paths
+}
+
+func setUp() {
+ var err error
+ buildDir, err = ioutil.TempDir("", "soong_csuite_test")
+ if err != nil {
+ panic(err)
+ }
+}
+
+func tearDown() {
+ os.RemoveAll(buildDir)
+}
+
+func createContextAndConfig(t *testing.T, bp string) (*android.TestContext, android.Config) {
+ t.Helper()
+ config := android.TestArchConfig(buildDir, nil, bp, nil)
+ ctx := android.NewTestArchContext(config)
+ ctx.RegisterModuleType("csuite_test", CSuiteTestFactory)
+ ctx.Register()
+
+ return ctx, config
+}
diff --git a/tools/csuite_test/go.mod b/tools/csuite_test/go.mod
new file mode 100644
index 0000000..a373cd1
--- /dev/null
+++ b/tools/csuite_test/go.mod
@@ -0,0 +1,14 @@
+module android/soong/csuite
+
+require (
+ android/soong v0.0.0
+ github.com/google/blueprint v0.0.0
+)
+
+replace android/soong v0.0.0 => ../../../../../build/soong
+
+replace github.com/golang/protobuf v0.0.0 => ../../../../../external/golang-protobuf
+
+replace github.com/google/blueprint v0.0.0 => ../../../../../build/blueprint
+
+go 1.13 \ No newline at end of file
diff --git a/tools/script/Android.bp b/tools/script/Android.bp
new file mode 100644
index 0000000..c1eb990
--- /dev/null
+++ b/tools/script/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+python_binary_host {
+ name: "csuite_generate_module",
+ main: "generate_module.py",
+ srcs: [
+ "generate_module.py",
+ ],
+ defaults: [
+ "csuite_python_defaults",
+ ],
+}
+
+python_test_host {
+ name: "generate_module_test",
+ srcs: [
+ "generate_module.py",
+ "generate_module_test.py",
+ ],
+ libs: [
+ "csuite_test",
+ "pyfakefs",
+ ],
+ test_config_template: "csuite_test_template.xml",
+ test_options: {
+ unit_test: true,
+ },
+ defaults: [
+ "csuite_python_defaults",
+ ],
+}
diff --git a/tools/script/csuite_test_template.xml b/tools/script/csuite_test_template.xml
new file mode 100644
index 0000000..837716a
--- /dev/null
+++ b/tools/script/csuite_test_template.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+<configuration>
+ <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest">
+ <option name="par-file-name" value="{MODULE}"/>
+ <option name="inject-serial-option" value="true"/>
+ <option name="use-test-output-file" value="true"/>
+ <option name="test-timeout" value="10m"/>
+ </test>
+</configuration>
diff --git a/tools/script/generate_module.py b/tools/script/generate_module.py
index 30ae7b4..5bda01f 100644
--- a/tools/script/generate_module.py
+++ b/tools/script/generate_module.py
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+# Lint as: python3
#
-# Copyright (C) 2020 The Android Open Source Project
+# 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.
@@ -13,246 +13,237 @@
# 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.
+"""This script generates C-Suite configuration files for a list of apps."""
import argparse
+import contextlib
import glob
import os
+import string
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
+from typing import IO, Set, Text
_ANDROID_BP_FILE_NAME = 'Android.bp'
_ANDROID_XML_FILE_NAME = 'AndroidTest.xml'
+_AUTO_GENERATE_NOTE = 'THIS FILE WAS AUTO-GENERATED. DO NOT EDIT MANUALLY!'
+
+DEFAULT_BUILD_MODULE_TEMPLATE = string.Template("""\
+// 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.
-_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'
+// ${auto_generate_note}
-_CONFIG_TYPE_TARGET_PREPARER = 'target_preparer'
-_CONFIG_TYPE_TEST = 'test'
+csuite_config {
+ name: "csuite_${package_name}",
+}
+""")
+DEFAULT_TEST_MODULE_TEMPLATE = string.Template("""\
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
-def generate_all_modules_from_config(package_list_file_path, root_dir):
- """Generate multiple test and build modules.
+ 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
- Args:
+ 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.
+-->
+<!-- ${auto_generate_note}-->
+
+<configuration description="Tests the compatibility of apps">
+ <option key="plan" name="config-descriptor:metadata" value="app-launch"/>
+ <option name="package-name" value="${package_name}"/>
+ <target_preparer class="com.android.compatibility.targetprep.AppSetupPreparer">
+ <option name="test-file-name" value="csuite-launch-instrumentation.apk"/>
+ <option name="test-file-name" value="app://${package_name}"/>
+ </target_preparer>
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.compatibility.testtype.AppLaunchTest"/>
+</configuration>
+""")
+
+
+def generate_all_modules_from_config(package_list_file_path,
+ root_dir,
+ build_module_template_file_path=None,
+ test_module_template_file_path=None):
+ """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)
+ build_module_template_file_path: path of a file containing build module
+ template.
+ test_module_template_file_path: path of a file containing test module
+ template.
+ """
+ build_module_template = DEFAULT_BUILD_MODULE_TEMPLATE
+ test_module_template = DEFAULT_TEST_MODULE_TEMPLATE
+ if build_module_template_file_path:
+ with open(build_module_template_file_path, 'r') as f:
+ build_module_template = string.Template(f.read())
+ if test_module_template_file_path:
+ with open(test_module_template_file_path, 'r') as f:
+ test_module_template = string.Template(f.read())
+
+ 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, build_module_template,
+ test_module_template)
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 + '/**/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)
+ for filename in glob.iglob(root_dir + '/**/Android.bp'):
+ if _is_auto_generated(filename):
+ os.remove(filename)
- _remove_empty_dirs(root_dir)
+ _remove_empty_dirs(root_dir)
def _is_auto_generated(filename):
- with open(filename, 'r') as f:
- return 'auto-generated' in f.read()
+ with open(filename, 'r') as f:
+ return _AUTO_GENERATE_NOTE 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)
+ 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 parse_package_list(package_list_file: IO[bytes]) -> Set[bytes]:
+ packages = {line.strip() for line in package_list_file.readlines()}
+ for package in packages:
+ if package and not package.startswith('#'):
+ yield package
-def _generate_module_files(package_name, root_dir):
- """Generate test and build modules for a single package.
+def _generate_module_files(package_name, root_dir, build_module_template,
+ test_module_template):
+ """Generate test and build modules for a single package.
- Args:
+ 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_template: template for build module.
+ test_module_template: template for test module.
+ """
+ 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)
+ 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(build_module_path, 'w') as f:
+ write_module(build_module_template, package_name, f)
- with open(test_module_path, 'w') as f:
- write_test_module(package_name, f)
+ with open(test_module_path, 'w') as f:
+ write_module(test_module_template, 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.
-
-"""
+ package_dir_path = os.path.join(root_dir, package_name)
+ os.mkdir(package_dir_path)
-_BUILD_MODULE_TEMPLATE = """csuite_config {{
- name: "csuite_{package_name}",
-}}
-"""
+ return package_dir_path
-_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 write_module(template: string.Template, package_name: Text,
+ out_file: IO[bytes]) -> Text:
+ """Writes the build or test module for the provided package into a file."""
+ test_module = template.substitute(
+ package_name=package_name, auto_generate_note=_AUTO_GENERATE_NOTE)
+ out_file.write(test_module)
def _file_path(path):
- if os.path.isfile(path):
- return path
- raise argparse.ArgumentTypeError('%s is not a valid 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')
+ if os.path.isdir(path):
+ return path
+ raise argparse.ArgumentTypeError('%s is not a valid path' % path)
+
+
+@contextlib.contextmanager
+def _redirect_sys_output(out, err):
+ current_out, current_err = sys.stdout, sys.stderr
+ try:
+ sys.stdout, sys.stderr = out, err
+ yield
+ finally:
+ sys.stdout, sys.stderr = current_out, current_err
+
+
+def parse_args(args, out=sys.stdout, err=sys.stderr):
+ """Parses the provided sequence of arguments."""
+ 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')
+ parser.add_argument(
+ '--test-module-template',
+ type=_file_path,
+ required=False,
+ help='path of the file containing test module configuration template')
+ parser.add_argument(
+ '--build-module-template',
+ type=_file_path,
+ required=False,
+ help='path of the file containing build module configuration template')
+
+ # We redirect stdout and stderr to improve testability since ArgumentParser
+ # always writes to those files. More specifically, the TradeFed python test
+ # runner will choke parsing output that is not in the expected format.
+ with _redirect_sys_output(out, err):
return parser.parse_args(args)
def main():
- parser = parse_args(sys.argv[1:])
- generate_all_modules_from_config(parser.package_list, parser.root_dir)
+ parser = parse_args(sys.argv[1:])
+ generate_all_modules_from_config(parser.package_list, parser.root_dir,
+ parser.build_module_template,
+ parser.test_module_template)
+
if __name__ == '__main__':
- main()
+ main()
diff --git a/tools/script/generate_module_test.py b/tools/script/generate_module_test.py
new file mode 100644
index 0000000..09370a0
--- /dev/null
+++ b/tools/script/generate_module_test.py
@@ -0,0 +1,245 @@
+# Lint as: python3
+#
+# 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.
+"""Tests for the generate_module package."""
+
+import io
+import os
+from xml.etree import cElementTree as ET
+import csuite_test
+import generate_module
+from pyfakefs import fake_filesystem_unittest
+
+_AUTO_GENERATE_NOTE = 'THIS FILE WAS AUTO-GENERATED. DO NOT EDIT MANUALLY!'
+
+
+class WriteTestModuleTest(csuite_test.TestCase):
+
+ def test_output_contains_license(self):
+ out = io.StringIO()
+
+ generate_module.write_module(generate_module.DEFAULT_BUILD_MODULE_TEMPLATE,
+ 'a.package.name', out)
+
+ self.assertIn('Copyright', out.getvalue())
+ self.assertIn('Android Open Source Project', out.getvalue())
+
+ def test_output_is_valid_xml(self):
+ out = io.StringIO()
+
+ generate_module.write_module(generate_module.DEFAULT_TEST_MODULE_TEMPLATE,
+ 'a.package.name', out)
+
+ self.assert_valid_xml(out.getvalue())
+
+ def test_output_contains_package_name(self):
+ package_name = 'a.package.name'
+ out = io.StringIO()
+
+ generate_module.write_module(generate_module.DEFAULT_TEST_MODULE_TEMPLATE,
+ 'a.package.name', out)
+
+ self.assertIn(package_name, out.getvalue())
+
+ def assert_valid_xml(self, xml_str: bytes) -> None:
+ try:
+ ET.parse(io.BytesIO(xml_str.encode('utf8')))
+ except ET.ParseError as e:
+ self.fail('Input \'%s\' is not a valid XML document: %s' % (xml_str, e))
+
+
+class WriteBuildModuleTest(csuite_test.TestCase):
+
+ def test_output_contains_license(self):
+ out = io.StringIO()
+
+ generate_module.write_module(generate_module.DEFAULT_BUILD_MODULE_TEMPLATE,
+ 'a.package.name', out)
+
+ self.assertIn('Copyright', out.getvalue())
+ self.assertIn('Android Open Source Project', out.getvalue())
+
+ def test_output_is_valid_build_file(self):
+ package_name = 'a.package.name'
+ out = io.StringIO()
+
+ generate_module.write_module(generate_module.DEFAULT_BUILD_MODULE_TEMPLATE,
+ 'a.package.name', out)
+
+ out_str = out.getvalue()
+ self.assert_braces_balanced(out_str)
+ self.assertIn('csuite_config', out_str)
+ self.assertIn(package_name, out_str)
+
+ def assert_braces_balanced(self, generated_str: bytes) -> None:
+ """Checks whether all braces in the provided string are balanced."""
+ count = 0
+
+ for c in generated_str:
+ if c == '{':
+ count += 1
+ elif c == '}':
+ count -= 1
+
+ if count < 0:
+ break
+
+ self.assertEqual(count, 0,
+ 'Braces in \'%s\' are not balanced' % generated_str)
+
+
+class ParsePackageListTest(csuite_test.TestCase):
+
+ def test_accepts_empty_lines(self):
+ lines = io.StringIO('\n\n\npackage_name\n\n')
+
+ package_list = generate_module.parse_package_list(lines)
+
+ self.assertListEqual(['package_name'], list(package_list))
+
+ def test_strips_trailing_whitespace(self):
+ lines = io.StringIO(' package_name ')
+
+ package_list = generate_module.parse_package_list(lines)
+
+ self.assertListEqual(['package_name'], list(package_list))
+
+ def test_duplicate_package_name(self):
+ lines = io.StringIO('\n\npackage_name\n\npackage_name\n')
+
+ package_list = generate_module.parse_package_list(lines)
+
+ self.assertListEqual(['package_name'], list(package_list))
+
+ def test_ignore_comment_lines(self):
+ lines = io.StringIO('\n# Comments.\npackage_name\n')
+
+ package_list = generate_module.parse_package_list(lines)
+
+ self.assertListEqual(['package_name'], list(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],
+ out=io.StringIO(),
+ err=io.StringIO())
+
+ 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],
+ out=io.StringIO(),
+ err=io.StringIO())
+
+ def test_test_module_template_file_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'
+ os.makedirs(root_dir)
+ template_file_path = '/test/template.txt'
+
+ with self.assertRaises(SystemExit):
+ generate_module.parse_args([
+ '--package-list', package_list_file_path, '--root-dir', root_dir,
+ '--test', template_file_path
+ ],
+ out=io.StringIO(),
+ err=io.StringIO())
+
+ def test_build_module_template_file_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'
+ os.makedirs(root_dir)
+ template_file_path = '/test/template.txt'
+
+ with self.assertRaises(SystemExit):
+ generate_module.parse_args([
+ '--package-list', package_list_file_path, '--root-dir', root_dir,
+ '--template', template_file_path
+ ],
+ out=io.StringIO(),
+ err=io.StringIO())
+
+
+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/'
+ self.fs.create_file(
+ 'test/existing_package/AndroidTest.xml', contents=_AUTO_GENERATE_NOTE)
+ self.fs.create_file(
+ 'test/existing_package/Android.bp', contents=_AUTO_GENERATE_NOTE)
+
+ generate_module.remove_existing_package_files(root_dir)
+
+ self.assertFalse(os.path.exists(package_dir))
+
+
+if __name__ == '__main__':
+ csuite_test.main()
diff --git a/tools/script/generate_module_unittest.py b/tools/script/generate_module_unittest.py
deleted file mode 100644
index 4d24162..0000000
--- a/tools/script/generate_module_unittest.py
+++ /dev/null
@@ -1,203 +0,0 @@
-#!/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()