aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-05-10 07:17:42 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2022-05-10 07:17:42 +0000
commit00e1bcad2e7eee10f5bca946afa1ae54f5596408 (patch)
tree34a81af095a69c5e94e06bc4bdedd0b0a9238b82
parentb5feecdd78a099d81ac3348e335e73b53fe2ab89 (diff)
parent2e0bc257c2235d2b807187d0652b76412afc559e (diff)
downloadcsuite-00e1bcad2e7eee10f5bca946afa1ae54f5596408.tar.gz
Snap for 8564071 from 2e0bc257c2235d2b807187d0652b76412afc559e to mainline-sdkext-releaseaml_sdk_331111000aml_sdk_330810050aml_sdk_330810010
Change-Id: If9829ebb92eb704950b012bfe5278ca32292fc46
-rw-r--r--TEST_MAPPING (renamed from integration_tests/TEST_MAPPING)2
-rw-r--r--harness/Android.bp2
-rw-r--r--harness/src/main/java/com/android/compatibility/FailureCollectingListener.java58
-rw-r--r--harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java89
-rw-r--r--harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java5
-rw-r--r--harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java449
-rw-r--r--harness/src/main/java/com/android/csuite/config/ModuleGenerator.java257
-rw-r--r--harness/src/main/java/com/android/csuite/core/ApkInstaller.java196
-rw-r--r--harness/src/main/java/com/android/csuite/core/AppCrawlTester.java438
-rw-r--r--harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java194
-rw-r--r--harness/src/main/java/com/android/csuite/core/CommandLineTemplateMappingProvider.java (renamed from harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java)26
-rw-r--r--harness/src/main/java/com/android/csuite/core/DeviceUtils.java477
-rw-r--r--harness/src/main/java/com/android/csuite/core/DirectoryBasedModuleInfoProvider.java108
-rw-r--r--harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java60
-rw-r--r--harness/src/main/java/com/android/csuite/core/FileBasedTemplateMappingProvider.java77
-rw-r--r--harness/src/main/java/com/android/csuite/core/ModuleGenerator.java240
-rw-r--r--harness/src/main/java/com/android/csuite/core/ModuleInfoProvider.java58
-rw-r--r--harness/src/main/java/com/android/csuite/core/ModuleTemplate.java197
-rw-r--r--harness/src/main/java/com/android/csuite/core/PackageModuleInfoProvider.java57
-rw-r--r--harness/src/main/java/com/android/csuite/core/PackagesFileModuleInfoProvider.java79
-rw-r--r--harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java23
-rw-r--r--harness/src/main/java/com/android/csuite/core/TemplateMappingProvider.java (renamed from harness/src/main/java/com/android/csuite/core/PackageNameProvider.java)21
-rw-r--r--harness/src/main/java/com/android/csuite/core/TestUtils.java394
-rw-r--r--harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java98
-rw-r--r--harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java225
-rw-r--r--harness/src/main/resources/config/csuite-base.xml12
-rw-r--r--harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java196
-rw-r--r--harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java3
-rw-r--r--harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java457
-rw-r--r--harness/src/test/java/com/android/csuite/CSuiteUnitTests.java37
-rw-r--r--harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java321
-rw-r--r--harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java94
-rw-r--r--harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java182
-rw-r--r--harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java535
-rw-r--r--harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java45
-rw-r--r--harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java323
-rw-r--r--harness/src/test/java/com/android/csuite/core/DirectoryBasedModuleInfoProviderTest.java181
-rw-r--r--harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java116
-rw-r--r--harness/src/test/java/com/android/csuite/core/FileBasedTemplateMappingProviderTest.java148
-rw-r--r--harness/src/test/java/com/android/csuite/core/ModuleGeneratorTest.java326
-rw-r--r--harness/src/test/java/com/android/csuite/core/ModuleTemplateTest.java284
-rw-r--r--harness/src/test/java/com/android/csuite/core/PackageModuleInfoProviderTest.java152
-rw-r--r--harness/src/test/java/com/android/csuite/core/PackagesFileModuleInfoProviderTest.java213
-rw-r--r--harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java9
-rw-r--r--harness/src/test/java/com/android/csuite/core/TestUtilsTest.java325
-rw-r--r--harness/src/test/java/com/android/csuite/testing/MoreAsserts.java2
-rw-r--r--instrumentation/launch/src/main/AndroidManifest.xml28
-rw-r--r--instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java371
-rw-r--r--instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java22
-rw-r--r--integration_tests/Android.bp6
-rw-r--r--integration_tests/csuite_cli_test.py6
-rw-r--r--integration_tests/csuite_crash_detection_test.py41
-rw-r--r--integration_tests/csuite_test_utils.py29
-rw-r--r--test_scripts/Android.bp (renamed from instrumentation/launch/Android.bp)28
-rw-r--r--test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java105
-rw-r--r--test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java180
-rw-r--r--test_scripts/src/main/java/com/android/pixel/Android.bp47
-rw-r--r--test_scripts/src/main/java/com/android/pixel/AndroidManifest.xml27
-rw-r--r--test_scripts/src/main/java/com/android/pixel/OWNERS2
-rw-r--r--test_scripts/src/main/java/com/android/pixel/README.md4
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java70
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java92
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java80
-rw-r--r--test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java81
-rw-r--r--test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java204
-rw-r--r--test_scripts/src/main/java/com/android/webview/OWNERS2
-rw-r--r--test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java296
-rw-r--r--test_targets/csuite-app-crawl/Android.bp23
-rw-r--r--test_targets/csuite-app-crawl/plan.xml3
-rw-r--r--test_targets/csuite-app-crawl/template.xml27
-rw-r--r--test_targets/csuite-app-launch/Android.bp3
-rw-r--r--test_targets/csuite-app-launch/default.xml (renamed from test_targets/csuite-app-launch/template.xml)10
-rw-r--r--test_targets/csuite-app-launch/pre-installed-apps.xml27
-rw-r--r--test_targets/csuite-pre-installed-app-launch/template.xml6
-rw-r--r--test_targets/csuite-system-app-launch/template.xml5
-rw-r--r--test_targets/csuite-test-package-launch/template.xml12
-rw-r--r--test_targets/drm-app-launch/Android.bp23
-rw-r--r--test_targets/drm-app-launch/plan.xml3
-rw-r--r--test_targets/drm-app-launch/template.xml29
-rw-r--r--test_targets/pixel-app-launch-lock/Android.bp22
-rw-r--r--test_targets/pixel-app-launch-lock/OWNERS2
-rw-r--r--test_targets/pixel-app-launch-lock/template.xml34
-rw-r--r--test_targets/pixel-app-launch-recentapp/Android.bp22
-rw-r--r--test_targets/pixel-app-launch-recentapp/OWNERS2
-rw-r--r--test_targets/pixel-app-launch-recentapp/template.xml34
-rw-r--r--test_targets/pixel-app-launch-rotate/Android.bp22
-rw-r--r--test_targets/pixel-app-launch-rotate/OWNERS2
-rw-r--r--test_targets/pixel-app-launch-rotate/template.xml34
-rw-r--r--test_targets/webview-app-launch/Android.bp22
-rw-r--r--test_targets/webview-app-launch/OWNERS2
-rw-r--r--test_targets/webview-app-launch/default.xml28
-rw-r--r--tools/csuite-tradefed/Android.bp3
-rw-r--r--tools/csuite-tradefed/src/test/java/com/android/csuite/tradefed/CSuiteTradefedTest.java (renamed from tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java)2
-rw-r--r--tools/csuite_test/csuite_test.go71
-rw-r--r--tools/csuite_test/csuite_test_test.go224
-rw-r--r--tools/csuite_test/go.mod16
96 files changed, 7383 insertions, 2842 deletions
diff --git a/integration_tests/TEST_MAPPING b/TEST_MAPPING
index 9d98a72..b21d630 100644
--- a/integration_tests/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "csuite_cli_test"
},
diff --git a/harness/Android.bp b/harness/Android.bp
index 5111c1b..4a1d721 100644
--- a/harness/Android.bp
+++ b/harness/Android.bp
@@ -29,6 +29,8 @@ java_library_host {
],
static_libs: [
"compatibility-tradefed",
+ "platformprotos",
+ "libprotobuf-java-lite",
]
}
diff --git a/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java b/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java
deleted file mode 100644
index d022231..0000000
--- a/harness/src/main/java/com/android/compatibility/FailureCollectingListener.java
+++ /dev/null
@@ -1,58 +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 com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.TestDescription;
-
-public final class FailureCollectingListener implements ITestInvocationListener {
- private String mTestTrace = null;
-
- @Override
- public void testFailed(TestDescription test, String trace) {
- setStackTrace(trace != null ? trace : "unknown failure");
- }
-
- @Override
- public void testAssumptionFailure(TestDescription test, String trace) {
- setStackTrace(trace != null ? trace : "unknown assumption failure");
- }
-
- /** {@inheritDoc} */
- @Override
- public void testRunFailed(String errorMessage) {
- setStackTrace(errorMessage);
- }
-
- /**
- * Fetches the stack trace if any.
- *
- * @return the stack trace.
- */
- public String getStackTrace() {
- return mTestTrace;
- }
-
- /**
- * Sets the stack trace.
- *
- * @param stackTrace {@link String} stack trace to set.
- */
- public void setStackTrace(String stackTrace) {
- this.mTestTrace = stackTrace;
- }
-}
diff --git a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
index 12e30f7..0002fbe 100644
--- a/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
+++ b/harness/src/main/java/com/android/compatibility/targetprep/AppSetupPreparer.java
@@ -19,19 +19,23 @@ package com.android.compatibility.targetprep;
import static com.google.common.base.Preconditions.checkArgument;
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.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestLoggerReceiver;
+import com.android.tradefed.result.LogDataType;
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.android.tradefed.util.ZipUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.SimpleTimeLimiter;
@@ -39,6 +43,7 @@ import com.google.common.util.concurrent.TimeLimiter;
import com.google.common.util.concurrent.UncheckedTimeoutException;
import java.io.File;
+import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@@ -46,7 +51,7 @@ 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 final class AppSetupPreparer implements ITargetPreparer, ITestLoggerReceiver {
@VisibleForTesting
static final String OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS =
@@ -62,6 +67,11 @@ public final class AppSetupPreparer implements ITargetPreparer {
@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";
+ @VisibleForTesting static final String OPTION_INCREMENTAL_FILTER = "incremental-block-filter";
+ @VisibleForTesting static final String OPTION_SAVE_APKS = "save-apks";
+
+ @VisibleForTesting
+ static final String OPTION_INCREMENTAL_TIMEOUT_SECS = "incremental-install-timeout-secs";
@Option(name = "package-name", description = "Package name of testing app.")
private String mPackageName;
@@ -86,6 +96,16 @@ public final class AppSetupPreparer implements ITargetPreparer {
description = "Enable packages to be installed incrementally.")
private boolean mIncrementalInstallation = false;
+ @Option(
+ name = OPTION_INCREMENTAL_FILTER,
+ description = "Specify percentage of blocks to filter.")
+ private double mBlockFilterPercentage = 0.0;
+
+ @Option(
+ name = OPTION_INCREMENTAL_TIMEOUT_SECS,
+ description = "Specify timeout of incremental installation.")
+ private int mIncrementalTimeout = 1800;
+
@Option(name = OPTION_MAX_RETRY, description = "Max number of retries upon TargetSetupError.")
private int mMaxRetry = 0;
@@ -112,10 +132,16 @@ public final class AppSetupPreparer implements ITargetPreparer {
+ "be applied to each retry attempt.")
private long mSetupOnceTimeoutMillis = TimeUnit.MINUTES.toMillis(10);
+ @Option(
+ name = OPTION_SAVE_APKS,
+ description = "Whether to save the input APKs into test output.")
+ private boolean mSaveApks = false;
+
private final TestAppInstallSetup mTestAppInstallSetup;
private final Sleeper mSleeper;
private final TimeLimiter mTimeLimiter =
SimpleTimeLimiter.create(Executors.newCachedThreadPool());
+ private ITestLogger mTestLogger;
public AppSetupPreparer() {
this(new TestAppInstallSetup(), Sleepers.DefaultSleeper.INSTANCE);
@@ -129,7 +155,7 @@ public final class AppSetupPreparer implements ITargetPreparer {
/** {@inheritDoc} */
@Override
- public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ public void setUp(TestInformation testInfo)
throws DeviceNotAvailableException, BuildError, TargetSetupError {
checkArgumentNonNegative(mMaxRetry, OPTION_MAX_RETRY);
checkArgumentNonNegative(
@@ -137,6 +163,27 @@ public final class AppSetupPreparer implements ITargetPreparer {
OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS);
checkArgumentNonNegative(mSetupOnceTimeoutMillis, OPTION_SETUP_TIMEOUT_MILLIS);
+ if (mSaveApks) {
+ mTestFiles.forEach(
+ path -> {
+ if (!path.exists()) {
+ CLog.w(
+ "Skipping saving %s as the path might be a relative path.",
+ path);
+ return;
+ }
+ try {
+ File outputZip = ZipUtil.createZip(path);
+ mTestLogger.testLog(
+ mPackageName + "-input_apk-" + path.getName(),
+ LogDataType.ZIP,
+ new FileInputStreamSource(outputZip));
+ } catch (IOException e) {
+ CLog.e("Failed to zip the output directory: " + e);
+ }
+ });
+ }
+
int runCount = 0;
while (true) {
TargetSetupError currentException;
@@ -147,25 +194,27 @@ public final class AppSetupPreparer implements ITargetPreparer {
mTimeLimiter.newProxy(
new ITargetPreparer() {
@Override
- public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ public void setUp(TestInformation testInfo)
throws DeviceNotAvailableException, BuildError,
TargetSetupError {
- setUpOnce(device, buildInfo);
+ setUpOnce(testInfo);
}
},
ITargetPreparer.class,
mSetupOnceTimeoutMillis,
TimeUnit.MILLISECONDS);
- handler.setUp(device, buildInfo);
+ handler.setUp(testInfo);
break;
} catch (TargetSetupError e) {
currentException = e;
} catch (UncheckedTimeoutException e) {
- currentException = new TargetSetupError(e.getMessage(), e);
+ currentException =
+ new TargetSetupError(
+ e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
}
- waitForDeviceAvailable(device);
+ waitForDeviceAvailable(testInfo.getDevice());
if (runCount > mMaxRetry) {
throw currentException;
}
@@ -177,27 +226,34 @@ public final class AppSetupPreparer implements ITargetPreparer {
(int) Math.pow(mExponentialBackoffMultiplierSeconds, runCount)));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- throw new TargetSetupError(e.getMessage(), e);
+ throw new TargetSetupError(
+ e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
}
}
}
- private void setUpOnce(ITestDevice device, IBuildInfo buildInfo)
+ private void setUpOnce(TestInformation testInfo)
throws DeviceNotAvailableException, BuildError, TargetSetupError {
mTestAppInstallSetup.setAaptVersion(mAaptVersion);
try {
OptionSetter setter = new OptionSetter(mTestAppInstallSetup);
setter.setOptionValue("incremental", String.valueOf(mIncrementalInstallation));
+ setter.setOptionValue(
+ "incremental-block-filter", String.valueOf(mBlockFilterPercentage));
+ setter.setOptionValue(
+ "incremental-install-timeout-secs", String.valueOf(mIncrementalTimeout));
} catch (ConfigurationException e) {
- throw new TargetSetupError(e.getMessage(), e);
+ throw new TargetSetupError(
+ e.getMessage(), e, testInfo.getDevice().getDeviceDescriptor());
}
if (mPackageName != null) {
- SystemPackageUninstaller.uninstallPackage(mPackageName, device);
+ SystemPackageUninstaller.uninstallPackage(mPackageName, testInfo.getDevice());
}
for (File testFile : mTestFiles) {
+ CLog.d("Adding apk path %s for installation.", testFile);
mTestAppInstallSetup.addTestFile(testFile);
}
@@ -205,7 +261,7 @@ public final class AppSetupPreparer implements ITargetPreparer {
mTestAppInstallSetup.addInstallArg(installArg);
}
- mTestAppInstallSetup.setUp(device, buildInfo);
+ mTestAppInstallSetup.setUp(testInfo);
}
/** {@inheritDoc} */
@@ -231,7 +287,7 @@ public final class AppSetupPreparer implements ITargetPreparer {
void sleep(Duration duration) throws InterruptedException;
}
- static class Sleepers {
+ private static class Sleepers {
enum DefaultSleeper implements Sleeper {
INSTANCE;
@@ -243,4 +299,9 @@ public final class AppSetupPreparer implements ITargetPreparer {
private Sleepers() {}
}
+
+ @Override
+ public void setTestLogger(ITestLogger testLogger) {
+ mTestLogger = testLogger;
+ }
}
diff --git a/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java b/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java
index 8d40d04..4e4d565 100644
--- a/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java
+++ b/harness/src/main/java/com/android/compatibility/targetprep/CheckGmsPreparer.java
@@ -20,6 +20,7 @@ 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.result.error.DeviceErrorIdentifier;
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.util.CommandResult;
@@ -57,7 +58,9 @@ public final class CheckGmsPreparer implements ITargetPreparer {
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");
+ "GMS required but did not detect a running GMS process after device reboot",
+ testInfo.getDevice().getDeviceDescriptor(),
+ DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
}
}
diff --git a/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java b/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
deleted file mode 100644
index 7a52735..0000000
--- a/harness/src/main/java/com/android/compatibility/testtype/AppLaunchTest.java
+++ /dev/null
@@ -1,449 +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.testtype;
-
-import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.android.compatibility.FailureCollectingListener;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.IConfigurationReceiver;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.LogcatReceiver;
-import com.android.tradefed.invoker.TestInformation;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ByteArrayInputStreamSource;
-import com.android.tradefed.result.CompatibilityTestResult;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.InputStreamSource;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.InstrumentationTest;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.StreamUtil;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
-
-import org.json.JSONException;
-import org.junit.Assert;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
-
-/** A test that verifies that a single app can be successfully launched. */
-public class AppLaunchTest
- implements IDeviceTest, IRemoteTest, IConfigurationReceiver, ITestFilterReceiver {
- @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;
-
- @Option(name = "test-label", description = "Unique test identifier label.")
- private String mTestLabel = "AppCompatibility";
-
- /** @deprecated */
- @Deprecated
- @Option(
- name = "retry-count",
- description = "Number of times to retry a failed test case. 0 means no retry.")
- private int mRetryCount = 0;
-
- @Option(name = "include-filter", description = "The include filter of the test type.")
- protected Set<String> mIncludeFilters = new HashSet<>();
-
- @Option(name = "exclude-filter", description = "The exclude filter of the test type.")
- protected Set<String> mExcludeFilters = new HashSet<>();
-
- @Option(name = "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.")
- private int mAppLaunchTimeoutMs = 15000;
-
- private static final String LAUNCH_TEST_RUNNER =
- "com.android.compatibilitytest.AppCompatibilityRunner";
- private static final String LAUNCH_TEST_PACKAGE = "com.android.compatibilitytest";
- private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
- private static final String 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() {
- this(null);
- }
-
- @VisibleForTesting
- public AppLaunchTest(String packageName) {
- this(packageName, 0);
- }
-
- @VisibleForTesting
- public AppLaunchTest(String packageName, int retryCount) {
- mPackageName = packageName;
- mRetryCount = retryCount;
- }
-
- /**
- * Creates and sets up an instrumentation test with information about the test runner as well as
- * the package being tested (provided as a parameter).
- */
- protected InstrumentationTest createInstrumentationTest(String packageBeingTested) {
- InstrumentationTest 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));
-
- int testTimeoutMs = BASE_INSTRUMENTATION_TEST_TIMEOUT_MS + mAppLaunchTimeoutMs * 2;
- instrumentationTest.setShellTimeout(testTimeoutMs);
- instrumentationTest.setTestTimeout(testTimeoutMs);
-
- return instrumentationTest;
- }
-
- /*
- * {@inheritDoc}
- */
- @Override
- public void run(final TestInformation testInfo, final ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- CLog.d("Start of run method.");
- CLog.d("Include filters: %s", mIncludeFilters);
- CLog.d("Exclude filters: %s", mExcludeFilters);
-
- Assert.assertNotNull("Package name cannot be null", mPackageName);
-
- TestDescription testDescription = createTestDescription();
-
- if (!inFilter(testDescription.toString())) {
- CLog.d("Test case %s doesn't match any filter", testDescription);
- return;
- }
- CLog.d("Complete filtering test case: %s", testDescription);
-
- long start = System.currentTimeMillis();
- listener.testRunStarted(mTestLabel, 1);
- mLogcat = new LogcatReceiver(getDevice(), LOGCAT_SIZE_BYTES, 0);
- mLogcat.start();
-
- try {
- testPackage(testInfo, testDescription, listener);
- } catch (InterruptedException e) {
- CLog.e(e);
- throw new RuntimeException(e);
- } finally {
- mLogcat.stop();
- listener.testRunEnded(
- System.currentTimeMillis() - start, new HashMap<String, Metric>());
- }
- }
-
- /**
- * Attempts to test a package and reports the results.
- *
- * @param listener The {@link ITestInvocationListener}.
- * @throws DeviceNotAvailableException
- */
- private void testPackage(
- final TestInformation testInfo,
- TestDescription testDescription,
- ITestInvocationListener listener)
- throws DeviceNotAvailableException, InterruptedException {
- CLog.d("Started testing package: %s.", mPackageName);
-
- listener.testStarted(testDescription, System.currentTimeMillis());
-
- CompatibilityTestResult result = createCompatibilityTestResult();
- result.packageName = mPackageName;
-
- try {
- for (int i = 0; i <= mRetryCount; i++) {
- result.status = null;
- result.message = null;
- // Clear test result between retries.
- launchPackage(testInfo, result);
- if (result.status == CompatibilityTestResult.STATUS_SUCCESS) {
- 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 {
- reportResult(listener, testDescription, result);
- stopPackage();
- try {
- postLogcat(result, listener);
- } catch (JSONException e) {
- CLog.w("Posting failed: %s.", e.getMessage());
- }
- listener.testEnded(
- testDescription,
- System.currentTimeMillis(),
- Collections.<String, String>emptyMap());
-
- CLog.d("Completed testing package: %s.", mPackageName);
- }
- }
-
- /**
- * Method which attempts to launch a package.
- *
- * <p>Will set the result status to success if the package could be launched. Otherwise the
- * result status will be set to failure.
- *
- * @param result the {@link CompatibilityTestResult} containing the package info.
- * @throws DeviceNotAvailableException
- */
- private void launchPackage(final TestInformation testInfo, CompatibilityTestResult result)
- throws DeviceNotAvailableException {
- CLog.d("Launching package: %s.", result.packageName);
-
- CommandResult resetResult = resetPackage();
- if (resetResult.getStatus() != CommandStatus.SUCCESS) {
- result.status = CompatibilityTestResult.STATUS_ERROR;
- result.message = resetResult.getStatus() + resetResult.getStderr();
- return;
- }
-
- InstrumentationTest instrTest = createInstrumentationTest(result.packageName);
-
- FailureCollectingListener failureListener = createFailureListener();
- instrTest.run(testInfo, failureListener);
- CLog.d("Stack Trace: %s", failureListener.getStackTrace());
-
- if (failureListener.getStackTrace() != null) {
- CLog.w("Failed to launch package: %s.", result.packageName);
- result.status = CompatibilityTestResult.STATUS_FAILURE;
- result.message = failureListener.getStackTrace();
- } else {
- result.status = CompatibilityTestResult.STATUS_SUCCESS;
- }
-
- CLog.d("Completed launching package: %s", result.packageName);
- }
-
- /** Helper method which reports a test failed if the status is either a failure or an error. */
- private void reportResult(
- ITestInvocationListener listener, TestDescription id, CompatibilityTestResult result) {
- String message = result.message != null ? result.message : "unknown";
- String tag = errorStatusToTag(result.status);
- if (tag != null) {
- listener.testFailed(id, result.status + ":" + message);
- }
- }
-
- private String errorStatusToTag(String status) {
- if (status.equals(CompatibilityTestResult.STATUS_ERROR)) {
- return "ERROR";
- }
- if (status.equals(CompatibilityTestResult.STATUS_FAILURE)) {
- return "FAILURE";
- }
- return null;
- }
-
- /** Helper method which posts the logcat. */
- private void postLogcat(CompatibilityTestResult result, ITestInvocationListener listener)
- throws JSONException {
- InputStreamSource stream = null;
- String header =
- String.format(
- "%s%s%s\n",
- CompatibilityTestResult.SEPARATOR,
- result.toJsonString(),
- CompatibilityTestResult.SEPARATOR);
-
- try (InputStreamSource logcatData = mLogcat.getLogcatData()) {
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- baos.write(header.getBytes());
- StreamUtil.copyStreams(logcatData.createInputStream(), baos);
- stream = new ByteArrayInputStreamSource(baos.toByteArray());
- } catch (IOException e) {
- CLog.e("error inserting compatibility test result into logcat");
- CLog.e(e);
- // fallback to logcat data
- stream = logcatData;
- }
- listener.testLog("logcat_" + result.packageName, LogDataType.LOGCAT, stream);
- } finally {
- StreamUtil.cancel(stream);
- }
- }
-
- /**
- * Return true if a test matches one or more of the include filters AND does not match any of
- * the exclude filters. If no include filters are given all tests should return true as long as
- * they do not match any of the exclude filters.
- */
- protected boolean inFilter(String testName) {
- if (mExcludeFilters.contains(testName)) {
- return false;
- }
- if (mIncludeFilters.size() == 0 || mIncludeFilters.contains(testName)) {
- return true;
- }
- return false;
- }
-
- protected CommandResult resetPackage() throws DeviceNotAvailableException {
- return mDevice.executeShellV2Command(String.format("pm clear %s", mPackageName));
- }
-
- private void stopPackage() throws DeviceNotAvailableException {
- mDevice.executeShellCommand(String.format("am force-stop %s", mPackageName));
- }
-
- @Override
- public void setConfiguration(IConfiguration configuration) {
- mConfiguration = configuration;
- }
-
- /*
- * {@inheritDoc}
- */
- @Override
- public void setDevice(ITestDevice device) {
- mDevice = device;
- }
-
- /*
- * {@inheritDoc}
- */
- @Override
- public ITestDevice getDevice() {
- return mDevice;
- }
-
- public int getmRetryCount() {
- return mRetryCount;
- }
-
- /**
- * Get a test description for use in logging. For compatibility with logs, this should be
- * TestDescription(test class name, test type).
- */
- private TestDescription createTestDescription() {
- return new TestDescription(getClass().getSimpleName(), mPackageName);
- }
-
- /** Get a FailureCollectingListener for failure listening. */
- private FailureCollectingListener createFailureListener() {
- return new FailureCollectingListener();
- }
-
- /**
- * Get a CompatibilityTestResult for encapsulating compatibility run results for a single app
- * package tested.
- */
- private CompatibilityTestResult createCompatibilityTestResult() {
- return new CompatibilityTestResult();
- }
-
- /** {@inheritDoc} */
- @Override
- public void addIncludeFilter(String filter) {
- checkArgument(!Strings.isNullOrEmpty(filter), "Include filter cannot be null or empty.");
- mIncludeFilters.add(filter);
- }
-
- /** {@inheritDoc} */
- @Override
- public void addAllIncludeFilters(Set<String> filters) {
- checkNotNull(filters, "Include filters cannot be null.");
- mIncludeFilters.addAll(filters);
- }
-
- /** {@inheritDoc} */
- @Override
- public void clearIncludeFilters() {
- mIncludeFilters.clear();
- }
-
- /** {@inheritDoc} */
- @Override
- public Set<String> getIncludeFilters() {
- return Collections.unmodifiableSet(mIncludeFilters);
- }
-
- /** {@inheritDoc} */
- @Override
- public void addExcludeFilter(String filter) {
- checkArgument(!Strings.isNullOrEmpty(filter), "Exclude filter cannot be null or empty.");
- mExcludeFilters.add(filter);
- }
-
- /** {@inheritDoc} */
- @Override
- public void addAllExcludeFilters(Set<String> filters) {
- checkNotNull(filters, "Exclude filters cannot be null.");
- mExcludeFilters.addAll(filters);
- }
-
- /** {@inheritDoc} */
- @Override
- public void clearExcludeFilters() {
- mExcludeFilters.clear();
- }
-
- /** {@inheritDoc} */
- @Override
- public Set<String> getExcludeFilters() {
- return Collections.unmodifiableSet(mExcludeFilters);
- }
-}
diff --git a/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java b/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java
deleted file mode 100644
index 0760085..0000000
--- a/harness/src/main/java/com/android/csuite/config/ModuleGenerator.java
+++ /dev/null
@@ -1,257 +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.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/ApkInstaller.java b/harness/src/main/java/com/android/csuite/core/ApkInstaller.java
new file mode 100644
index 0000000..2882164
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/ApkInstaller.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import com.android.csuite.core.TestUtils.TestUtilsException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** A utility class to install APKs. */
+public final class ApkInstaller {
+ private static long sCommandTimeOut = TimeUnit.MINUTES.toMillis(4);
+ private final String mDeviceSerial;
+ private final List<Path> mInstalledBaseApks = new ArrayList<>();
+ private final IRunUtil mRunUtil;
+ private final PackageNameParser mPackageNameParser;
+
+ public static ApkInstaller getInstance(ITestDevice device) {
+ return getInstance(device.getSerialNumber());
+ }
+
+ public static ApkInstaller getInstance(String deviceSerial) {
+ return new ApkInstaller(deviceSerial, new RunUtil(), new AaptPackageNameParser());
+ }
+
+ @VisibleForTesting
+ ApkInstaller(String deviceSerial, IRunUtil runUtil, PackageNameParser packageNameParser) {
+ mDeviceSerial = deviceSerial;
+ mRunUtil = runUtil;
+ mPackageNameParser = packageNameParser;
+ }
+
+ /**
+ * Installs a package.
+ *
+ * @param apkPath Path to the apk files. Only accept file/directory path containing a single APK
+ * or split APK files for one package.
+ * @param args Install args for the 'adb install-multiple' command.
+ * @throws ApkInstallerException If the installation failed.
+ * @throws IOException If an IO exception occurred.
+ */
+ public void install(Path apkPath, String... args) throws ApkInstallerException, IOException {
+ List<Path> apkFilePaths;
+ try {
+ apkFilePaths = TestUtils.listApks(apkPath);
+ } catch (TestUtilsException e) {
+ throw new ApkInstallerException("Failed to list APK files from the path " + apkPath, e);
+ }
+
+ CLog.d("Installing a package from " + apkPath);
+
+ String[] cmd = createInstallCommand(apkFilePaths, mDeviceSerial, args);
+
+ CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd);
+ if (res.getStatus() != CommandStatus.SUCCESS) {
+ throw new ApkInstallerException(
+ String.format(
+ "Failed to install APKs from the path %s: %s",
+ apkPath, res.toString()));
+ }
+
+ mInstalledBaseApks.add(apkFilePaths.get(0));
+
+ CLog.i("Successfully installed " + apkPath);
+ }
+
+ /**
+ * Attempts to uninstall all the installed packages.
+ *
+ * <p>When failed to uninstall one of the installed packages, this method will still attempt to
+ * uninstall all other packages before throwing an exception.
+ *
+ * @throws ApkInstallerException when failed to uninstall a package.
+ */
+ public void uninstallAllInstalledPackages() throws ApkInstallerException {
+ StringBuilder errorMessage = new StringBuilder();
+ mInstalledBaseApks.forEach(
+ baseApk -> {
+ String packageName;
+ try {
+ packageName = mPackageNameParser.parsePackageName(baseApk);
+ } catch (IOException e) {
+ errorMessage.append(
+ String.format(
+ "Failed to parse the package name from %s. Reason: %s.\n",
+ baseApk, e.getMessage()));
+ return;
+ }
+
+ String[] cmd =
+ new String[] {"adb", "-s", mDeviceSerial, "uninstall", packageName};
+
+ CommandResult res = mRunUtil.runTimedCmd(sCommandTimeOut, cmd);
+ if (res.getStatus() != CommandStatus.SUCCESS) {
+ errorMessage.append(
+ String.format(
+ "Failed to uninstall package %s from %s. Reason: %s.\n",
+ packageName, baseApk, res.toString()));
+ }
+ });
+
+ if (errorMessage.length() > 0) {
+ throw new ApkInstallerException(errorMessage.toString());
+ }
+ }
+
+ private String[] createInstallCommand(
+ List<Path> apkFilePaths, String deviceSerial, String[] args) {
+ ArrayList<String> cmd = new ArrayList<>();
+ cmd.addAll(Arrays.asList("adb", "-s", deviceSerial, "install-multiple"));
+
+ cmd.addAll(Arrays.asList(args));
+
+ apkFilePaths.stream().map(Path::toString).forEach(cmd::add);
+
+ return cmd.toArray(new String[cmd.size()]);
+ }
+
+ /** An exception class representing ApkInstaller error. */
+ public static final class ApkInstallerException extends Exception {
+ /**
+ * Constructs a new {@link ApkInstallerException} with a meaningful error message.
+ *
+ * @param message A error message describing the cause of the error.
+ */
+ private ApkInstallerException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@link ApkInstallerException} with a meaningful error message, and a
+ * cause.
+ *
+ * @param message A detailed error message.
+ * @param cause A {@link Throwable} capturing the original cause of the {@link
+ * ApkInstallerException}.
+ */
+ private ApkInstallerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new {@link ApkInstallerException} with a cause.
+ *
+ * @param cause A {@link Throwable} capturing the original cause of the {@link
+ * ApkInstallerException}.
+ */
+ private ApkInstallerException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ private static final class AaptPackageNameParser implements PackageNameParser {
+ @Override
+ public String parsePackageName(Path apkFile) throws IOException {
+ String packageName = AaptParser.parse(apkFile.toFile()).getPackageName();
+ if (packageName == null) {
+ throw new IOException(
+ String.format("Failed to parse package name with AAPT for %s", apkFile));
+ }
+ return packageName;
+ }
+ }
+
+ @VisibleForTesting
+ interface PackageNameParser {
+ String parsePackageName(Path apkFile) throws IOException;
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java b/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java
new file mode 100644
index 0000000..96785ef
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/AppCrawlTester.java
@@ -0,0 +1,438 @@
+/*
+ * 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.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.ZipUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.io.MoreFiles;
+
+import org.junit.Assert;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** A tester that interact with an app crawler during testing. */
+public final class AppCrawlTester {
+ @VisibleForTesting Path mOutput;
+ private final RunUtilProvider mRunUtilProvider;
+ private final TestUtils mTestUtils;
+ private final String mPackageName;
+ private static final long COMMAND_TIMEOUT_MILLIS = 4 * 60 * 1000;
+ private boolean mRecordScreen = false;
+ private boolean mCollectGmsVersion = false;
+ private boolean mCollectAppVersion = false;
+ private boolean mUiAutomatorMode = false;
+ private Path mApkRoot;
+
+ /**
+ * Creates an {@link AppCrawlTester} instance.
+ *
+ * @param packageName The package name of the apk files.
+ * @param testInformation The TradeFed test information.
+ * @param testLogData The TradeFed test output receiver.
+ * @return an {@link AppCrawlTester} instance.
+ */
+ public static AppCrawlTester newInstance(
+ String packageName,
+ TestInformation testInformation,
+ TestLogData testLogData) {
+ return new AppCrawlTester(
+ packageName,
+ TestUtils.getInstance(testInformation, testLogData),
+ () -> new RunUtil());
+ }
+
+ @VisibleForTesting
+ AppCrawlTester(
+ String packageName,
+ TestUtils testUtils,
+ RunUtilProvider runUtilProvider) {
+ mRunUtilProvider = runUtilProvider;
+ mPackageName = packageName;
+ mTestUtils = testUtils;
+ }
+
+ /** An exception class representing crawler test failures. */
+ public static final class CrawlerException extends Exception {
+ /**
+ * Constructs a new {@link CrawlerException} with a meaningful error message.
+ *
+ * @param message A error message describing the cause of the error.
+ */
+ private CrawlerException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@link CrawlerException} with a meaningful error message, and a cause.
+ *
+ * @param message A detailed error message.
+ * @param cause A {@link Throwable} capturing the original cause of the CrawlerException.
+ */
+ private CrawlerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new {@link CrawlerException} with a cause.
+ *
+ * @param cause A {@link Throwable} capturing the original cause of the CrawlerException.
+ */
+ private CrawlerException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ /**
+ * Starts crawling the app and throw AssertionError if app crash is detected.
+ *
+ * @throws DeviceNotAvailableException When device because unavailable.
+ */
+ public void startAndAssertAppNoCrash() throws DeviceNotAvailableException {
+ DeviceTimestamp startTime = mTestUtils.getDeviceUtils().currentTimeMillis();
+
+ CrawlerException crawlerException = null;
+ try {
+ start();
+ } catch (CrawlerException e) {
+ crawlerException = e;
+ }
+
+ ArrayList<String> failureMessages = new ArrayList<>();
+
+ try {
+ String dropboxCrashLog =
+ mTestUtils.getDropboxPackageCrashLog(mPackageName, startTime, true);
+ if (dropboxCrashLog != null) {
+ // Put dropbox crash log on the top of the failure messages.
+ failureMessages.add(dropboxCrashLog);
+ }
+ } catch (IOException e) {
+ failureMessages.add("Error while getting dropbox crash log: " + e.getMessage());
+ }
+
+ if (crawlerException != null) {
+ failureMessages.add(crawlerException.getMessage());
+ }
+
+ Assert.assertTrue(
+ String.join(
+ "\n============\n",
+ failureMessages.toArray(new String[failureMessages.size()])),
+ failureMessages.isEmpty());
+ }
+
+ /**
+ * Starts a crawler run on the configured app.
+ *
+ * @throws CrawlerException When the crawler was not set up correctly or the crawler run command
+ * failed.
+ * @throws DeviceNotAvailableException When device because unavailable.
+ */
+ public void start() throws CrawlerException, DeviceNotAvailableException {
+ if (!AppCrawlTesterHostPreparer.isReady(mTestUtils.getTestInformation())) {
+ throw new CrawlerException(
+ "The "
+ + AppCrawlTesterHostPreparer.class.getName()
+ + " is not ready. Please check whether "
+ + AppCrawlTesterHostPreparer.class.getName()
+ + " was included in the test plan and completed successfully.");
+ }
+
+ if (mOutput != null) {
+ throw new CrawlerException(
+ "The crawler has already run. Multiple runs in the same "
+ + AppCrawlTester.class.getName()
+ + " instance are not supported.");
+ }
+
+ try {
+ mOutput = Files.createTempDirectory("crawler");
+ } catch (IOException e) {
+ throw new CrawlerException("Failed to create temp directory for output.", e);
+ }
+
+ String[] command = createCrawlerRunCommand(mTestUtils.getTestInformation());
+
+ CLog.d("Launching package: %s.", mPackageName);
+
+ IRunUtil runUtil = mRunUtilProvider.get();
+
+ AtomicReference<CommandResult> commandResult = new AtomicReference<>();
+ runUtil.setEnvVariable(
+ "GOOGLE_APPLICATION_CREDENTIALS",
+ AppCrawlTesterHostPreparer.getCredentialPath(mTestUtils.getTestInformation())
+ .toString());
+
+ if (mCollectGmsVersion) {
+ mTestUtils.collectGmsVersion(mPackageName);
+ }
+
+ if (mRecordScreen) {
+ mTestUtils.collectScreenRecord(
+ () -> {
+ commandResult.set(runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, command));
+ },
+ mPackageName);
+ } else {
+ commandResult.set(runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, command));
+ }
+
+ // Must be done after the crawler run because the app is installed by the crawler.
+ if (mCollectAppVersion) {
+ mTestUtils.collectAppVersion(mPackageName);
+ }
+
+ collectOutputZip();
+ collectCrawlStepScreenshots();
+
+ if (!commandResult.get().getStatus().equals(CommandStatus.SUCCESS)) {
+ throw new CrawlerException("Crawler command failed: " + commandResult.get());
+ }
+
+ CLog.i("Completed crawling the package %s. Outputs: %s", mPackageName, commandResult.get());
+ }
+
+ /** Copys the step screenshots into test outputs for easier access. */
+ private void collectCrawlStepScreenshots() {
+ if (mOutput == null) {
+ CLog.e("Output directory is not created yet. Skipping collecting step screenshots.");
+ return;
+ }
+
+ Path subDir = mOutput.resolve("app_firebase_test_lab");
+ if (!Files.exists(subDir)) {
+ CLog.e(
+ "The crawler output directory is not complete, skipping collecting step"
+ + " screenshots.");
+ return;
+ }
+
+ try (Stream<Path> files = Files.list(subDir)) {
+ files.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".png"))
+ .forEach(
+ path -> {
+ mTestUtils
+ .getTestArtifactReceiver()
+ .addTestArtifact(
+ mPackageName
+ + "-crawl_step_screenshot_"
+ + path.getFileName(),
+ LogDataType.PNG,
+ path.toFile());
+ });
+ } catch (IOException e) {
+ CLog.e(e);
+ }
+ }
+
+ /** Puts the zipped crawler output files into test output. */
+ private void collectOutputZip() {
+ if (mOutput == null) {
+ CLog.e("Output directory is not created yet. Skipping collecting output.");
+ return;
+ }
+
+ // Compress the crawler output directory and add it to test outputs.
+ try {
+ File outputZip = ZipUtil.createZip(mOutput.toFile());
+ mTestUtils
+ .getTestArtifactReceiver()
+ .addTestArtifact(mPackageName + "-crawler_output", LogDataType.ZIP, outputZip);
+ } catch (IOException e) {
+ CLog.e("Failed to zip the output directory: " + e);
+ }
+ }
+
+ /**
+ * Generates a list of APK paths where the base.apk of split apk files are always on the first
+ * index if exists.
+ *
+ * <p>If the apk path is a single apk, then the apk is returned. If the apk path is a directory
+ * containing only one non-split apk file, the apk file is returned. If the apk path is a
+ * directory containing split apk files for one package, then the list of apks are returned and
+ * the base.apk sits on the first index. If the apk path does not contain any apk files, or
+ * multiple apk files without base.apk, then an IOException is thrown.
+ *
+ * @return A list of APK paths.
+ * @throws CrawlerException If failed to read the apk path or unexpected number of apk files are
+ * found under the path.
+ */
+ private static List<Path> getApks(Path root) throws CrawlerException {
+ // The apk path points to a non-split apk file.
+ if (Files.isRegularFile(root)) {
+ if (!root.toString().endsWith(".apk")) {
+ throw new CrawlerException(
+ "The file on the given apk path is not an apk file: " + root);
+ }
+ return List.of(root);
+ }
+
+ List<Path> apks;
+ CLog.d("APK path = " + root);
+ try (Stream<Path> fileTree = Files.walk(root)) {
+ apks =
+ fileTree.filter(Files::isRegularFile)
+ .filter(path -> path.getFileName().toString().endsWith(".apk"))
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ throw new CrawlerException("Failed to list apk files.", e);
+ }
+
+ if (apks.isEmpty()) {
+ throw new CrawlerException("The apk directory does not contain any apk files");
+ }
+
+ // The apk path contains a single non-split apk or the base.apk of a split-apk.
+ if (apks.size() == 1) {
+ return apks;
+ }
+
+ if (apks.stream().map(path -> path.getParent().toString()).distinct().count() != 1) {
+ throw new CrawlerException(
+ "Apk files are not all in the same folder: "
+ + Arrays.deepToString(apks.toArray(new Path[apks.size()])));
+ }
+
+ if (apks.stream().filter(path -> path.getFileName().toString().equals("base.apk")).count()
+ == 0) {
+ throw new CrawlerException(
+ "Multiple non-split apk files detected: "
+ + Arrays.deepToString(apks.toArray(new Path[apks.size()])));
+ }
+
+ Collections.sort(
+ apks,
+ (first, second) -> first.getFileName().toString().equals("base.apk") ? -1 : 0);
+
+ return apks;
+ }
+
+ @VisibleForTesting
+ String[] createCrawlerRunCommand(TestInformation testInfo) throws CrawlerException {
+
+ ArrayList<String> cmd = new ArrayList<>();
+ cmd.addAll(
+ Arrays.asList(
+ "java",
+ "-jar",
+ AppCrawlTesterHostPreparer.getCrawlerBinPath(testInfo)
+ .resolve("crawl_launcher_deploy.jar")
+ .toString(),
+ "--android-sdk-path",
+ AppCrawlTesterHostPreparer.getSdkPath(testInfo).toString(),
+ "--device-serial-code",
+ testInfo.getDevice().getSerialNumber(),
+ "--output-dir",
+ mOutput.toString(),
+ "--key-store-file",
+ // Using the publicly known default file name of the debug keystore.
+ AppCrawlTesterHostPreparer.getCrawlerBinPath(testInfo)
+ .resolve("debug.keystore")
+ .toString(),
+ "--key-store-password",
+ // Using the publicly known default password of the debug keystore.
+ "android"));
+
+ if (mUiAutomatorMode) {
+ cmd.addAll(Arrays.asList("--ui-automator-mode", "--app-package-name", mPackageName));
+ } else {
+ Preconditions.checkNotNull(
+ mApkRoot, "Apk file path is required when not running in UIAutomator mode");
+
+ List<Path> apks = getApks(mApkRoot);
+
+ cmd.add("--apk-file");
+ cmd.add(apks.get(0).toString());
+
+ for (int i = 1; i < apks.size(); i++) {
+ cmd.add("--split-apk-files");
+ cmd.add(apks.get(i).toString());
+ }
+ }
+
+ return cmd.toArray(new String[cmd.size()]);
+ }
+
+ /** Cleans up the crawler output directory. */
+ public void cleanUp() {
+ if (mOutput == null) {
+ return;
+ }
+
+ try {
+ MoreFiles.deleteRecursively(mOutput);
+ } catch (IOException e) {
+ CLog.e("Failed to clean up the crawler output directory: " + e);
+ }
+ }
+
+ /** Sets the option of whether to record the device screen during crawling. */
+ public void setRecordScreen(boolean recordScreen) {
+ mRecordScreen = recordScreen;
+ }
+
+ /** Sets the option of whether to collect GMS version in test artifacts. */
+ public void setCollectGmsVersion(boolean collectGmsVersion) {
+ mCollectGmsVersion = collectGmsVersion;
+ }
+
+ /** Sets the option of whether to collect the app version in test artifacts. */
+ public void setCollectAppVersion(boolean collectAppVersion) {
+ mCollectAppVersion = collectAppVersion;
+ }
+
+ /** Sets the option of whether to run the crawler with UIAutomator mode. */
+ public void setUiAutomatorMode(boolean uiAutomatorMode) {
+ mUiAutomatorMode = uiAutomatorMode;
+ }
+
+ /**
+ * Sets the apk file path. Required when not running in UIAutomator mode.
+ *
+ * @param apkRoot The root path for an apk or a directory that contains apk files for a package.
+ */
+ public void setApkPath(Path apkRoot) {
+ mApkRoot = apkRoot;
+ }
+
+ @VisibleForTesting
+ interface RunUtilProvider {
+ IRunUtil get();
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java b/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java
new file mode 100644
index 0000000..4be78f7
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/AppCrawlTesterHostPreparer.java
@@ -0,0 +1,194 @@
+/*
+ * 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.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.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.MoreFiles;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** A Tradefed preparer that preparers an app crawler on the host before testing. */
+public final class AppCrawlTesterHostPreparer implements ITargetPreparer {
+ private static final long COMMAND_TIMEOUT_MILLIS = 4 * 60 * 1000;
+ private static final String SDK_PATH_KEY = "SDK_PATH_KEY";
+ private static final String CRAWLER_BIN_PATH_KEY = "CSUITE_INTERNAL_CRAWLER_BIN_PATH";
+ private static final String CREDENTIAL_PATH_KEY = "CSUITE_INTERNAL_CREDENTIAL_PATH";
+ private static final String IS_READY_KEY = "CSUITE_INTERNAL_IS_READY";
+ @VisibleForTesting static final String SDK_TAR_OPTION = "sdk-tar";
+ @VisibleForTesting static final String CRAWLER_BIN_OPTION = "crawler-bin";
+ @VisibleForTesting static final String CREDENTIAL_JSON_OPTION = "credential-json";
+
+ @Option(
+ name = SDK_TAR_OPTION,
+ mandatory = true,
+ description = "The path to a tar file that contains the Android SDK.")
+ private File mSdkTar;
+
+ @Option(
+ name = CRAWLER_BIN_OPTION,
+ mandatory = true,
+ description = "Path to the directory containing the required crawler binary files.")
+ private File mCrawlerBin;
+
+ @Option(
+ name = CREDENTIAL_JSON_OPTION,
+ mandatory = true,
+ description = "The credential json file to access the crawler server.")
+ private File mCredential;
+
+ private RunUtilProvider mRunUtilProvider;
+
+ public AppCrawlTesterHostPreparer() {
+ this(() -> new RunUtil());
+ }
+
+ @VisibleForTesting
+ AppCrawlTesterHostPreparer(RunUtilProvider runUtilProvider) {
+ mRunUtilProvider = runUtilProvider;
+ }
+
+ /**
+ * Returns a path that contains Android SDK.
+ *
+ * @param testInfo The test info where the path is stored in.
+ * @return The path to Android SDK; Null if not set.
+ */
+ public static Path getSdkPath(TestInformation testInfo) {
+ return getPathFromBuildInfo(testInfo, SDK_PATH_KEY);
+ }
+
+ /**
+ * Returns a path that contains the crawler binaries.
+ *
+ * @param testInfo The test info where the path is stored in.
+ * @return The path to the crawler binaries folder; Null if not set.
+ */
+ public static Path getCrawlerBinPath(TestInformation testInfo) {
+ return getPathFromBuildInfo(testInfo, CRAWLER_BIN_PATH_KEY);
+ }
+
+ /**
+ * Returns a path to the credential json file for accessing the Robo crawler server.
+ *
+ * @param testInfo The test info where the path is stored in.
+ * @return The path to the crawler credential json file.
+ */
+ public static Path getCredentialPath(TestInformation testInfo) {
+ return getPathFromBuildInfo(testInfo, CREDENTIAL_PATH_KEY);
+ }
+
+ /**
+ * Checks whether the preparer has successfully executed.
+ *
+ * @param testInfo The test info .
+ * @return True if the preparer was executed successfully; False otherwise.
+ */
+ public static boolean isReady(TestInformation testInfo) {
+ return testInfo.getBuildInfo().getBuildAttributes().get(IS_READY_KEY) != null;
+ }
+
+ private static Path getPathFromBuildInfo(TestInformation testInfo, String key) {
+ String path = testInfo.getBuildInfo().getBuildAttributes().get(key);
+ return path == null ? null : Path.of(path);
+ }
+
+ @VisibleForTesting
+ static void setSdkPath(TestInformation testInfo, Path path) {
+ testInfo.getBuildInfo().addBuildAttribute(SDK_PATH_KEY, path.toString());
+ }
+
+ @VisibleForTesting
+ static void setCrawlerBinPath(TestInformation testInfo, Path path) {
+ testInfo.getBuildInfo().addBuildAttribute(CRAWLER_BIN_PATH_KEY, path.toString());
+ }
+
+ @VisibleForTesting
+ static void setCredentialPath(TestInformation testInfo, Path path) {
+ testInfo.getBuildInfo().addBuildAttribute(CREDENTIAL_PATH_KEY, path.toString());
+ }
+
+ @Override
+ public void setUp(TestInformation testInfo) throws TargetSetupError {
+ IRunUtil runUtil = mRunUtilProvider.get();
+
+ Path sdkPath;
+ try {
+ sdkPath = Files.createTempDirectory("android-sdk");
+ } catch (IOException e) {
+ throw new TargetSetupError("Failed to create the output path for android sdk.", e);
+ }
+
+ String cmd = "tar -xvzf " + mSdkTar.getPath() + " -C " + sdkPath.toString();
+ CLog.i("Decompressing Android SDK to " + sdkPath.toString());
+ CommandResult res = runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, cmd.split(" "));
+ if (!res.getStatus().equals(CommandStatus.SUCCESS)) {
+ throw new TargetSetupError(String.format("Failed to untar android sdk: %s", res));
+ }
+
+ setSdkPath(testInfo, sdkPath);
+
+ // Make the crawler binary executable.
+ String chmodCmd =
+ "chmod 555 " + mCrawlerBin.toPath().resolve("crawl_launcher_deploy.jar").toString();
+ CommandResult chmodRes = runUtil.runTimedCmd(COMMAND_TIMEOUT_MILLIS, chmodCmd.split(" "));
+ if (!chmodRes.getStatus().equals(CommandStatus.SUCCESS)) {
+ throw new TargetSetupError(
+ String.format("Failed to make crawler binary executable: %s", chmodRes));
+ }
+
+ setCrawlerBinPath(testInfo, mCrawlerBin.toPath());
+
+ setCredentialPath(testInfo, mCredential.toPath());
+
+ testInfo.getBuildInfo().addBuildAttribute(IS_READY_KEY, "true");
+ }
+
+ @Override
+ public void tearDown(TestInformation testInfo, Throwable e) {
+ try {
+ cleanUp(getSdkPath(testInfo));
+ } catch (IOException ioException) {
+ CLog.e(ioException);
+ }
+ }
+
+ private static void cleanUp(Path path) throws IOException {
+ if (path == null || !Files.exists(path)) {
+ return;
+ }
+
+ MoreFiles.deleteRecursively(path);
+ }
+
+ @VisibleForTesting
+ interface RunUtilProvider {
+ IRunUtil get();
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/CommandLineTemplateMappingProvider.java
index 33426ed..306e232 100644
--- a/harness/src/main/java/com/android/csuite/core/CommandLinePackageNameProvider.java
+++ b/harness/src/main/java/com/android/csuite/core/CommandLineTemplateMappingProvider.java
@@ -17,23 +17,27 @@
package com.android.csuite.core;
import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.config.Option.Importance;
import com.google.common.annotations.VisibleForTesting;
-import java.util.HashSet;
-import java.util.Set;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
-/** 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";
+/** Accepts template mapping from command line option. */
+public class CommandLineTemplateMappingProvider implements TemplateMappingProvider {
+ @VisibleForTesting static final String TEMPLATE_MAPPING_OPTION = "template-mapping";
- @Option(name = PACKAGE, description = "App package names.")
- private final Set<String> mPackages = new HashSet<>();
+ @Option(
+ name = TEMPLATE_MAPPING_OPTION,
+ description = "Optional template mapping for modules.",
+ importance = Importance.NEVER)
+ private Map<String, String> mTemplateMappings = new HashMap<>();
@Override
- public Set<String> get() {
- return mPackages;
+ public Stream<Map.Entry<String, String>> get() throws IOException {
+ return mTemplateMappings.entrySet().stream();
}
}
diff --git a/harness/src/main/java/com/android/csuite/core/DeviceUtils.java b/harness/src/main/java/com/android/csuite/core/DeviceUtils.java
new file mode 100644
index 0000000..997f41b
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/DeviceUtils.java
@@ -0,0 +1,477 @@
+/*
+ * 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 android.service.dropbox.DropBoxManagerServiceDumpProto;
+import android.service.dropbox.DropBoxManagerServiceDumpProto.Entry;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/** A utility class that contains common methods to interact with the test device. */
+public class DeviceUtils {
+ @VisibleForTesting static final String UNKNOWN = "Unknown";
+ @VisibleForTesting static final String VERSION_CODE_PREFIX = "versionCode=";
+ @VisibleForTesting static final String VERSION_NAME_PREFIX = "versionName=";
+ @VisibleForTesting static final String RESET_PACKAGE_COMMAND_PREFIX = "pm clear ";
+ public static final Set<String> DROPBOX_APP_CRASH_TAGS =
+ Set.of(
+ "SYSTEM_TOMBSTONE",
+ "system_app_anr",
+ "system_app_native_crash",
+ "system_app_crash",
+ "data_app_anr",
+ "data_app_native_crash",
+ "data_app_crash");
+
+ @VisibleForTesting
+ static final String LAUNCH_PACKAGE_COMMAND_TEMPLATE =
+ "monkey -p %s -c android.intent.category.LAUNCHER 1";
+
+ private static final String VIDEO_PATH_ON_DEVICE_TEMPLATE = "/sdcard/screenrecord_%s.mp4";
+ @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS = 10 * 1000;
+ @VisibleForTesting static final int WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS = 500;
+
+ private final ITestDevice mDevice;
+ private final Sleeper mSleeper;
+ private final Clock mClock;
+ private final RunUtilProvider mRunUtilProvider;
+ private final TempFileSupplier mTempFileSupplier;
+
+ public static DeviceUtils getInstance(ITestDevice device) {
+ return new DeviceUtils(
+ device,
+ duration -> {
+ Thread.sleep(duration);
+ },
+ () -> System.currentTimeMillis(),
+ () -> RunUtil.getDefault(),
+ () -> Files.createTempFile(TestUtils.class.getName(), ".tmp"));
+ }
+
+ @VisibleForTesting
+ DeviceUtils(
+ ITestDevice device,
+ Sleeper sleeper,
+ Clock clock,
+ RunUtilProvider runUtilProvider,
+ TempFileSupplier tempFileSupplier) {
+ mDevice = device;
+ mSleeper = sleeper;
+ mClock = clock;
+ mRunUtilProvider = runUtilProvider;
+ mTempFileSupplier = tempFileSupplier;
+ }
+
+ /**
+ * A runnable that throws DeviceNotAvailableException. Use this interface instead of Runnable so
+ * that the DeviceNotAvailableException won't need to be handled inside the run() method.
+ */
+ public interface RunnableThrowingDeviceNotAvailable {
+ void run() throws DeviceNotAvailableException;
+ }
+
+ /**
+ * Get the current device timestamp in milliseconds.
+ *
+ * @return The device time
+ * @throws DeviceNotAvailableException When the device is not available.
+ * @throws DeviceRuntimeException When the command to get device time failed or failed to parse
+ * the timestamp.
+ */
+ public DeviceTimestamp currentTimeMillis()
+ throws DeviceNotAvailableException, DeviceRuntimeException {
+ CommandResult result = mDevice.executeShellV2Command("echo ${EPOCHREALTIME:0:14}");
+ if (result.getStatus() != CommandStatus.SUCCESS) {
+ throw new DeviceRuntimeException(
+ "Failed to get device time: " + result,
+ DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
+ }
+ try {
+ return new DeviceTimestamp(Long.parseLong(result.getStdout().replace(".", "").trim()));
+ } catch (NumberFormatException e) {
+ CLog.e("Cannot parse device time string: " + result.getStdout());
+ throw new DeviceRuntimeException(
+ "Cannot parse device time string: " + result.getStdout(),
+ DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
+ }
+ }
+
+ /**
+ * Record the device screen while running a task.
+ *
+ * <p>This method will not throw exception when the screenrecord command failed unless the
+ * device is unresponsive.
+ *
+ * @param action A runnable job that throws DeviceNotAvailableException.
+ * @param handler A file handler that process the output screen record mp4 file located on the
+ * host.
+ * @throws DeviceNotAvailableException When the device is unresponsive.
+ */
+ public void runWithScreenRecording(
+ RunnableThrowingDeviceNotAvailable action, ScreenrecordFileHandler handler)
+ throws DeviceNotAvailableException {
+ String videoPath = String.format(VIDEO_PATH_ON_DEVICE_TEMPLATE, new Random().nextInt());
+ mDevice.deleteFile(videoPath);
+
+ // Start screen recording
+ Process recordingProcess = null;
+ try {
+ recordingProcess =
+ mRunUtilProvider
+ .get()
+ .runCmdInBackground(
+ String.format(
+ "adb -s %s shell screenrecord %s",
+ mDevice.getSerialNumber(), videoPath)
+ .split("\\s+"));
+ } catch (IOException ioException) {
+ CLog.e("Exception is thrown when starting screen recording process: %s", ioException);
+ }
+
+ try {
+ long start = mClock.currentTimeMillis();
+ // Wait for the recording to start since it may take time for the device to start
+ // recording
+ while (recordingProcess != null) {
+ CommandResult result = mDevice.executeShellV2Command("ls " + videoPath);
+ if (result.getStatus() == CommandStatus.SUCCESS) {
+ break;
+ }
+
+ CLog.d(
+ "Screenrecord not started yet. Waiting %s milliseconds.",
+ WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS);
+
+ try {
+ mSleeper.sleep(WAIT_FOR_SCREEN_RECORDING_START_INTERVAL_MILLIS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ if (mClock.currentTimeMillis() - start
+ > WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS) {
+ CLog.e(
+ "Screenrecord did not start within %s milliseconds.",
+ WAIT_FOR_SCREEN_RECORDING_START_TIMEOUT_MILLIS);
+ break;
+ }
+ }
+
+ action.run();
+ } finally {
+ if (recordingProcess != null) {
+ recordingProcess.destroy();
+ }
+ // Try to pull, handle, and delete the video file from the device anyway.
+ handler.handleScreenRecordFile(mDevice.pullFile(videoPath));
+ mDevice.deleteFile(videoPath);
+ }
+ }
+
+ /** A file handler for screen record results. */
+ public interface ScreenrecordFileHandler {
+ /**
+ * Handles the screen record mp4 file located on the host.
+ *
+ * @param screenRecord The mp4 file located on the host. If screen record failed then the
+ * input could be null.
+ */
+ void handleScreenRecordFile(File screenRecord);
+ }
+
+ /**
+ * Freeze the screen rotation to the default orientation.
+ *
+ * @return True if succeed; False otherwise.
+ * @throws DeviceNotAvailableException
+ */
+ public boolean freezeRotation() throws DeviceNotAvailableException {
+ CommandResult result =
+ mDevice.executeShellV2Command(
+ "content insert --uri content://settings/system --bind"
+ + " name:s:accelerometer_rotation --bind value:i:0");
+ if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
+ CLog.e("The command to disable auto screen rotation failed: %s", result);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Unfreeze the screen rotation to the default orientation.
+ *
+ * @return True if succeed; False otherwise.
+ * @throws DeviceNotAvailableException
+ */
+ public boolean unfreezeRotation() throws DeviceNotAvailableException {
+ CommandResult result =
+ mDevice.executeShellV2Command(
+ "content insert --uri content://settings/system --bind"
+ + " name:s:accelerometer_rotation --bind value:i:1");
+ if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
+ CLog.e("The command to enable auto screen rotation failed: %s", result);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Launches a package on the device.
+ *
+ * @param packageName The package name to launch.
+ * @throws DeviceNotAvailableException When device was lost.
+ * @throws DeviceUtilsException When failed to launch the package.
+ */
+ public void launchPackage(String packageName)
+ throws DeviceUtilsException, DeviceNotAvailableException {
+ CommandResult result =
+ mDevice.executeShellV2Command(
+ String.format(LAUNCH_PACKAGE_COMMAND_TEMPLATE, packageName));
+ if (result.getStatus() != CommandStatus.SUCCESS || result.getExitCode() != 0) {
+ throw new DeviceUtilsException(
+ String.format(
+ "The command to launch package %s failed: %s", packageName, result));
+ }
+ }
+
+ /**
+ * Gets the version name of a package installed on the device.
+ *
+ * @param packageName The full package name to query
+ * @return The package version name, or 'Unknown' if the package doesn't exist or the adb
+ * command failed.
+ * @throws DeviceNotAvailableException
+ */
+ public String getPackageVersionName(String packageName) throws DeviceNotAvailableException {
+ CommandResult cmdResult =
+ mDevice.executeShellV2Command(
+ String.format("dumpsys package %s | grep versionName", packageName));
+
+ if (cmdResult.getStatus() != CommandStatus.SUCCESS
+ || !cmdResult.getStdout().trim().startsWith(VERSION_NAME_PREFIX)) {
+ return UNKNOWN;
+ }
+
+ return cmdResult.getStdout().trim().substring(VERSION_NAME_PREFIX.length());
+ }
+
+ /**
+ * Gets the version code of a package installed on the device.
+ *
+ * @param packageName The full package name to query
+ * @return The package version code, or 'Unknown' if the package doesn't exist or the adb
+ * command failed.
+ * @throws DeviceNotAvailableException
+ */
+ public String getPackageVersionCode(String packageName) throws DeviceNotAvailableException {
+ CommandResult cmdResult =
+ mDevice.executeShellV2Command(
+ String.format("dumpsys package %s | grep versionCode", packageName));
+
+ if (cmdResult.getStatus() != CommandStatus.SUCCESS
+ || !cmdResult.getStdout().trim().startsWith(VERSION_CODE_PREFIX)) {
+ return UNKNOWN;
+ }
+
+ return cmdResult.getStdout().trim().split(" ")[0].substring(VERSION_CODE_PREFIX.length());
+ }
+
+ /**
+ * Stops a running package on the device.
+ *
+ * @param packageName
+ * @throws DeviceNotAvailableException
+ */
+ public void stopPackage(String packageName) throws DeviceNotAvailableException {
+ mDevice.executeShellV2Command("am force-stop " + packageName);
+ }
+
+ /**
+ * Resets a package's data storage on the device.
+ *
+ * @param packageName The package name of an app to reset.
+ * @return True if the package exists and its data was reset; False otherwise.
+ * @throws DeviceNotAvailableException If the device was lost.
+ */
+ public boolean resetPackage(String packageName) throws DeviceNotAvailableException {
+ return mDevice.executeShellV2Command(RESET_PACKAGE_COMMAND_PREFIX + packageName).getStatus()
+ == CommandStatus.SUCCESS;
+ }
+
+ /**
+ * Gets dropbox entries from the device filtered by the provided tags.
+ *
+ * @param tags Dropbox tags to query.
+ * @return A list of dropbox entries.
+ * @throws IOException when failed to dump or read the dropbox protos.
+ */
+ public List<DropboxEntry> getDropboxEntries(Set<String> tags) throws IOException {
+ List<DropboxEntry> entries = new ArrayList<>();
+
+ for (String tag : tags) {
+ Path dumpFile = mTempFileSupplier.get();
+
+ CommandResult res =
+ mRunUtilProvider
+ .get()
+ .runTimedCmd(
+ 6000,
+ "sh",
+ "-c",
+ String.format(
+ "adb -s %s shell dumpsys dropbox --proto %s > %s",
+ mDevice.getSerialNumber(), tag, dumpFile));
+ if (res.getStatus() != CommandStatus.SUCCESS) {
+ throw new IOException("Dropbox dump command failed: " + res);
+ }
+
+ DropBoxManagerServiceDumpProto p =
+ DropBoxManagerServiceDumpProto.parseFrom(Files.readAllBytes(dumpFile));
+ Files.delete(dumpFile);
+
+ for (Entry entry : p.getEntriesList()) {
+ entries.add(
+ new DropboxEntry(entry.getTimeMs(), tag, entry.getData().toStringUtf8()));
+ }
+ }
+
+ return entries;
+ }
+
+ /** A class that stores the information of a dropbox entry. */
+ public static final class DropboxEntry {
+ private final long mTime;
+ private final String mTag;
+ private final String mData;
+
+ /** Returns the entrt's time stamp on device. */
+ public long getTime() {
+ return mTime;
+ }
+
+ /** Returns the entrt's tag. */
+ public String getTag() {
+ return mTag;
+ }
+
+ /** Returns the entrt's data. */
+ public String getData() {
+ return mData;
+ }
+
+ @VisibleForTesting
+ DropboxEntry(long time, String tag, String data) {
+ mTime = time;
+ mTag = tag;
+ mData = data;
+ }
+ }
+
+ /** A general exception class representing failed device utility operations. */
+ public static final class DeviceUtilsException extends Exception {
+ /**
+ * Constructs a new {@link DeviceUtilsException} with a meaningful error message.
+ *
+ * @param message A error message describing the cause of the error.
+ */
+ private DeviceUtilsException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@link DeviceUtilsException} with a meaningful error message, and a
+ * cause.
+ *
+ * @param message A detailed error message.
+ * @param cause A {@link Throwable} capturing the original cause of the {@link
+ * DeviceUtilsException}.
+ */
+ private DeviceUtilsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new {@link DeviceUtilsException} with a cause.
+ *
+ * @param cause A {@link Throwable} capturing the original cause of the {@link
+ * DeviceUtilsException}.
+ */
+ private DeviceUtilsException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ /**
+ * A class to contain a device timestamp.
+ *
+ * <p>Use this class instead of long to pass device timestamps so that they are less likely to
+ * be confused with host timestamps.
+ */
+ public static class DeviceTimestamp {
+ private final long mTimestamp;
+
+ public DeviceTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /** Gets the timestamp on a device. */
+ public long get() {
+ return mTimestamp;
+ }
+ }
+
+ @VisibleForTesting
+ interface Sleeper {
+ void sleep(long milliseconds) throws InterruptedException;
+ }
+
+ @VisibleForTesting
+ interface Clock {
+ long currentTimeMillis();
+ }
+
+ @VisibleForTesting
+ interface RunUtilProvider {
+ IRunUtil get();
+ }
+
+ @VisibleForTesting
+ interface TempFileSupplier {
+ Path get() throws IOException;
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/DirectoryBasedModuleInfoProvider.java b/harness/src/main/java/com/android/csuite/core/DirectoryBasedModuleInfoProvider.java
new file mode 100644
index 0000000..326db82
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/DirectoryBasedModuleInfoProvider.java
@@ -0,0 +1,108 @@
+/*
+ * 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.IConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.util.AaptParser;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/** Generates modules from package files in a directory. */
+public final class DirectoryBasedModuleInfoProvider implements ModuleInfoProvider {
+ @VisibleForTesting static final String DIRECTORY_OPTION = "directory";
+
+ @VisibleForTesting
+ static final String PACKAGE_INSTALL_FILE_PLACEHOLDER = "{package_install_file}";
+
+ @VisibleForTesting static final String PACKAGE_PLACEHOLDER = "{package}";
+
+ // TODO(yuexima): Add split APK directories support.
+ @Option(
+ name = DIRECTORY_OPTION,
+ description =
+ "A directory that contains package installation files for scanning. Modules"
+ + " will be generated using the package installation file names as the"
+ + " module names. Currently, only non-split APK files placed on the root"
+ + " of the directory are scanned. Directories and other type of files will"
+ + " be ignored.",
+ importance = Importance.NEVER)
+ private final Set<File> mDirectories = new HashSet<>();
+
+ private final PackageNameParser mPackageNameParser;
+
+ public DirectoryBasedModuleInfoProvider() {
+ this(new AaptPackageNameParser());
+ }
+
+ @VisibleForTesting
+ DirectoryBasedModuleInfoProvider(PackageNameParser packageNameParser) {
+ mPackageNameParser = packageNameParser;
+ }
+
+ @Override
+ public Stream<ModuleInfo> get(IConfiguration configuration) throws IOException {
+ ModuleTemplate template = ModuleTemplate.loadFrom(configuration);
+ return mDirectories.stream()
+ .flatMap(dir -> Arrays.stream(dir.listFiles()))
+ .filter(File::isFile)
+ .filter(file -> file.getPath().toLowerCase().endsWith(".apk"))
+ .map(
+ file -> {
+ try {
+ return new ModuleInfo(
+ file.getName(),
+ template.substitute(
+ file.getName(),
+ Map.of(
+ PACKAGE_PLACEHOLDER,
+ mPackageNameParser.parsePackageName(file),
+ PACKAGE_INSTALL_FILE_PLACEHOLDER,
+ file.getPath())));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ }
+
+ private static final class AaptPackageNameParser implements PackageNameParser {
+ @Override
+ public String parsePackageName(File apkFile) throws IOException {
+ String packageName = AaptParser.parse(apkFile).getPackageName();
+ if (packageName == null) {
+ throw new IOException(
+ String.format("Failed to parse package name with AAPT for %s", apkFile));
+ }
+ return packageName;
+ }
+ }
+
+ @VisibleForTesting
+ interface PackageNameParser {
+ String parsePackageName(File apkFile) throws IOException;
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java
deleted file mode 100644
index 6f6af06..0000000
--- a/harness/src/main/java/com/android/csuite/core/FileBasedPackageNameProvider.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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/FileBasedTemplateMappingProvider.java b/harness/src/main/java/com/android/csuite/core/FileBasedTemplateMappingProvider.java
new file mode 100644
index 0000000..44ad442
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/FileBasedTemplateMappingProvider.java
@@ -0,0 +1,77 @@
+/*
+ * 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.Option.Importance;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** Accepts files that contains template mapping entries. */
+public class FileBasedTemplateMappingProvider implements TemplateMappingProvider {
+ @VisibleForTesting static final String TEMPLATE_MAPPING_FILE_OPTION = "template-mapping-file";
+
+ @VisibleForTesting static final String COMMENT_LINE_PREFIX = "#";
+ @VisibleForTesting static final String MODULE_TEMPLATE_SEPARATOR = " ";
+
+ @Option(
+ name = TEMPLATE_MAPPING_FILE_OPTION,
+ description = "Template mapping file paths.",
+ importance = Importance.NEVER)
+ private Set<File> mTemplateMappingFiles = new HashSet<>();
+
+ @Override
+ public Stream<Map.Entry<String, String>> get() throws IOException {
+ List<Map.Entry<String, String>> entries = new ArrayList<>();
+
+ // Using for loop instead of stream here so that exceptions can be caught early.
+ for (File file : mTemplateMappingFiles) {
+ List<String> lines =
+ Files.readAllLines(file.toPath()).stream()
+ .map(String::trim)
+ .filter(FileBasedTemplateMappingProvider::isNotCommentLine)
+ .distinct()
+ .collect(Collectors.toList());
+ for (String line : lines) {
+ String[] pair = line.split(MODULE_TEMPLATE_SEPARATOR);
+ Preconditions.checkArgument(
+ pair.length == 2, "Unrecognized template map format " + line);
+ entries.add(new AbstractMap.SimpleEntry<>(pair[0].trim(), pair[1].trim()));
+ }
+ }
+
+ return entries.stream();
+ }
+
+ private static boolean isNotCommentLine(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/ModuleGenerator.java b/harness/src/main/java/com/android/csuite/core/ModuleGenerator.java
new file mode 100644
index 0000000..af1de81
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/ModuleGenerator.java
@@ -0,0 +1,240 @@
+/*
+ * 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 com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+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.base.Preconditions;
+import com.google.errorprone.annotations.MustBeClosed;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * Generates 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 modules' info from all ModuleInfoProvider objects specified in
+ * the test plan config.
+ *
+ * <h2>Syntax and usage</h2>
+ *
+ * <p>References to module info providers in TradeFed test plan config must have the following
+ * syntax:
+ *
+ * <blockquote>
+ *
+ * <b>&lt;object type="MODULE_INFO_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 ModuleInfoProvider
+ * implementation class.
+ */
+public final class ModuleGenerator
+ implements IRemoteTest,
+ IShardableTest,
+ IBuildReceiver,
+ IConfigurationReceiver,
+ ITargetPreparer {
+ @VisibleForTesting static final String MODULE_FILE_NAME_EXTENSION = ".config";
+ private static final Collection<IRemoteTest> NOT_SPLITTABLE = null;
+
+ private final TestDirectoryProvider mTestDirectoryProvider;
+ private IBuildInfo mBuildInfo;
+ private IConfiguration mConfiguration;
+
+ public ModuleGenerator() {
+ this(buildInfo -> new CompatibilityBuildHelper(buildInfo).getTestsDir().toPath());
+ }
+
+ @VisibleForTesting
+ ModuleGenerator(TestDirectoryProvider testDirectoryProvider) {
+ mTestDirectoryProvider = testDirectoryProvider;
+ }
+
+ @Override
+ public void setUp(TestInformation testInfo) {
+ // Do not add cleanup code here as this method is executed after the split method.
+ }
+
+ /**
+ * Cleans up the generated test modules files.
+ *
+ * <p>Note that this method does not execute from the same instance of this class that generates
+ * the modules so be careful when using any class fields.
+ */
+ @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 {
+ deleteModuleFiles();
+ } catch (IOException ioException) {
+ throw new UncheckedIOException(ioException);
+ }
+ }
+
+ private void deleteModuleFiles() throws IOException {
+ Files.list(mTestDirectoryProvider.get(mBuildInfo))
+ .filter(Files::isRegularFile)
+ .filter(path -> path.toString().endsWith(MODULE_FILE_NAME_EXTENSION))
+ .filter(
+ path -> {
+ try {
+ return Files.readString(path).contains(GENERATED_MODULE_NOTE);
+ } catch (IOException ioException) {
+ throw new UncheckedIOException(ioException);
+ }
+ })
+ .forEach(
+ path -> {
+ try {
+ Files.delete(path);
+ } catch (IOException ioException) {
+ throw new UncheckedIOException(ioException);
+ }
+ });
+ }
+
+ private void generateModules() throws IOException {
+ deleteModuleFiles();
+ try (Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = getModulesInfo()) {
+ Set<String> moduleNames = new HashSet<>();
+ modulesInfo.forEachOrdered(
+ moduleInfo -> {
+ String moduleName = moduleInfo.getName().trim();
+ if (moduleName.isEmpty()) {
+ throw new IllegalArgumentException("Module name cannot be empty.");
+ }
+
+ if (moduleNames.contains(moduleName)) {
+ throw new IllegalArgumentException(
+ "Duplicated module name: " + moduleName);
+ }
+
+ try {
+ Files.write(
+ getModulePath(moduleName),
+ (moduleInfo.getContent() + GENERATED_MODULE_NOTE).getBytes());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+
+ moduleNames.add(moduleName);
+ });
+ }
+ }
+
+ @MustBeClosed
+ @SuppressWarnings("MustBeClosedChecker")
+ private Stream<ModuleInfoProvider.ModuleInfo> getModulesInfo() {
+ List<?> configurations =
+ mConfiguration.getConfigurationObjectList(
+ ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE);
+ Preconditions.checkNotNull(
+ configurations, "Missing " + ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE);
+ return configurations.stream()
+ .map(obj -> (ModuleInfoProvider) obj)
+ .flatMap(
+ info -> {
+ try {
+ return info.get(mConfiguration);
+ } catch (IOException ioException) {
+ throw new UncheckedIOException(ioException);
+ }
+ });
+ }
+
+ /**
+ * Generates test modules. Note that the implementation of this method is not related to
+ * sharding in any way.
+ */
+ @Override
+ public Collection<IRemoteTest> split() {
+ try {
+ generateModules();
+ } catch (IOException ioException) {
+ throw new UncheckedIOException(ioException);
+ }
+ return NOT_SPLITTABLE;
+ }
+
+ private Path getModulePath(String moduleName) throws IOException {
+ return mTestDirectoryProvider
+ .get(mBuildInfo)
+ .resolve(moduleName + MODULE_FILE_NAME_EXTENSION);
+ }
+
+ @Override
+ public void run(final TestInformation testInfo, final ITestInvocationListener listener) {
+ // Intentionally left blank since this class is not really a test.
+ }
+
+ @Override
+ public void setBuild(IBuildInfo buildInfo) {
+ mBuildInfo = buildInfo;
+ }
+
+ @Override
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
+ @VisibleForTesting
+ interface TestDirectoryProvider {
+ Path get(IBuildInfo buildInfo) throws IOException;
+ }
+
+ @VisibleForTesting
+ static final String GENERATED_MODULE_NOTE =
+ "<!-- Note: The content of this module is auto generated from a template. Please do"
+ + " not modify manually. -->\n";
+}
diff --git a/harness/src/main/java/com/android/csuite/core/ModuleInfoProvider.java b/harness/src/main/java/com/android/csuite/core/ModuleInfoProvider.java
new file mode 100644
index 0000000..3d3bb72
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/ModuleInfoProvider.java
@@ -0,0 +1,58 @@
+/*
+ * 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.IConfiguration;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.errorprone.annotations.MustBeClosed;
+
+import java.io.IOException;
+import java.util.stream.Stream;
+
+/** An interface for providing module configuration contents. */
+public interface ModuleInfoProvider {
+ @VisibleForTesting String MODULE_INFO_PROVIDER_OBJECT_TYPE = "MODULE_INFO_PROVIDER";
+
+ final class ModuleInfo {
+ private final String mName;
+ private final String mContent;
+
+ public ModuleInfo(String name, String content) {
+ mName = name;
+ mContent = content;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getContent() {
+ return mContent;
+ }
+ }
+
+ /**
+ * Returns a stream of module configuration contents.
+ *
+ * @param configuration TradeFed suite configuration.
+ * @return A stream of ModuleInfo objects.
+ * @throws IOException if any IO exception occurs.
+ */
+ @MustBeClosed
+ Stream<ModuleInfo> get(IConfiguration configuration) throws IOException;
+}
diff --git a/harness/src/main/java/com/android/csuite/core/ModuleTemplate.java b/harness/src/main/java/com/android/csuite/core/ModuleTemplate.java
new file mode 100644
index 0000000..5dc5699
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/ModuleTemplate.java
@@ -0,0 +1,197 @@
+/*
+ * 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.IConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class ModuleTemplate {
+ @VisibleForTesting static final String XML_FILE_EXTENSION = ".xml";
+ @VisibleForTesting static final String TEMPLATE_FILE_EXTENSION = ".xml.template";
+
+ @VisibleForTesting
+ static final String MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE = "MODULE_TEMPLATE_PROVIDER";
+
+ @VisibleForTesting static final String DEFAULT_TEMPLATE_OPTION = "default-template";
+ @VisibleForTesting static final String EXTRA_TEMPLATES_OPTION = "extra-templates";
+ @VisibleForTesting static final String TEMPLATE_ROOT_OPTION = "template-root";
+
+ @Option(
+ name = DEFAULT_TEMPLATE_OPTION,
+ description = "The default module config template resource path.",
+ importance = Importance.ALWAYS)
+ private String mDefaultTemplate;
+
+ @Option(
+ name = TEMPLATE_ROOT_OPTION,
+ description = "The root path of the template files.",
+ importance = Importance.ALWAYS)
+ private String mTemplateRoot;
+
+ @Option(
+ name = EXTRA_TEMPLATES_OPTION,
+ description = "Extra module config template resource paths.",
+ importance = Importance.NEVER)
+ private List<String> mExtraTemplates = new ArrayList<>();
+
+ private final ResourceLoader mResourceLoader;
+ private String mDefaultTemplateContent;
+ private Map<String, String> mTemplateContentMap;
+ private Map<String, String> mTemplateMapping;
+
+ /**
+ * Load the ModuleTemplate object from a suite configuration.
+ *
+ * <p>An error will be thrown if there's no such objects or more than one objects.
+ *
+ * @param configuration The suite configuration.
+ * @return A ModuleTemplate object.
+ * @throws IOException
+ */
+ public static ModuleTemplate loadFrom(IConfiguration configuration) throws IOException {
+ List<?> moduleTemplates =
+ configuration.getConfigurationObjectList(MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE);
+ Preconditions.checkNotNull(
+ moduleTemplates, "Missing " + MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE);
+ Preconditions.checkArgument(
+ moduleTemplates.size() == 1,
+ "Only one module template object is expected. Found " + moduleTemplates.size());
+ ModuleTemplate moduleTemplate = (ModuleTemplate) moduleTemplates.get(0);
+ moduleTemplate.init(configuration);
+ return moduleTemplate;
+ }
+
+ public ModuleTemplate() {
+ this(new ClassResourceLoader());
+ }
+
+ @VisibleForTesting
+ ModuleTemplate(ResourceLoader resourceLoader) {
+ mResourceLoader = resourceLoader;
+ }
+
+ @SuppressWarnings("MustBeClosedChecker")
+ private void init(IConfiguration configuration) throws IOException {
+ if (mDefaultTemplateContent != null) { // Already loaded.
+ return;
+ }
+
+ mTemplateContentMap = new HashMap<>();
+
+ String defaultTemplateContent = mResourceLoader.load(mDefaultTemplate);
+ mDefaultTemplateContent = defaultTemplateContent;
+ mTemplateContentMap.put(
+ getTemplateNameFromTemplateFile(mDefaultTemplate), defaultTemplateContent);
+
+ for (String extraTemplate : mExtraTemplates) {
+ mTemplateContentMap.put(
+ getTemplateNameFromTemplateFile(extraTemplate),
+ mResourceLoader.load(extraTemplate));
+ }
+
+ mTemplateMapping = new HashMap<>();
+
+ List<?> templateMappingObjects =
+ configuration.getConfigurationObjectList(
+ TemplateMappingProvider.TEMPLATE_MAPPING_PROVIDER_OBJECT_TYPE);
+
+ if (templateMappingObjects == null) { // No mapping objects found.
+ return;
+ }
+
+ for (Object provider : templateMappingObjects) {
+ ((TemplateMappingProvider) provider)
+ .get()
+ .forEach(
+ entry -> {
+ String moduleName = entry.getKey();
+ String templateName =
+ getTemplateNameFromTemplateMapping(entry.getValue());
+
+ Preconditions.checkArgument(
+ !mTemplateMapping.containsKey(moduleName),
+ "Duplicated module template map key: " + moduleName);
+ Preconditions.checkArgument(
+ mTemplateContentMap.containsKey(templateName),
+ "The template specified in module template map does not"
+ + " exist: "
+ + templateName);
+
+ mTemplateMapping.put(moduleName, templateName);
+ });
+ }
+ }
+
+ private String getTemplateNameFromTemplateMapping(String name) {
+ String fileName = Path.of(name).toString();
+ if (fileName.toLowerCase().endsWith(XML_FILE_EXTENSION)) {
+ return fileName.substring(0, fileName.length() - XML_FILE_EXTENSION.length());
+ }
+ return fileName;
+ }
+
+ private String getTemplateNameFromTemplateFile(String path) {
+ Preconditions.checkArgument(
+ path.endsWith(TEMPLATE_FILE_EXTENSION),
+ "Unexpected file extension for template path: " + path);
+ String fileName = Path.of(mTemplateRoot).relativize(Path.of(path)).toString();
+ return fileName.substring(0, fileName.length() - TEMPLATE_FILE_EXTENSION.length());
+ }
+
+ public String substitute(String moduleName, Map<String, String> replacementPairs) {
+ Preconditions.checkNotNull(
+ mDefaultTemplateContent, "The module template object is not fully loaded.");
+ return replacementPairs.keySet().stream()
+ .reduce(
+ getTemplateContent(moduleName),
+ (res, placeholder) ->
+ res.replace(placeholder, replacementPairs.get(placeholder)));
+ }
+
+ private String getTemplateContent(String moduleName) {
+ if (!mTemplateMapping.containsKey(moduleName)) {
+ return mDefaultTemplateContent;
+ }
+
+ return mTemplateContentMap.get(mTemplateMapping.get(moduleName));
+ }
+
+ public interface ResourceLoader {
+ String load(String resourceName) throws IOException;
+ }
+
+ public static final class ClassResourceLoader implements ResourceLoader {
+ @Override
+ public String load(String resourceName) throws IOException {
+ return Resources.toString(
+ getClass().getClassLoader().getResource(resourceName), StandardCharsets.UTF_8);
+ }
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/PackageModuleInfoProvider.java b/harness/src/main/java/com/android/csuite/core/PackageModuleInfoProvider.java
new file mode 100644
index 0000000..56e34db
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/PackageModuleInfoProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.csuite.core;
+
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/** A module info provider that accepts package names and files that contains package names. */
+public final class PackageModuleInfoProvider implements ModuleInfoProvider {
+ @VisibleForTesting static final String PACKAGE_OPTION = "package";
+ @VisibleForTesting static final String PACKAGE_PLACEHOLDER = "{package}";
+
+ @Option(
+ name = PACKAGE_OPTION,
+ description = "App package names.",
+ importance = Importance.NEVER)
+ private final Set<String> mPackages = new HashSet<>();
+
+ @Override
+ public Stream<ModuleInfoProvider.ModuleInfo> get(IConfiguration configuration)
+ throws IOException {
+ ModuleTemplate moduleTemplate = ModuleTemplate.loadFrom(configuration);
+
+ return mPackages.stream()
+ .distinct()
+ .map(
+ packageName ->
+ new ModuleInfoProvider.ModuleInfo(
+ packageName,
+ moduleTemplate.substitute(
+ packageName,
+ Map.of(PACKAGE_PLACEHOLDER, packageName))));
+ }
+}
diff --git a/harness/src/main/java/com/android/csuite/core/PackagesFileModuleInfoProvider.java b/harness/src/main/java/com/android/csuite/core/PackagesFileModuleInfoProvider.java
new file mode 100644
index 0000000..ac9b1f0
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/PackagesFileModuleInfoProvider.java
@@ -0,0 +1,79 @@
+/*
+ * 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.IConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/** A module info provider that accepts files that contains package names. */
+public final class PackagesFileModuleInfoProvider implements ModuleInfoProvider {
+ @VisibleForTesting static final String PACKAGES_FILE_OPTION = "packages-file";
+ @VisibleForTesting static final String COMMENT_LINE_PREFIX = "#";
+ @VisibleForTesting static final String PACKAGE_PLACEHOLDER = "{package}";
+
+ @Option(
+ name = PACKAGES_FILE_OPTION,
+ 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.",
+ importance = Importance.NEVER)
+ private final Set<File> mPackagesFiles = new HashSet<>();
+
+ @Override
+ public Stream<ModuleInfoProvider.ModuleInfo> get(IConfiguration configuration)
+ throws IOException {
+ ModuleTemplate moduleTemplate = ModuleTemplate.loadFrom(configuration);
+
+ return mPackagesFiles.stream()
+ .flatMap(
+ file -> {
+ try {
+ return Files.readAllLines(file.toPath()).stream();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ })
+ .map(String::trim)
+ .filter(PackagesFileModuleInfoProvider::isNotCommentLine)
+ .distinct()
+ .map(
+ packageName ->
+ new ModuleInfoProvider.ModuleInfo(
+ packageName,
+ moduleTemplate.substitute(
+ packageName,
+ Map.of(PACKAGE_PLACEHOLDER, packageName))));
+ }
+
+ private static boolean isNotCommentLine(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/SystemPackageUninstaller.java b/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java
index 4ed6efe..1c2fc6a 100644
--- a/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java
+++ b/harness/src/main/java/com/android/csuite/core/SystemPackageUninstaller.java
@@ -61,7 +61,8 @@ public final class SystemPackageUninstaller {
startFramework(device);
});
if (!isPackageManagerRunning(device)) {
- throw new TargetSetupError("The package manager failed to start.");
+ throw new TargetSetupError(
+ "The package manager failed to start.", device.getDeviceDescriptor());
}
}
@@ -140,7 +141,8 @@ public final class SystemPackageUninstaller {
if (!device.isAdbRoot()) {
if (!device.enableAdbRoot()) {
- throw new TargetSetupError("Failed to enable adb root");
+ throw new TargetSetupError(
+ "Failed to enable adb root", device.getDeviceDescriptor());
}
disableRootAfterUninstall = true;
@@ -150,7 +152,8 @@ public final class SystemPackageUninstaller {
action.run();
} finally {
if (disableRootAfterUninstall && !device.disableAdbRoot()) {
- throw new TargetSetupError("Failed to disable adb root");
+ throw new TargetSetupError(
+ "Failed to disable adb root", device.getDeviceDescriptor());
}
}
}
@@ -182,7 +185,8 @@ public final class SystemPackageUninstaller {
if (commandResult.getStatus() != CommandStatus.SUCCESS) {
throw new TargetSetupError(
- String.format("%s; Command result: %s", failureMessage, commandResult));
+ String.format("%s; Command result: %s", failureMessage, commandResult),
+ device.getDeviceDescriptor());
}
return commandResult;
@@ -238,7 +242,9 @@ public final class SystemPackageUninstaller {
CLog.i("Removed an update package for %s", packageName);
}
- throw new TargetSetupError("Too many updates were uninstalled. Something must be wrong.");
+ throw new TargetSetupError(
+ "Too many updates were uninstalled. Something must be wrong.",
+ device.getDeviceDescriptor());
}
private static void removePackageData(String packageName, ITestDevice device)
@@ -267,8 +273,8 @@ public final class SystemPackageUninstaller {
if (commandResult.getStdout() == null) {
throw new TargetSetupError(
- String.format(
- "Failed to get pm command output: %s", commandResult.getStdout()));
+ String.format("Failed to get pm command output: %s", commandResult.getStdout()),
+ device.getDeviceDescriptor());
}
return Arrays.asList(commandResult.getStdout().split("\\r?\\n"))
@@ -287,7 +293,8 @@ public final class SystemPackageUninstaller {
|| !commandResult.getStdout().startsWith("package:")) {
throw new TargetSetupError(
String.format(
- "Failed to get pm path command output %s", commandResult.getStdout()));
+ "Failed to get pm path command output %s", commandResult.getStdout()),
+ device.getDeviceDescriptor());
}
String packageInstallPath = commandResult.getStdout().substring("package:".length());
diff --git a/harness/src/main/java/com/android/csuite/core/PackageNameProvider.java b/harness/src/main/java/com/android/csuite/core/TemplateMappingProvider.java
index 05709a1..8b1a73d 100644
--- a/harness/src/main/java/com/android/csuite/core/PackageNameProvider.java
+++ b/harness/src/main/java/com/android/csuite/core/TemplateMappingProvider.java
@@ -16,16 +16,17 @@
package com.android.csuite.core;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.errorprone.annotations.MustBeClosed;
+
import java.io.IOException;
-import java.util.Set;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/** An interface for providing template mapping to the module generator. */
+public interface TemplateMappingProvider {
+ @VisibleForTesting String TEMPLATE_MAPPING_PROVIDER_OBJECT_TYPE = "TEMPLATE_MAPPING_PROVIDER";
-/** 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;
+ @MustBeClosed
+ Stream<Map.Entry<String, String>> get() throws IOException;
}
diff --git a/harness/src/main/java/com/android/csuite/core/TestUtils.java b/harness/src/main/java/com/android/csuite/core/TestUtils.java
new file mode 100644
index 0000000..7bae9df
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/TestUtils.java
@@ -0,0 +1,394 @@
+/*
+ * 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.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.csuite.core.DeviceUtils.DropboxEntry;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** A utility class that contains common methods used by tests. */
+public class TestUtils {
+ private static final String GMS_PACKAGE_NAME = "com.google.android.gms";
+ private final TestInformation mTestInformation;
+ private final TestArtifactReceiver mTestArtifactReceiver;
+ private final DeviceUtils mDeviceUtils;
+ private static final int MAX_CRASH_SNIPPET_LINES = 60;
+
+ public static TestUtils getInstance(TestInformation testInformation, TestLogData testLogData) {
+ return new TestUtils(
+ testInformation,
+ new TestLogDataTestArtifactReceiver(testLogData),
+ DeviceUtils.getInstance(testInformation.getDevice()));
+ }
+
+ public static TestUtils getInstance(
+ TestInformation testInformation, TestArtifactReceiver testArtifactReceiver) {
+ return new TestUtils(
+ testInformation,
+ testArtifactReceiver,
+ DeviceUtils.getInstance(testInformation.getDevice()));
+ }
+
+ @VisibleForTesting
+ TestUtils(
+ TestInformation testInformation,
+ TestArtifactReceiver testArtifactReceiver,
+ DeviceUtils deviceUtils) {
+ mTestInformation = testInformation;
+ mTestArtifactReceiver = testArtifactReceiver;
+ mDeviceUtils = deviceUtils;
+ }
+
+ /**
+ * Take a screenshot on the device and save it to the test result artifacts.
+ *
+ * @param prefix The file name prefix.
+ * @throws DeviceNotAvailableException
+ */
+ public void collectScreenshot(String prefix) throws DeviceNotAvailableException {
+ try (InputStreamSource screenSource = mTestInformation.getDevice().getScreenshot()) {
+ mTestArtifactReceiver.addTestArtifact(
+ prefix + "_screenshot_" + mTestInformation.getDevice().getSerialNumber(),
+ LogDataType.PNG,
+ screenSource);
+ }
+ }
+
+ /**
+ * Record the device screen while running a task and save the video file to the test result
+ * artifacts.
+ *
+ * @param job A job to run while recording the screen.
+ * @param prefix The file name prefix.
+ * @throws DeviceNotAvailableException
+ */
+ public void collectScreenRecord(
+ DeviceUtils.RunnableThrowingDeviceNotAvailable job, String prefix)
+ throws DeviceNotAvailableException {
+ mDeviceUtils.runWithScreenRecording(
+ job,
+ video -> {
+ if (video != null) {
+ mTestArtifactReceiver.addTestArtifact(
+ prefix
+ + "_screenrecord_"
+ + mTestInformation.getDevice().getSerialNumber(),
+ LogDataType.MP4,
+ video);
+ } else {
+ CLog.e("Failed to get screen recording.");
+ }
+ });
+ }
+
+ /**
+ * Collect the GMS version name and version code, and save them as test result artifacts.
+ *
+ * @param prefix The file name prefix.
+ * @throws DeviceNotAvailableException
+ */
+ public void collectGmsVersion(String prefix) throws DeviceNotAvailableException {
+ String gmsVersionCode = mDeviceUtils.getPackageVersionCode(GMS_PACKAGE_NAME);
+ String gmsVersionName = mDeviceUtils.getPackageVersionName(GMS_PACKAGE_NAME);
+ CLog.i("GMS core versionCode=%s, versionName=%s", gmsVersionCode, gmsVersionName);
+
+ // Note: If the file name format needs to be modified, do it with cautions as some users may
+ // be parsing the output file name to get the version information.
+ mTestArtifactReceiver.addTestArtifact(
+ String.format("%s_[GMS_versionCode=%s]", prefix, gmsVersionCode),
+ LogDataType.TEXT,
+ gmsVersionCode.getBytes());
+ mTestArtifactReceiver.addTestArtifact(
+ String.format("%s_[GMS_versionName=%s]", prefix, gmsVersionName),
+ LogDataType.TEXT,
+ gmsVersionName.getBytes());
+ }
+
+ /**
+ * Collect the given package's version name and version code, and save them as test result
+ * artifacts.
+ *
+ * @param packageName The package name.
+ * @throws DeviceNotAvailableException
+ */
+ public void collectAppVersion(String packageName) throws DeviceNotAvailableException {
+ String versionCode = mDeviceUtils.getPackageVersionCode(packageName);
+ String versionName = mDeviceUtils.getPackageVersionName(packageName);
+ CLog.i("Package %s versionCode=%s, versionName=%s", packageName, versionCode, versionName);
+
+ // Note: If the file name format needs to be modified, do it with cautions as some users may
+ // be parsing the output file name to get the version information.
+ mTestArtifactReceiver.addTestArtifact(
+ String.format("%s_[versionCode=%s]", packageName, versionCode),
+ LogDataType.TEXT,
+ versionCode.getBytes());
+ mTestArtifactReceiver.addTestArtifact(
+ String.format("%s_[versionName=%s]", packageName, versionName),
+ LogDataType.TEXT,
+ versionName.getBytes());
+ }
+
+ /**
+ * Looks for crash log of a package in the device's dropbox entries.
+ *
+ * @param packageName The package name of an app.
+ * @param startTimeOnDevice The device timestamp after which the check starts. Dropbox items
+ * before this device timestamp will be ignored.
+ * @param saveToFile whether to save the package's full dropbox crash logs to a test output
+ * file.
+ * @return A string of crash log if crash was found; null otherwise.
+ * @throws IOException unexpected IOException
+ */
+ public String getDropboxPackageCrashLog(
+ String packageName, DeviceTimestamp startTimeOnDevice, boolean saveToFile)
+ throws IOException {
+ BiFunction<String, Integer, String> truncate =
+ (text, maxLines) -> {
+ String[] lines = text.split("\\r?\\n");
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < maxLines && i < lines.length; i++) {
+ sb.append(lines[i]);
+ sb.append('\n');
+ }
+ if (lines.length > maxLines) {
+ sb.append("... ");
+ sb.append(lines.length - maxLines);
+ sb.append(" more lines truncated ...\n");
+ }
+ return sb.toString();
+ };
+
+ List<DropboxEntry> entries =
+ mDeviceUtils.getDropboxEntries(DeviceUtils.DROPBOX_APP_CRASH_TAGS).stream()
+ .filter(entry -> (entry.getTime() >= startTimeOnDevice.get()))
+ .filter(entry -> entry.getData().contains(packageName))
+ .collect(Collectors.toList());
+
+ if (entries.size() == 0) {
+ return null;
+ }
+
+ String fullText =
+ entries.stream()
+ .map(
+ entry ->
+ String.format(
+ "Dropbox tag: %s\n%s",
+ entry.getTag(), entry.getData()))
+ .collect(Collectors.joining("\n============\n"));
+ String truncatedText =
+ entries.stream()
+ .map(
+ entry ->
+ String.format(
+ "Dropbox tag: %s\n%s",
+ entry.getTag(),
+ truncate.apply(
+ entry.getData(), MAX_CRASH_SNIPPET_LINES)))
+ .collect(Collectors.joining("\n============\n"));
+
+ mTestArtifactReceiver.addTestArtifact(
+ String.format("%s_dropbox_entries", packageName),
+ LogDataType.TEXT,
+ fullText.getBytes());
+ return truncatedText;
+ }
+
+ /**
+ * Generates a list of APK paths where the base.apk of split apk files are always on the first
+ * index if exists.
+ *
+ * <p>If the apk path is a single apk, then the apk is returned. If the apk path is a directory
+ * containing only one non-split apk file, the apk file is returned. If the apk path is a
+ * directory containing split apk files for one package, then the list of apks are returned and
+ * the base.apk sits on the first index. If the apk path does not contain any apk files, or
+ * multiple apk files without base.apk, then an IOException is thrown.
+ *
+ * @return A list of APK paths.
+ * @throws TestUtilsException If failed to read the apk path or unexpected number of apk files
+ * are found under the path.
+ */
+ public static List<Path> listApks(Path root) throws TestUtilsException {
+ // The apk path points to a non-split apk file.
+ if (Files.isRegularFile(root)) {
+ if (!root.toString().endsWith(".apk")) {
+ throw new TestUtilsException(
+ "The file on the given apk path is not an apk file: " + root);
+ }
+ return List.of(root);
+ }
+
+ List<Path> apks;
+ CLog.d("APK path = " + root);
+ try (Stream<Path> fileTree = Files.walk(root)) {
+ apks =
+ fileTree.filter(Files::isRegularFile)
+ .filter(path -> path.getFileName().toString().endsWith(".apk"))
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ throw new TestUtilsException("Failed to list apk files.", e);
+ }
+
+ if (apks.isEmpty()) {
+ throw new TestUtilsException("The apk directory does not contain any apk files");
+ }
+
+ // The apk path contains a single non-split apk or the base.apk of a split-apk.
+ if (apks.size() == 1) {
+ return apks;
+ }
+
+ if (apks.stream().map(path -> path.getParent().toString()).distinct().count() != 1) {
+ throw new TestUtilsException(
+ "Apk files are not all in the same folder: "
+ + Arrays.deepToString(apks.toArray(new Path[apks.size()])));
+ }
+
+ if (apks.stream().filter(path -> path.getFileName().toString().equals("base.apk")).count()
+ == 0) {
+ throw new TestUtilsException(
+ "Multiple non-split apk files detected: "
+ + Arrays.deepToString(apks.toArray(new Path[apks.size()])));
+ }
+
+ Collections.sort(
+ apks,
+ (first, second) -> first.getFileName().toString().equals("base.apk") ? -1 : 0);
+
+ return apks;
+ }
+
+ /** Returns the test information. */
+ public TestInformation getTestInformation() {
+ return mTestInformation;
+ }
+
+ /** Returns the test artifact receiver. */
+ public TestArtifactReceiver getTestArtifactReceiver() {
+ return mTestArtifactReceiver;
+ }
+
+ /** Returns the device utils. */
+ public DeviceUtils getDeviceUtils() {
+ return mDeviceUtils;
+ }
+
+ /** An exception class representing exceptions thrown from the test utils. */
+ public static final class TestUtilsException extends Exception {
+ /**
+ * Constructs a new {@link TestUtilsException} with a meaningful error message.
+ *
+ * @param message A error message describing the cause of the error.
+ */
+ private TestUtilsException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new {@link TestUtilsException} with a meaningful error message, and a cause.
+ *
+ * @param message A detailed error message.
+ * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException.
+ */
+ private TestUtilsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new {@link TestUtilsException} with a cause.
+ *
+ * @param cause A {@link Throwable} capturing the original cause of the TestUtilsException.
+ */
+ private TestUtilsException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ public static class TestLogDataTestArtifactReceiver implements TestArtifactReceiver {
+ @SuppressWarnings("hiding")
+ private final TestLogData mTestLogData;
+
+ public TestLogDataTestArtifactReceiver(TestLogData testLogData) {
+ mTestLogData = testLogData;
+ }
+
+ @Override
+ public void addTestArtifact(String name, LogDataType type, byte[] bytes) {
+ mTestLogData.addTestLog(name, type, new ByteArrayInputStreamSource(bytes));
+ }
+
+ @Override
+ public void addTestArtifact(String name, LogDataType type, File file) {
+ mTestLogData.addTestLog(name, type, new FileInputStreamSource(file));
+ }
+
+ @Override
+ public void addTestArtifact(String name, LogDataType type, InputStreamSource source) {
+ mTestLogData.addTestLog(name, type, source);
+ }
+ }
+
+ public interface TestArtifactReceiver {
+
+ /**
+ * Add a test artifact.
+ *
+ * @param name File name.
+ * @param type Output data type.
+ * @param bytes The output data.
+ */
+ void addTestArtifact(String name, LogDataType type, byte[] bytes);
+
+ /**
+ * Add a test artifact.
+ *
+ * @param name File name.
+ * @param type Output data type.
+ * @param inputStreamSource The inputStreamSource.
+ */
+ void addTestArtifact(String name, LogDataType type, InputStreamSource inputStreamSource);
+
+ /**
+ * Add a test artifact.
+ *
+ * @param name File name.
+ * @param type Output data type.
+ * @param file The output file.
+ */
+ void addTestArtifact(String name, LogDataType type, File file);
+ }
+}
diff --git a/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java b/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java
deleted file mode 100644
index e1f92b9..0000000
--- a/harness/src/main/java/com/android/tradefed/result/CompatibilityTestResult.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.result;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.regex.Pattern;
-
-/** encapsulates compatibility run results for a single app package tested */
-public class CompatibilityTestResult {
-
- public static final String KEY_PACKAGE = "app_package";
- // Keep app_version for backwards compatibility
- public static final String KEY_VERSION_CODE = "app_version_code";
- public static final String KEY_VERSION_STRING = "app_version_string";
- public static final String KEY_NAME = "app_name";
- public static final String KEY_RANK = "app_rank";
- public static final String KEY_STATUS = "status";
- public static final String KEY_MESSAGE = "message";
-
- public static final String SEPARATOR = "=@ppcomp@t=";
- public static final Pattern REGEX =
- Pattern.compile(String.format("^%s(.*?)%s", SEPARATOR, SEPARATOR));
-
- public static final String STATUS_SUCCESS = "success";
- public static final String STATUS_ERROR = "error"; // installation errors etc
- public static final String STATUS_FAILURE = "failure"; // app launch failures
-
- public String packageName = null;
- public String versionString = null;
- public String versionCode = null;
- public String name = null;
- public Integer rank = null;
- public String status = null;
- public String message = null;
-
- /**
- * Return the Serialized fields into JSON string
- *
- * @throws JSONException
- */
- public String toJsonString() throws JSONException {
- JSONObject o = new JSONObject();
- o.put(KEY_PACKAGE, packageName);
- o.put(KEY_VERSION_STRING, versionString);
- o.put(KEY_VERSION_CODE, versionString);
- o.put(KEY_NAME, name);
- o.put(KEY_RANK, rank);
- o.put(KEY_STATUS, status);
- o.put(KEY_MESSAGE, message);
- return o.toString();
- }
-
- /**
- * Reconstructs an instance from a JSON string
- *
- * @param json
- * @return the {@link CompatibilityTestResult} instance from the JSON serialized string.
- * @throws JSONException
- */
- public static CompatibilityTestResult fromJsonString(String json) throws JSONException {
- JSONObject o = new JSONObject(json);
- CompatibilityTestResult result = new CompatibilityTestResult();
- result.packageName = o.getString(KEY_PACKAGE);
- if (o.has(KEY_VERSION_STRING)) {
- result.versionString = o.getString(KEY_VERSION_STRING);
- }
- if (o.has(KEY_VERSION_CODE)) {
- result.versionString = o.getString(KEY_VERSION_CODE);
- }
- if (o.has(KEY_NAME)) {
- result.name = o.getString(KEY_NAME);
- }
- if (o.has(KEY_RANK)) {
- result.rank = o.getInt(KEY_RANK);
- }
- result.status = o.getString(KEY_STATUS);
- if (o.has(KEY_MESSAGE)) {
- result.message = o.getString(KEY_MESSAGE);
- }
- return result;
- }
-}
diff --git a/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java b/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java
deleted file mode 100644
index 3e2915e..0000000
--- a/harness/src/main/java/com/android/tradefed/util/PublicApkUtil.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util;
-
-import com.android.tradefed.log.LogUtil.CLog;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Pattern;
-
-/**
- * Utility class to download files from x20 to the corp TF environment. In particular, it is used in
- * conjunction with the <a href="http://go/app-compatibility-readme">App Compatibility pipeline</a>.
- */
-public class PublicApkUtil {
- private static final Pattern DATE_FORMAT = Pattern.compile("\\d{8}");
- private static final long DOWNLOAD_TIMEOUT_MS = 60 * 1000;
- private static final int DOWNLOAD_RETRIES = 3;
- private static final String LATEST_FILE = "latest.txt";
-
- /**
- * Helper method which constructs the dated CNS directory from the base directory and either the
- * supplied date option or the most recent directory.
- *
- * @param baseDir The base directory with the "latest" file and dated subdirectories.
- * @param subDir A specific target directory, or null if using the latest file.
- * @return The {@link File} of the x20 dir where the APKs are stored.
- * @throws IOException
- */
- public static File constructApkDir(String baseDir, String subDir) throws IOException {
- if (subDir != null) {
- return new File(baseDir, subDir);
- }
- File latestFile = null;
- try {
- latestFile =
- downloadFile(
- new File(baseDir, LATEST_FILE), DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
- String date = FileUtil.readStringFromFile(latestFile).trim();
- if (DATE_FORMAT.matcher(date).matches()) {
- return new File(baseDir, date);
- }
- return null;
- } finally {
- FileUtil.deleteFile(latestFile);
- }
- }
-
- /**
- * A configurable helper method for downloading a remote file.
- *
- * @param remoteFile The remote {@link File} location.
- * @param downloadTimeout The download timeout in milliseconds.
- * @param downloadRetries The download retry count, in case of failure.
- * @return The local {@link File} that was downloaded.
- * @throws IOException
- */
- public static File downloadFile(File remoteFile, long downloadTimeout, int downloadRetries)
- throws IOException {
- CLog.i("Attempting to download %s", remoteFile);
- File tmpFile = FileUtil.createTempFile(remoteFile.getName(), null);
- FileUtil.copyFile(remoteFile, tmpFile);
- return tmpFile;
- }
-
- /**
- * Helper method which downloads the ranking file and returns the list of apks.
- *
- * @param flavor The APK variant to pick.
- * @param dir The {@link File} of the dated x20 dir.
- * @param fallbackToApkScan fallback to scan for apk files in folder if no ranking csv file
- * @return The list of {@link ApkInfo} objects.
- * @throws IOException
- */
- public static List<ApkInfo> getApkList(String flavor, File dir, boolean fallbackToApkScan)
- throws IOException {
- File apkFile = new File(dir, String.format("%s_ranking.csv", flavor));
- if (!apkFile.exists() && fallbackToApkScan) {
- return getApkListFromDirectory(dir);
- } else {
- return getApkListFromRankingInfo(apkFile);
- }
- }
-
- /**
- * Constructs a list of degenerate {@link ApkInfo} based on apks files found in provided base
- * directory. The {@link ApkInfo} instance only contains relative filename, without the ranking
- * information.
- *
- * @param baseDir
- * @return
- * @throws IOException
- */
- private static List<ApkInfo> getApkListFromDirectory(File baseDir) throws IOException {
- List<ApkInfo> apkList = new ArrayList<>();
- File[] apks =
- baseDir.listFiles(
- new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- // filters out all apk files
- return name.endsWith(".apk");
- }
- });
- for (File apk : apks) {
- AaptParser parser = AaptParser.parse(apk);
- if (parser == null) {
- throw new IOException(
- String.format("Failed to parse apk file %s", apk.getCanonicalPath()));
- }
- ApkInfo apkInfo =
- new ApkInfo(
- -1,
- parser.getPackageName(),
- parser.getVersionName(),
- parser.getVersionCode(),
- apk.getName());
- apkList.add(apkInfo);
- }
- return apkList;
- }
-
- /**
- * Parses ranking information csv file into the data structure representing a list of apks with
- * ranking and package information
- *
- * @param rankingInfo the path to ranking csv file
- * @return
- * @throws IOException
- */
- private static List<ApkInfo> getApkListFromRankingInfo(File rankingInfo) throws IOException {
- List<ApkInfo> apkList = new ArrayList<>();
- File copiedFile = null;
- BufferedReader br = null;
- try {
- copiedFile = downloadFile(rankingInfo, DOWNLOAD_TIMEOUT_MS, DOWNLOAD_RETRIES);
- br = new BufferedReader(new FileReader(copiedFile));
- String line;
- boolean firstLine = true;
- while ((line = br.readLine()) != null) {
- if (firstLine) {
- firstLine = false;
- } else {
- try {
- apkList.add(ApkInfo.fromCsvLine(line));
- } catch (IllegalArgumentException e) {
- CLog.e("Ranking file not formatted properly, skipping.");
- CLog.e(e);
- }
- }
- }
- } finally {
- StreamUtil.close(br);
- FileUtil.deleteFile(copiedFile);
- }
- return apkList;
- }
-
- /**
- * Helper class which holds information about the ranking list such as rank, package name, etc.
- */
- public static class ApkInfo {
- public final int rank;
- public final String packageName;
- public final String versionString;
- public final String versionCode;
- public final String fileName;
-
- public ApkInfo(
- int rank,
- String packageName,
- String versionString,
- String versionCode,
- String fileName) {
- this.rank = rank;
- this.packageName = packageName;
- this.versionString = versionString;
- this.versionCode = versionCode;
- this.fileName = fileName;
- }
-
- public static ApkInfo fromCsvLine(String line) {
- String[] cols = QuotationAwareTokenizer.tokenizeLine(line, ",");
- int rank = -1;
- try {
- rank = Integer.parseInt(cols[0]);
- } catch (NumberFormatException e) {
- // rethrow as IAE with content of problematic line
- throw new IllegalArgumentException(
- String.format("Invalid line (rank field not a number): %s", line), e);
- }
- if (cols.length != 5) {
- throw new IllegalArgumentException(
- String.format("Invalid line (expected 5 data columns): %s", line));
- }
- return new ApkInfo(rank, cols[1], cols[2], cols[3], cols[4]);
- }
-
- @Override
- public String toString() {
- return String.format(
- "Package: %s v%s (%s), rank: %d, file: %s",
- packageName, versionCode, versionString, rank, fileName);
- }
- }
-}
diff --git a/harness/src/main/resources/config/csuite-base.xml b/harness/src/main/resources/config/csuite-base.xml
index 03e418b..c6a5553 100644
--- a/harness/src/main/resources/config/csuite-base.xml
+++ b/harness/src/main/resources/config/csuite-base.xml
@@ -28,12 +28,8 @@
<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.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" />
+ <object type="MODULE_INFO_PROVIDER" class="com.android.csuite.core.PackageModuleInfoProvider" />
+ <object type="MODULE_INFO_PROVIDER" class="com.android.csuite.core.PackagesFileModuleInfoProvider" />
+ <object type="TEMPLATE_MAPPING_PROVIDER" class="com.android.csuite.core.CommandLineTemplateMappingProvider" />
+ <object type="TEMPLATE_MAPPING_PROVIDER" class="com.android.csuite.core.FileBasedTemplateMappingProvider" />
</configuration>
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 e2e4352..f51c93e 100644
--- a/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
+++ b/harness/src/test/java/com/android/compatibility/targetprep/AppSetupPreparerTest.java
@@ -15,13 +15,17 @@
*/
package com.android.compatibility.targetprep;
-import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.BuildInfo;
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.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.ITestLogger;
+import com.android.tradefed.result.error.DeviceErrorIdentifier;
import com.android.tradefed.targetprep.TargetSetupError;
import com.android.tradefed.targetprep.TestAppInstallSetup;
import com.android.tradefed.util.AaptParser.AaptVersion;
@@ -41,34 +45,47 @@ import static org.testng.Assert.assertThrows;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
+import org.junit.Ignore;
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.Mockito;
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 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;
+ private static final DeviceErrorIdentifier NULL_DEVICE_ERROR_ID = null;
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
@Test
+ public void setUp_saveApkOptionEnabled_savesApk() throws Exception {
+ File apkPath = tempFolder.newFolder();
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, apkPath.getPath())
+ .setOption(AppSetupPreparer.OPTION_SAVE_APKS, "true")
+ .build();
+ ITestLogger testLogger = Mockito.mock(ITestLogger.class);
+ preparer.setTestLogger(testLogger);
+
+ preparer.setUp(createTestInfo());
+
+ verify(testLogger)
+ .testLog(Mockito.contains(apkPath.getName()), Mockito.any(), Mockito.any());
+ }
+
+ @Test
public void setUp_unresolvedAppUri_installs() throws Exception {
String appUri = "app://com.example.app";
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
@@ -78,7 +95,7 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, appUri)
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
verify(installer).addTestFile(new File(appUri));
}
@@ -97,34 +114,34 @@ public final class AppSetupPreparerTest {
@Test
public void setUp_withinRetryLimit_doesNotThrowException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doThrow(new TargetSetupError("Still failing"))
+ doThrow(new TargetSetupError("Still failing", NULL_DEVICE_ERROR_ID))
.doNothing()
.when(installer)
- .setUp(any(), any());
+ .setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
}
@Test
public void setUp_exceedsRetryLimit_throwsException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doThrow(new TargetSetupError("Still failing"))
- .doThrow(new TargetSetupError("Still failing"))
+ doThrow(new TargetSetupError("Still failing", NULL_DEVICE_ERROR_ID))
+ .doThrow(new TargetSetupError("Still failing", NULL_DEVICE_ERROR_ID))
.doNothing()
.when(installer)
- .setUp(any(), any());
+ .setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
.build();
- assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
}
@Test
@@ -134,43 +151,41 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "-1")
.build();
- assertThrows(
- IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(IllegalArgumentException.class, () -> preparer.setUp(createTestInfo()));
}
@Test
public void setUp_withinTimeout_doesNotThrowException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "1000")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
}
@Test
+ @Ignore // TODO(yuexima): Temporally disabled because of high flakiness b/187380263
public void setUp_exceedsTimeout_throwsException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(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));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
}
@Test
+ @Ignore // TODO(yuexima): Temporally disabled because of high flakiness b/187506768
public void setUp_timesOutWithoutExceedingRetryLimit_doesNotThrowException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER))
- .doNothing()
- .when(installer)
- .setUp(any(), any());
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).doNothing().when(installer).setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
@@ -178,13 +193,14 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "5")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
}
@Test
+ @Ignore // TODO(yuexima): Temporally disabled because of high flakiness b/187380263
public void setUp_timesOutAndExceedsRetryLimit_doesNotThrowException() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any(), any());
+ doAnswer(new AnswersWithDelay(10, EMPTY_ANSWER)).when(installer).setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
@@ -192,37 +208,37 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_SETUP_TIMEOUT_MILLIS, "5")
.build();
- assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
}
@Test
public void setUp_zeroMaxRetry_runsOnce() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doNothing().when(installer).setUp(any(), any());
+ doNothing().when(installer).setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_MAX_RETRY, "0")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
- verify(installer).setUp(any(), any());
+ verify(installer).setUp(any());
}
@Test
public void setUp_positiveMaxRetryButNoException_runsOnlyOnce() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doNothing().when(installer).setUp(any(), any());
+ doNothing().when(installer).setUp(any());
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_MAX_RETRY, "1")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
- verify(installer).setUp(any(), any());
+ verify(installer).setUp(any());
}
@Test
@@ -230,8 +246,7 @@ public final class AppSetupPreparerTest {
AppSetupPreparer preparer =
new PreparerBuilder().setOption(AppSetupPreparer.OPTION_MAX_RETRY, "-1").build();
- assertThrows(
- IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(IllegalArgumentException.class, () -> preparer.setUp(createTestInfo()));
}
@Test
@@ -241,13 +256,14 @@ public final class AppSetupPreparerTest {
new PreparerBuilder()
.setInstaller(
mockInstallerThatThrows(
- new TargetSetupError("Connection reset by peer.")))
+ new TargetSetupError(
+ "Connection reset by peer.", NULL_DEVICE_ERROR_ID)))
.setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "1")
.build();
ITestDevice device = createUnavailableDevice();
assertThrows(
- DeviceNotAvailableException.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
+ DeviceNotAvailableException.class, () -> preparer.setUp(createTestInfo(device)));
}
@Test
@@ -256,12 +272,13 @@ public final class AppSetupPreparerTest {
new PreparerBuilder()
.setInstaller(
mockInstallerThatThrows(
- new TargetSetupError("Connection reset by peer.")))
+ new TargetSetupError(
+ "Connection reset by peer.", NULL_DEVICE_ERROR_ID)))
.setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "1")
.build();
ITestDevice device = createAvailableDevice();
- assertThrows(TargetSetupError.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo(device)));
}
@Test
@@ -270,12 +287,13 @@ public final class AppSetupPreparerTest {
new PreparerBuilder()
.setInstaller(
mockInstallerThatThrows(
- new TargetSetupError("Connection reset by peer.")))
+ new TargetSetupError(
+ "Connection reset by peer.", NULL_DEVICE_ERROR_ID)))
.setOption(AppSetupPreparer.OPTION_WAIT_FOR_DEVICE_AVAILABLE_SECONDS, "-1")
.build();
ITestDevice device = createUnavailableDevice();
- assertThrows(TargetSetupError.class, () -> preparer.setUp(device, NULL_BUILD_INFO));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo(device)));
}
@Test
@@ -288,8 +306,7 @@ public final class AppSetupPreparerTest {
"-1")
.build();
- assertThrows(
- IllegalArgumentException.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(IllegalArgumentException.class, () -> preparer.setUp(createTestInfo()));
}
@Test
@@ -304,7 +321,7 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_TEST_FILE_NAME, "additional2.apk")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
assertThat(captor.getAllValues())
.containsAtLeast(new File("additional1.apk"), new File("additional2.apk"));
@@ -322,7 +339,7 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_INSTALL_ARG, "-arg2")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
assertThat(captor.getAllValues()).containsExactly("-arg1", "-arg2");
}
@@ -330,18 +347,49 @@ public final class AppSetupPreparerTest {
@Test
public void setUp_installIncrementalOptionSet_forwardsToInstaller() throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
-
+
AppSetupPreparer preparer =
new PreparerBuilder()
.setInstaller(installer)
.setOption(AppSetupPreparer.OPTION_INCREMENTAL_INSTALL, "true")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
- String result = ArgsOptionParser.getOptionHelp(false, installer);
- System.out.println(result);
+ preparer.setUp(createTestInfo());
+ String installOptions = ArgsOptionParser.getOptionHelp(false, installer);
+
+ assertThat(installOptions).contains("incremental");
+ }
+
+ @Test
+ public void setUp_incrementalFilterOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_INCREMENTAL_FILTER, "0.01")
+ .build();
+
+ preparer.setUp(createTestInfo());
+ String installOptions = ArgsOptionParser.getOptionHelp(false, installer);
- assertThat(result).contains("incremental");
+ assertThat(installOptions).contains("incremental-block-filter");
+ }
+
+ @Test
+ public void setUp_incrementalTimeoutOptionSet_forwardsToInstaller() throws Exception {
+ TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
+
+ AppSetupPreparer preparer =
+ new PreparerBuilder()
+ .setInstaller(installer)
+ .setOption(AppSetupPreparer.OPTION_INCREMENTAL_TIMEOUT_SECS, "60")
+ .build();
+
+ preparer.setUp(createTestInfo());
+ String installOptions = ArgsOptionParser.getOptionHelp(false, installer);
+
+ assertThat(installOptions).contains("incremental-install-timeout-secs");
}
@Test
@@ -355,7 +403,7 @@ public final class AppSetupPreparerTest {
.setOption(AppSetupPreparer.OPTION_AAPT_VERSION, "AAPT2")
.build();
- preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO);
+ preparer.setUp(createTestInfo());
assertThat(captor.getValue()).isEqualTo(AaptVersion.AAPT2);
}
@@ -366,13 +414,15 @@ public final class AppSetupPreparerTest {
AppSetupPreparer preparer =
new PreparerBuilder()
.setSleeper(fakeSleeper)
- .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Oops", NULL_DEVICE_ERROR_ID)))
.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));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
assertThat(fakeSleeper.getSleepHistory().get(0)).isEqualTo(Duration.ofSeconds(0));
}
@@ -382,13 +432,15 @@ public final class AppSetupPreparerTest {
AppSetupPreparer preparer =
new PreparerBuilder()
.setSleeper(fakeSleeper)
- .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Oops", NULL_DEVICE_ERROR_ID)))
.setOption(
AppSetupPreparer.OPTION_EXPONENTIAL_BACKOFF_MULTIPLIER_SECONDS, "3")
.setOption(AppSetupPreparer.OPTION_MAX_RETRY, "3")
.build();
- assertThrows(TargetSetupError.class, () -> preparer.setUp(NULL_DEVICE, NULL_BUILD_INFO));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
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));
@@ -400,15 +452,16 @@ public final class AppSetupPreparerTest {
AppSetupPreparer preparer =
new PreparerBuilder()
.setSleeper(fakeSleeper)
- .setInstaller(mockInstallerThatThrows(new TargetSetupError("Oops")))
+ .setInstaller(
+ mockInstallerThatThrows(
+ new TargetSetupError("Oops", NULL_DEVICE_ERROR_ID)))
.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));
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
assertThat(Thread.currentThread().isInterrupted()).isTrue();
assertThat(fakeSleeper.getSleepHistory().size()).isEqualTo(1);
} finally {
@@ -419,19 +472,10 @@ public final class AppSetupPreparerTest {
private TestAppInstallSetup mockInstallerThatThrows(Exception e) throws Exception {
TestAppInstallSetup installer = mock(TestAppInstallSetup.class);
- doThrow(e).when(installer).setUp(any(), any());
+ doThrow(e).when(installer).setUp(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);
@@ -472,7 +516,6 @@ public final class AppSetupPreparerTest {
}
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();
@@ -503,4 +546,15 @@ public final class AppSetupPreparerTest {
return preparer;
}
}
+
+ private static TestInformation createTestInfo() throws Exception {
+ return createTestInfo(createAvailableDevice());
+ }
+
+ 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();
+ }
}
diff --git a/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java b/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java
index 09f2141..684e9ab 100644
--- a/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java
+++ b/harness/src/test/java/com/android/compatibility/targetprep/CheckGmsPreparerTest.java
@@ -45,7 +45,6 @@ import java.util.ArrayList;
@RunWith(JUnit4.class)
public final class CheckGmsPreparerTest {
-
private CheckGmsPreparer mPreparer;
private LogCaptor mLogCaptor;
@@ -262,7 +261,7 @@ public final class CheckGmsPreparerTest {
return mMessage;
}
- LogItem(LogLevel logLevel, String tag, String message) {
+ LogItem(LogLevel logLevel, @SuppressWarnings("unused") String tag, String message) {
mLogLevel = logLevel;
mMessage = message;
}
diff --git a/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java b/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
deleted file mode 100644
index 0c23cc3..0000000
--- a/harness/src/test/java/com/android/compatibility/testtype/AppLaunchTestTest.java
+++ /dev/null
@@ -1,457 +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.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;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.anyObject;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import org.junit.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;
-import java.util.Map;
-import java.util.Set;
-
-@RunWith(JUnit4.class)
-public final class AppLaunchTestTest {
-
- private final ITestInvocationListener mMockListener = mock(ITestInvocationListener.class);
- private static final String TEST_PACKAGE_NAME = "package_name";
- private static final TestInformation NULL_TEST_INFORMATION = null;
- @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
-
- @Test
- public void run_testFailed() throws DeviceNotAvailableException {
- InstrumentationTest instrumentationTest = createFailingInstrumentationTest();
- AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyFailedAndEndedCall(mMockListener);
- }
-
- @Test
- public void run_testPassed() throws DeviceNotAvailableException {
- InstrumentationTest instrumentationTest = createPassingInstrumentationTest();
- AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyPassedAndEndedCall(mMockListener);
- }
-
- @Test
- public void run_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(createSuccessfulCommandResult());
- AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyPassedAndEndedCall(mMockListener);
- }
-
- @Test
- public void run_packageResetError() throws DeviceNotAvailableException {
- ITestDevice mMockDevice = mock(ITestDevice.class);
- when(mMockDevice.executeShellV2Command(String.format("pm clear %s", TEST_PACKAGE_NAME)))
- .thenReturn(createFailedCommandResult());
- AppLaunchTest appLaunchTest = createLaunchTestWithMockDevice(mMockDevice);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyFailedAndEndedCall(mMockListener);
- }
-
- @Test
- public void run_testRetry_passedAfterTwoFailings() throws Exception {
- InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(2);
- AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest, 2);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyPassedAndEndedCall(mMockListener);
- }
-
- @Test
- public void run_testRetry_failedAfterThreeFailings() throws Exception {
- InstrumentationTest instrumentationTest = createPassingInstrumentationTestAfterFailing(3);
- AppLaunchTest appLaunchTest = createLaunchTestWithInstrumentation(instrumentationTest, 2);
-
- appLaunchTest.run(NULL_TEST_INFORMATION, mMockListener);
-
- verifyFailedAndEndedCall(mMockListener);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void addIncludeFilter_nullIncludeFilter_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter(null);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void addIncludeFilter_emptyIncludeFilter_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("");
- }
-
- @Test
- public void addIncludeFilter_validIncludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("test_filter");
-
- assertTrue(sut.mIncludeFilters.contains("test_filter"));
- }
-
- @Test(expected = NullPointerException.class)
- public void addAllIncludeFilters_nullIncludeFilter_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addAllIncludeFilters(null);
- }
-
- @Test
- public void addAllIncludeFilters_validIncludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
- Set<String> test_filters = new HashSet<>();
- test_filters.add("filter_one");
- test_filters.add("filter_two");
-
- sut.addAllIncludeFilters(test_filters);
-
- assertTrue(sut.mIncludeFilters.contains("filter_one"));
- assertTrue(sut.mIncludeFilters.contains("filter_two"));
- }
-
- @Test
- public void clearIncludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
- sut.addIncludeFilter("filter_test");
-
- sut.clearIncludeFilters();
-
- assertTrue(sut.mIncludeFilters.isEmpty());
- }
-
- @Test
- public void getIncludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
- sut.addIncludeFilter("filter_test");
-
- assertEquals(sut.mIncludeFilters, sut.getIncludeFilters());
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void addExcludeFilter_nullExcludeFilter_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter(null);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void addExcludeFilter_emptyExcludeFilter_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter("");
- }
-
- @Test
- public void addExcludeFilter_validExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter("test_filter");
-
- assertTrue(sut.mExcludeFilters.contains("test_filter"));
- }
-
- @Test(expected = NullPointerException.class)
- public void addAllExcludeFilters_nullExcludeFilters_throwsException() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addAllExcludeFilters(null);
- }
-
- @Test
- public void addAllExcludeFilters_validExcludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
- Set<String> test_filters = new HashSet<>();
- test_filters.add("filter_one");
- test_filters.add("filter_two");
-
- sut.addAllExcludeFilters(test_filters);
-
- assertTrue(sut.mExcludeFilters.contains("filter_one"));
- assertTrue(sut.mExcludeFilters.contains("filter_two"));
- }
-
- @Test
- public void clearExcludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
- sut.addExcludeFilter("filter_test");
-
- sut.clearExcludeFilters();
-
- assertTrue(sut.mExcludeFilters.isEmpty());
- }
-
- @Test
- public void getExcludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter("filter_test");
-
- assertEquals(sut.mExcludeFilters, sut.getExcludeFilters());
- }
-
- @Test
- public void inFilter_withEmptyFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- assertTrue(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withRelatedIncludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_one");
-
- assertTrue(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withUnrelatedIncludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_two");
-
- assertFalse(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withRelatedExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter("filter_one");
-
- assertFalse(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withUnrelatedExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addExcludeFilter("filter_two");
-
- assertTrue(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withSameIncludeAndExcludeFilters() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_one");
- sut.addExcludeFilter("filter_one");
-
- assertFalse(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withUnrelatedIncludeFilterAndRelatedExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_one");
- sut.addExcludeFilter("filter_two");
-
- assertFalse(sut.inFilter("filter_two"));
- }
-
- @Test
- public void inFilter_withRelatedIncludeFilterAndUnrelatedExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_one");
- sut.addExcludeFilter("filter_two");
-
- assertTrue(sut.inFilter("filter_one"));
- }
-
- @Test
- public void inFilter_withUnrelatedIncludeFilterAndUnrelatedExcludeFilter() {
- AppLaunchTest sut = new AppLaunchTest();
-
- sut.addIncludeFilter("filter_one");
- sut.addExcludeFilter("filter_two");
-
- assertFalse(sut.inFilter("filter_three"));
- }
-
- private InstrumentationTest createFailingInstrumentationTest() {
- InstrumentationTest instrumentation =
- new InstrumentationTest() {
- @Override
- public void run(
- final TestInformation testInfo, final ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- listener.testFailed(new TestDescription("", ""), "test failed");
- }
- };
- return instrumentation;
- }
-
- private InstrumentationTest createPassingInstrumentationTest() {
- InstrumentationTest instrumentation =
- new InstrumentationTest() {
- @Override
- public void run(
- final TestInformation testInfo, final ITestInvocationListener listener)
- throws DeviceNotAvailableException {}
- };
- return instrumentation;
- }
-
- private InstrumentationTest createPassingInstrumentationTestAfterFailing(int failedCount) {
- InstrumentationTest instrumentation =
- new InstrumentationTest() {
- private int mRetryCount = 0;
-
- @Override
- public void run(
- final TestInformation testInfo, final ITestInvocationListener listener)
- throws DeviceNotAvailableException {
- if (mRetryCount < failedCount) {
- listener.testFailed(new TestDescription("", ""), "test failed");
- }
- mRetryCount++;
- }
- };
- return instrumentation;
- }
-
- private AppLaunchTest createLaunchTestWithInstrumentation(InstrumentationTest instrumentation) {
- return createLaunchTestWithInstrumentation(instrumentation, 0);
- }
-
- private AppLaunchTest createLaunchTestWithInstrumentation(
- InstrumentationTest instrumentation, int retryCount) {
- AppLaunchTest appLaunchTest =
- new AppLaunchTest(TEST_PACKAGE_NAME, retryCount) {
- @Override
- protected InstrumentationTest createInstrumentationTest(
- String packageBeingTested) {
- return instrumentation;
- }
-
- @Override
- protected CommandResult resetPackage() throws DeviceNotAvailableException {
- return createSuccessfulCommandResult();
- }
- };
- appLaunchTest.setDevice(mock(ITestDevice.class));
- return appLaunchTest;
- }
-
- private AppLaunchTest createLaunchTestWithMockDevice(ITestDevice device) {
- AppLaunchTest appLaunchTest = new AppLaunchTest(TEST_PACKAGE_NAME, 0);
- appLaunchTest.setDevice(device);
- return appLaunchTest;
- }
-
- private static void verifyFailedAndEndedCall(ITestInvocationListener listener) {
- InOrder inOrder = inOrder(listener);
- inOrder.verify(listener, times(1)).testRunStarted(anyString(), anyInt());
- inOrder.verify(listener, times(1)).testStarted(anyObject(), anyLong());
- inOrder.verify(listener, times(1)).testFailed(any(), anyString());
- inOrder.verify(listener, times(1))
- .testEnded(anyObject(), anyLong(), (Map<String, String>) any());
- inOrder.verify(listener, times(1)).testRunEnded(anyLong(), (HashMap<String, Metric>) any());
- }
-
- private static void verifyPassedAndEndedCall(ITestInvocationListener listener) {
- InOrder inOrder = inOrder(listener);
- inOrder.verify(listener, times(1)).testRunStarted(anyString(), anyInt());
- inOrder.verify(listener, times(1)).testStarted(anyObject(), anyLong());
- inOrder.verify(listener, never()).testFailed(any(), anyString());
- inOrder.verify(listener, times(1))
- .testEnded(anyObject(), anyLong(), (Map<String, String>) any());
- inOrder.verify(listener, times(1)).testRunEnded(anyLong(), (HashMap<String, Metric>) any());
- }
-
- 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/csuite/CSuiteUnitTests.java b/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
deleted file mode 100644
index 6eb1103..0000000
--- a/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
+++ /dev/null
@@ -1,37 +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.csuite;
-
-import org.junit.runner.RunWith;
-import org.junit.runners.Suite;
-import org.junit.runners.Suite.SuiteClasses;
-
-@RunWith(Suite.class)
-@SuiteClasses({
- 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/ModuleGeneratorTest.java b/harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java
deleted file mode 100644
index a679828..0000000
--- a/harness/src/test/java/com/android/csuite/config/ModuleGeneratorTest.java
+++ /dev/null
@@ -1,321 +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.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/ApkInstallerTest.java b/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java
new file mode 100644
index 0000000..99962ab
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/ApkInstallerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.csuite.core;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.csuite.core.ApkInstaller.ApkInstallerException;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+
+import com.google.common.jimfs.Jimfs;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@RunWith(JUnit4.class)
+public final class ApkInstallerTest {
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+
+ @Test
+ public void install_failedToListApks_throwsException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("not_apk.file"));
+ ApkInstaller sut =
+ new ApkInstaller("serial", Mockito.mock(IRunUtil.class), apk -> "package.name");
+
+ assertThrows(ApkInstallerException.class, () -> sut.install(root));
+ }
+
+ @Test
+ public void install_installCommandFailed_throwsException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+ IRunUtil runUtil = Mockito.mock(IRunUtil.class);
+ Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createFailedCommandResult());
+ ApkInstaller sut = new ApkInstaller("serial", runUtil, apk -> "package.name");
+
+ assertThrows(ApkInstallerException.class, () -> sut.install(root));
+ }
+
+ @Test
+ public void install_installCommandSucceed_doesNotThrow() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+ IRunUtil runUtil = Mockito.mock(IRunUtil.class);
+ Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+ ApkInstaller sut = new ApkInstaller("serial", runUtil, apk -> "package.name");
+
+ sut.install(root);
+ }
+
+ private static CommandResult createSuccessfulCommandResultWithStdout(String stdout) {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ commandResult.setStdout(stdout);
+ commandResult.setStderr("");
+ return commandResult;
+ }
+
+ private static CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ commandResult.setStdout("");
+ commandResult.setStderr("error");
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java
new file mode 100644
index 0000000..87762ea
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterHostPreparerTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.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.android.tradefed.util.IRunUtil;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.jimfs.Jimfs;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@RunWith(JUnit4.class)
+public final class AppCrawlTesterHostPreparerTest {
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+ ITestDevice mDevice = Mockito.mock(ITestDevice.class);
+ TestInformation mTestInfo = createTestInfo();
+ IRunUtil mRunUtil = Mockito.mock(IRunUtil.class);
+
+ @Test
+ public void getSdkPath_wasSet_returnsPath() {
+ Path path = Path.of("some");
+ AppCrawlTesterHostPreparer.setSdkPath(mTestInfo, path);
+
+ Path result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo);
+
+ assertThat(result.toString()).isEqualTo(path.toString());
+ }
+
+ @Test
+ public void getSdkPath_wasNotSet_returnsNull() {
+ Path result = AppCrawlTesterHostPreparer.getSdkPath(mTestInfo);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getCrawlerBinPath_wasSet_returnsPath() {
+ Path path = Path.of("some");
+ AppCrawlTesterHostPreparer.setCrawlerBinPath(mTestInfo, path);
+
+ Path result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo);
+
+ assertThat(result.toString()).isEqualTo(path.toString());
+ }
+
+ @Test
+ public void getCrawlerBinPath_wasNotSet_returnsNull() {
+ Path result = AppCrawlTesterHostPreparer.getCrawlerBinPath(mTestInfo);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void getCredentialPath_wasSet_returnsPath() {
+ Path path = Path.of("some");
+ AppCrawlTesterHostPreparer.setCredentialPath(mTestInfo, path);
+
+ Path result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo);
+
+ assertThat(result.toString()).isEqualTo(path.toString());
+ }
+
+ @Test
+ public void getCredentialPath_wasNotSet_returnsNull() {
+ Path result = AppCrawlTesterHostPreparer.getCredentialPath(mTestInfo);
+
+ assertNull(result);
+ }
+
+ @Test
+ public void setUp_commandsFailed_throwsException() throws Exception {
+ Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createFailedCommandResult());
+ AppCrawlTesterHostPreparer suj = createTestSubject();
+
+ assertThrows(TargetSetupError.class, () -> suj.setUp(mTestInfo));
+ }
+
+ @Test
+ public void isReady_setUpCommandsSucceed_returnsTrue() throws Exception {
+ Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createSuccessfulCommandResult());
+ AppCrawlTesterHostPreparer suj = createTestSubject();
+ suj.setUp(mTestInfo);
+
+ boolean ready = AppCrawlTesterHostPreparer.isReady(mTestInfo);
+
+ assertThat(ready).isTrue();
+ }
+
+ @Test
+ public void isReady_setUpFailed_returnsFalse() throws Exception {
+ Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createFailedCommandResult());
+ AppCrawlTesterHostPreparer suj = createTestSubject();
+ assertThrows(TargetSetupError.class, () -> suj.setUp(mTestInfo));
+
+ boolean ready = AppCrawlTesterHostPreparer.isReady(mTestInfo);
+
+ assertThat(ready).isFalse();
+ }
+
+ @Test
+ public void isReady_preparerNotExecuted_returnsFalse() throws Exception {
+ boolean ready = AppCrawlTesterHostPreparer.isReady(mTestInfo);
+
+ assertThat(ready).isFalse();
+ }
+
+ private AppCrawlTesterHostPreparer createTestSubject() throws Exception {
+ AppCrawlTesterHostPreparer suj = new AppCrawlTesterHostPreparer(() -> mRunUtil);
+ OptionSetter optionSetter = new OptionSetter(suj);
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.SDK_TAR_OPTION,
+ Files.createDirectories(mFileSystem.getPath("sdk")).toString());
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.CRAWLER_BIN_OPTION,
+ Files.createDirectories(mFileSystem.getPath("bin")).toString());
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.CREDENTIAL_JSON_OPTION,
+ Files.createDirectories(mFileSystem.getPath("cred.json")).toString());
+ return suj;
+ }
+
+ private TestInformation createTestInfo() {
+ IInvocationContext context = new InvocationContext();
+ context.addAllocatedDevice("device1", mDevice);
+ context.addDeviceBuildInfo("device1", new BuildInfo());
+ return TestInformation.newBuilder().setInvocationContext(context).build();
+ }
+
+ private static CommandResult createSuccessfulCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ commandResult.setStdout("");
+ commandResult.setStderr("");
+ return commandResult;
+ }
+
+ private static CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ commandResult.setStdout("");
+ commandResult.setStderr("error");
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java
new file mode 100644
index 0000000..dc9132b
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/AppCrawlTesterTest.java
@@ -0,0 +1,535 @@
+/*
+ * 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 static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.android.csuite.core.TestUtils.TestArtifactReceiver;
+import com.android.tradefed.build.BuildInfo;
+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.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.android.tradefed.util.IRunUtil;
+
+import com.google.common.jimfs.Jimfs;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public final class AppCrawlTesterTest {
+ private final TestArtifactReceiver mTestArtifactReceiver =
+ Mockito.mock(TestArtifactReceiver.class);
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+ private final ITestDevice mDevice = Mockito.mock(ITestDevice.class);
+ private final IRunUtil mRunUtil = Mockito.mock(IRunUtil.class);
+ private TestInformation mTestInfo;
+ private TestUtils mTestUtils;
+ private DeviceUtils mDeviceUtils = Mockito.spy(DeviceUtils.getInstance(mDevice));
+
+ @Before
+ public void setUp() throws Exception {
+ Mockito.when(mDevice.getSerialNumber()).thenReturn("serial");
+ mTestInfo = createTestInfo();
+ mTestUtils = createTestUtils();
+ }
+
+ @Test
+ public void start_apkNotProvided_throwsException() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setUiAutomatorMode(false);
+
+ assertThrows(NullPointerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void startAndAssertAppNoCrash_noCrashDetected_doesNotThrow() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ Mockito.doReturn(new DeviceUtils.DeviceTimestamp(1L))
+ .when(mDeviceUtils)
+ .currentTimeMillis();
+ String noCrashLog = null;
+ Mockito.doReturn(noCrashLog)
+ .when(mTestUtils)
+ .getDropboxPackageCrashLog(
+ Mockito.anyString(), Mockito.any(), Mockito.anyBoolean());
+
+ suj.startAndAssertAppNoCrash();
+ }
+
+ @Test
+ public void startAndAssertAppNoCrash_dropboxEntriesDetected_throws() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ Mockito.doReturn(new DeviceUtils.DeviceTimestamp(1L))
+ .when(mDeviceUtils)
+ .currentTimeMillis();
+ Mockito.doReturn("crash")
+ .when(mTestUtils)
+ .getDropboxPackageCrashLog(
+ Mockito.anyString(), Mockito.any(), Mockito.anyBoolean());
+
+ assertThrows(AssertionError.class, () -> suj.startAndAssertAppNoCrash());
+ }
+
+ @Test
+ public void startAndAssertAppNoCrash_crawlerExceptionIsThrown_throws() throws Exception {
+ AppCrawlTester suj = createNotPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ Mockito.doReturn(new DeviceUtils.DeviceTimestamp(1L))
+ .when(mDeviceUtils)
+ .currentTimeMillis();
+ String noCrashLog = null;
+ Mockito.doReturn(noCrashLog)
+ .when(mTestUtils)
+ .getDropboxPackageCrashLog(
+ Mockito.anyString(), Mockito.any(), Mockito.anyBoolean());
+
+ assertThrows(AssertionError.class, () -> suj.startAndAssertAppNoCrash());
+ }
+
+ @Test
+ public void start_screenRecordEnabled_screenIsRecorded() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setRecordScreen(true);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.times(1))
+ .collectScreenRecord(Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void start_screenRecordDisabled_screenIsNotRecorded() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setRecordScreen(false);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.never())
+ .collectScreenRecord(Mockito.any(), Mockito.anyString());
+ }
+
+ @Test
+ public void start_collectGmsVersionEnabled_versionIsCollected() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setCollectGmsVersion(true);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.times(1)).collectGmsVersion(Mockito.anyString());
+ }
+
+ @Test
+ public void start_collectGmsVersionDisabled_versionIsNotCollected() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setCollectGmsVersion(false);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.never()).collectGmsVersion(Mockito.anyString());
+ }
+
+ @Test
+ public void start_collectAppVersionEnabled_versionIsCollected() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setCollectAppVersion(true);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.times(1)).collectAppVersion(Mockito.anyString());
+ }
+
+ @Test
+ public void start_collectAppVersionDisabled_versionIsNotCollected() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.setCollectAppVersion(false);
+
+ suj.start();
+
+ Mockito.verify(mTestUtils, Mockito.never()).collectAppVersion(Mockito.anyString());
+ }
+
+ @Test
+ public void start_withSplitApksDirectory_doesNotThrowException() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+
+ suj.start();
+ }
+
+ @Test
+ public void start_credentialIsProvidedToCrawler() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+
+ suj.start();
+
+ Mockito.verify(mRunUtil)
+ .setEnvVariable(Mockito.eq("GOOGLE_APPLICATION_CREDENTIALS"), Mockito.anyString());
+ }
+
+ @Test
+ public void start_withSplitApksInSubDirectory_doesNotThrowException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createDirectories(root.resolve("sub"));
+ Files.createFile(root.resolve("sub").resolve("base.apk"));
+ Files.createFile(root.resolve("sub").resolve("config.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ suj.start();
+ }
+
+ @Test
+ public void start_withSingleSplitApkDirectory_doesNotThrowException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ suj.start();
+ }
+
+ @Test
+ public void start_withSingleApkDirectory_doesNotThrowException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ suj.start();
+ }
+
+ @Test
+ public void start_withSingleApkFile_doesNotThrowException() throws Exception {
+ Path root = mFileSystem.getPath("single.apk");
+ Files.createFile(root);
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ suj.start();
+ }
+
+ @Test
+ public void start_withApkDirectoryContainingOtherFileTypes_doesNotThrowException()
+ throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.apk"));
+ Files.createFile(root.resolve("single.not_apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ suj.start();
+ }
+
+ @Test
+ public void start_withApkDirectoryContainingNoApks_throwException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.not_apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ assertThrows(AppCrawlTester.CrawlerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void start_withNonApkPath_throwException() throws Exception {
+ Path root = mFileSystem.getPath("single.not_apk");
+ Files.createFile(root);
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ assertThrows(AppCrawlTester.CrawlerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void start_withApksInMultipleDirectories_throwException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createDirectories(root.resolve("1"));
+ Files.createDirectories(root.resolve("2"));
+ Files.createFile(root.resolve("1").resolve("single.apk"));
+ Files.createFile(root.resolve("2").resolve("single.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(root);
+
+ assertThrows(AppCrawlTester.CrawlerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void start_preparerNotRun_throwsException() throws Exception {
+ AppCrawlTester suj = createNotPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+
+ assertThrows(AppCrawlTester.CrawlerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void start_alreadyRun_throwsException() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.start();
+
+ assertThrows(AppCrawlTester.CrawlerException.class, () -> suj.start());
+ }
+
+ @Test
+ public void cleanUp_removesOutputDirectory() throws Exception {
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(createApkPathWithSplitApks());
+ suj.start();
+ assertTrue(Files.exists(suj.mOutput));
+
+ suj.cleanUp();
+
+ assertFalse(Files.exists(suj.mOutput));
+ }
+
+ @Test
+ public void createCrawlerRunCommand_containsRequiredCrawlerParams() throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("some.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(result).asList().contains("--key-store-file");
+ assertThat(result).asList().contains("--key-store-password");
+ assertThat(result).asList().contains("--device-serial-code");
+ assertThat(result).asList().contains("--apk-file");
+ }
+
+ @Test
+ public void createCrawlerRunCommand_crawlerIsExecutedThroughJavaJar() throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("some.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(result).asList().contains("java");
+ assertThat(result).asList().contains("-jar");
+ }
+
+ @Test
+ public void createCrawlerRunCommand_splitApksProvided_useApkFileAndSplitApkFilesParams()
+ throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("base.apk"));
+ Files.createFile(apkRoot.resolve("config1.apk"));
+ Files.createFile(apkRoot.resolve("config2.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apk-file")).count())
+ .isEqualTo(1);
+ assertThat(
+ Arrays.asList(result).stream()
+ .filter(s -> s.equals("--split-apk-files"))
+ .count())
+ .isEqualTo(2);
+ }
+
+ @Test
+ public void createCrawlerRunCommand_uiAutomatorModeEnabled_doesNotContainApks()
+ throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("base.apk"));
+ Files.createFile(apkRoot.resolve("config1.apk"));
+ Files.createFile(apkRoot.resolve("config2.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.setUiAutomatorMode(true);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(Arrays.asList(result).stream().filter(s -> s.equals("--apk-file")).count())
+ .isEqualTo(0);
+ assertThat(
+ Arrays.asList(result).stream()
+ .filter(s -> s.equals("--split-apk-files"))
+ .count())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void createCrawlerRunCommand_uiAutomatorModeEnabled_containsUiAutomatorParam()
+ throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("base.apk"));
+ Files.createFile(apkRoot.resolve("config1.apk"));
+ Files.createFile(apkRoot.resolve("config2.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.setUiAutomatorMode(true);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(
+ Arrays.asList(result).stream()
+ .filter(s -> s.equals("--ui-automator-mode"))
+ .count())
+ .isEqualTo(1);
+ assertThat(
+ Arrays.asList(result).stream()
+ .filter(s -> s.equals("--app-package-name"))
+ .count())
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void createCrawlerRunCommand_doesNotContainNullOrEmptyStrings() throws Exception {
+ Path apkRoot = mFileSystem.getPath("apk");
+ Files.createDirectories(apkRoot);
+ Files.createFile(apkRoot.resolve("base.apk"));
+ Files.createFile(apkRoot.resolve("config1.apk"));
+ Files.createFile(apkRoot.resolve("config2.apk"));
+ AppCrawlTester suj = createPreparedTestSubject();
+ suj.setApkPath(apkRoot);
+ suj.start();
+
+ String[] result = suj.createCrawlerRunCommand(mTestInfo);
+
+ assertThat(Arrays.asList(result).stream().filter(s -> s == null).count()).isEqualTo(0);
+
+ assertThat(Arrays.asList(result).stream().map(String::trim).filter(String::isEmpty).count())
+ .isEqualTo(0);
+ }
+
+ private void simulatePreparerWasExecutedSuccessfully()
+ throws ConfigurationException, IOException, TargetSetupError {
+ IRunUtil runUtil = Mockito.mock(IRunUtil.class);
+ Mockito.when(runUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createSuccessfulCommandResult());
+ AppCrawlTesterHostPreparer preparer = new AppCrawlTesterHostPreparer(() -> runUtil);
+ OptionSetter optionSetter = new OptionSetter(preparer);
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.SDK_TAR_OPTION,
+ Files.createDirectories(mFileSystem.getPath("sdk")).toString());
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.CRAWLER_BIN_OPTION,
+ Files.createDirectories(mFileSystem.getPath("bin")).toString());
+ optionSetter.setOptionValue(
+ AppCrawlTesterHostPreparer.CREDENTIAL_JSON_OPTION,
+ Files.createDirectories(mFileSystem.getPath("cred.json")).toString());
+ preparer.setUp(mTestInfo);
+ }
+
+ private AppCrawlTester createNotPreparedTestSubject() {
+ Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createSuccessfulCommandResult());
+ Mockito.when(mDevice.getSerialNumber()).thenReturn("serial");
+ return new AppCrawlTester("package.name", mTestUtils, () -> mRunUtil);
+ }
+
+ private AppCrawlTester createPreparedTestSubject()
+ throws IOException, ConfigurationException, TargetSetupError {
+ simulatePreparerWasExecutedSuccessfully();
+ Mockito.when(mRunUtil.runTimedCmd(Mockito.anyLong(), ArgumentMatchers.<String>any()))
+ .thenReturn(createSuccessfulCommandResult());
+ return new AppCrawlTester("package.name", mTestUtils, () -> mRunUtil);
+ }
+
+ private TestUtils createTestUtils() throws DeviceNotAvailableException {
+ TestUtils testUtils =
+ Mockito.spy(new TestUtils(mTestInfo, mTestArtifactReceiver, mDeviceUtils));
+ Mockito.doAnswer(
+ invocation -> {
+ ((DeviceUtils.RunnableThrowingDeviceNotAvailable)
+ invocation.getArguments()[0])
+ .run();
+ return null;
+ })
+ .when(testUtils)
+ .collectScreenRecord(Mockito.any(), Mockito.anyString());
+ Mockito.doNothing().when(testUtils).collectAppVersion(Mockito.anyString());
+ Mockito.doNothing().when(testUtils).collectGmsVersion(Mockito.anyString());
+ return testUtils;
+ }
+
+ private TestInformation createTestInfo() {
+ IInvocationContext context = new InvocationContext();
+ context.addAllocatedDevice("device1", mDevice);
+ context.addDeviceBuildInfo("device1", new BuildInfo());
+ return TestInformation.newBuilder().setInvocationContext(context).build();
+ }
+
+ private Path createApkPathWithSplitApks() throws IOException {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+ Files.createFile(root.resolve("config.apk"));
+
+ return root;
+ }
+
+ private static CommandResult createSuccessfulCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ commandResult.setStdout("");
+ commandResult.setStderr("");
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java b/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java
deleted file mode 100644
index 41e6dc1..0000000
--- a/harness/src/test/java/com/android/csuite/core/CommandLinePackageNameProviderTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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/DeviceUtilsTest.java b/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java
new file mode 100644
index 0000000..6d6bdec
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/DeviceUtilsTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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 static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
+import android.service.dropbox.DropBoxManagerServiceDumpProto;
+
+import com.android.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.csuite.core.DeviceUtils.DeviceUtilsException;
+import com.android.csuite.core.DeviceUtils.DropboxEntry;
+import com.android.tradefed.device.DeviceRuntimeException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.IRunUtil;
+
+import com.google.common.jimfs.Jimfs;
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(JUnit4.class)
+public final class DeviceUtilsTest {
+ private ITestDevice mDevice = Mockito.mock(ITestDevice.class);
+ private IRunUtil mRunUtil = Mockito.mock(IRunUtil.class);
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+
+ @Test
+ public void launchPackage_packageDoesNotExist_returnsFalse() throws Exception {
+ when(mDevice.executeShellV2Command(Mockito.startsWith("monkey -p")))
+ .thenReturn(createFailedCommandResult());
+ DeviceUtils sut = createSubjectUnderTest();
+
+ assertThrows(DeviceUtilsException.class, () -> sut.launchPackage("package.name"));
+ }
+
+ @Test
+ public void launchPackage_successfullyLaunchedThePackage_returnsTrue() throws Exception {
+ when(mDevice.executeShellV2Command(Mockito.startsWith("monkey -p")))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+ DeviceUtils sut = createSubjectUnderTest();
+
+ sut.launchPackage("package.name");
+ }
+
+ @Test
+ public void currentTimeMillis_deviceCommandFailed_throwsException() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.startsWith("echo")))
+ .thenReturn(createFailedCommandResult());
+
+ assertThrows(DeviceRuntimeException.class, () -> sut.currentTimeMillis());
+ }
+
+ @Test
+ public void currentTimeMillis_unexpectedFormat_throwsException() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.startsWith("echo")))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+
+ assertThrows(DeviceRuntimeException.class, () -> sut.currentTimeMillis());
+ }
+
+ @Test
+ public void currentTimeMillis_successful_returnsTime() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.startsWith("echo")))
+ .thenReturn(createSuccessfulCommandResultWithStdout("123"));
+
+ DeviceTimestamp result = sut.currentTimeMillis();
+
+ assertThat(result.get()).isEqualTo(Long.parseLong("123"));
+ }
+
+ @Test
+ public void runWithScreenRecording_recordingDidNotStart_jobIsExecuted() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mRunUtil.runCmdInBackground(Mockito.argThat(contains("shell", "screenrecord"))))
+ .thenReturn(Mockito.mock(Process.class));
+ when(mDevice.executeShellV2Command(Mockito.startsWith("ls")))
+ .thenReturn(createFailedCommandResult());
+ AtomicBoolean executed = new AtomicBoolean(false);
+ DeviceUtils.RunnableThrowingDeviceNotAvailable job = () -> executed.set(true);
+
+ sut.runWithScreenRecording(job, video -> {});
+
+ assertThat(executed.get()).isTrue();
+ }
+
+ @Test
+ public void runWithScreenRecording_recordCommandThrowsException_jobIsExecuted()
+ throws Exception {
+ when(mRunUtil.runCmdInBackground(Mockito.argThat(contains("shell", "screenrecord"))))
+ .thenThrow(new IOException());
+ DeviceUtils sut = createSubjectUnderTest();
+ AtomicBoolean executed = new AtomicBoolean(false);
+ DeviceUtils.RunnableThrowingDeviceNotAvailable job = () -> executed.set(true);
+
+ sut.runWithScreenRecording(job, video -> {});
+
+ assertThat(executed.get()).isTrue();
+ }
+
+ @Test
+ public void runWithScreenRecording_jobThrowsException_videoFileIsHandled() throws Exception {
+ when(mRunUtil.runCmdInBackground(Mockito.argThat(contains("shell", "screenrecord"))))
+ .thenReturn(Mockito.mock(Process.class));
+ when(mDevice.executeShellV2Command(Mockito.startsWith("ls")))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+ DeviceUtils sut = createSubjectUnderTest();
+ DeviceUtils.RunnableThrowingDeviceNotAvailable job =
+ () -> {
+ throw new RuntimeException();
+ };
+ AtomicBoolean handled = new AtomicBoolean(false);
+
+ assertThrows(
+ RuntimeException.class,
+ () -> sut.runWithScreenRecording(job, video -> handled.set(true)));
+
+ assertThat(handled.get()).isTrue();
+ }
+
+ @Test
+ public void getPackageVersionName_deviceCommandFailed_returnsUnknown() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionName")))
+ .thenReturn(createFailedCommandResult());
+
+ String result = sut.getPackageVersionName("any");
+
+ assertThat(result).isEqualTo(DeviceUtils.UNKNOWN);
+ }
+
+ @Test
+ public void getPackageVersionName_deviceCommandReturnsUnexpected_returnsUnknown()
+ throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionName")))
+ .thenReturn(
+ createSuccessfulCommandResultWithStdout(
+ "unexpected " + DeviceUtils.VERSION_NAME_PREFIX));
+
+ String result = sut.getPackageVersionName("any");
+
+ assertThat(result).isEqualTo(DeviceUtils.UNKNOWN);
+ }
+
+ @Test
+ public void getPackageVersionName_deviceCommandSucceed_returnsVersionName() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionName")))
+ .thenReturn(
+ createSuccessfulCommandResultWithStdout(
+ " " + DeviceUtils.VERSION_NAME_PREFIX + "123"));
+
+ String result = sut.getPackageVersionName("any");
+
+ assertThat(result).isEqualTo("123");
+ }
+
+ @Test
+ public void getPackageVersionCode_deviceCommandFailed_returnsUnknown() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionCode")))
+ .thenReturn(createFailedCommandResult());
+
+ String result = sut.getPackageVersionCode("any");
+
+ assertThat(result).isEqualTo(DeviceUtils.UNKNOWN);
+ }
+
+ @Test
+ public void getPackageVersionCode_deviceCommandReturnsUnexpected_returnsUnknown()
+ throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionCode")))
+ .thenReturn(
+ createSuccessfulCommandResultWithStdout(
+ "unexpected " + DeviceUtils.VERSION_CODE_PREFIX));
+
+ String result = sut.getPackageVersionCode("any");
+
+ assertThat(result).isEqualTo(DeviceUtils.UNKNOWN);
+ }
+
+ @Test
+ public void getPackageVersionCode_deviceCommandSucceed_returnVersionCode() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mDevice.executeShellV2Command(Mockito.endsWith("grep versionCode")))
+ .thenReturn(
+ createSuccessfulCommandResultWithStdout(
+ " " + DeviceUtils.VERSION_CODE_PREFIX + "123"));
+
+ String result = sut.getPackageVersionCode("any");
+
+ assertThat(result).isEqualTo("123");
+ }
+
+ @Test
+ public void getDropboxEntries_noEntries_returnsEmptyList() throws Exception {
+ DeviceUtils sut = createSubjectUnderTest();
+ when(mRunUtil.runTimedCmd(
+ Mockito.anyLong(),
+ Mockito.eq("sh"),
+ Mockito.eq("-c"),
+ Mockito.contains("dumpsys dropbox")))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+
+ List<DropboxEntry> result = sut.getDropboxEntries(Set.of(""));
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void getDropboxEntries_entryExists_returnsEntry() throws Exception {
+ Path dumpFile = Files.createTempFile(mFileSystem.getPath("/"), "test", ".tmp");
+ long time = 123;
+ String data = "abc";
+ String tag = "tag";
+ DropBoxManagerServiceDumpProto proto =
+ DropBoxManagerServiceDumpProto.newBuilder()
+ .addEntries(
+ DropBoxManagerServiceDumpProto.Entry.newBuilder()
+ .setTimeMs(time)
+ .setData(ByteString.copyFromUtf8(data)))
+ .build();
+ Files.write(dumpFile, proto.toByteArray());
+ DeviceUtils sut = createSubjectUnderTestWithTempFile(dumpFile);
+ when(mRunUtil.runTimedCmd(
+ Mockito.anyLong(), Mockito.eq("sh"), Mockito.eq("-c"), Mockito.anyString()))
+ .thenReturn(createSuccessfulCommandResultWithStdout(""));
+
+ List<DropboxEntry> result = sut.getDropboxEntries(Set.of(tag));
+
+ assertThat(result.get(0).getTime()).isEqualTo(time);
+ assertThat(result.get(0).getData()).isEqualTo(data);
+ assertThat(result.get(0).getTag()).isEqualTo(tag);
+ }
+
+ private DeviceUtils createSubjectUnderTestWithTempFile(Path tempFile) {
+ when(mDevice.getSerialNumber()).thenReturn("SERIAL");
+ FakeClock fakeClock = new FakeClock();
+ return new DeviceUtils(
+ mDevice, fakeClock.getSleeper(), fakeClock, () -> mRunUtil, () -> tempFile);
+ }
+
+ private DeviceUtils createSubjectUnderTest() {
+ when(mDevice.getSerialNumber()).thenReturn("SERIAL");
+ FakeClock fakeClock = new FakeClock();
+ return new DeviceUtils(
+ mDevice,
+ fakeClock.getSleeper(),
+ fakeClock,
+ () -> mRunUtil,
+ () -> Files.createTempFile(mFileSystem.getPath("/"), "test", ".tmp"));
+ }
+
+ private static class FakeClock implements DeviceUtils.Clock {
+ private long mCurrentTime = System.currentTimeMillis();
+ private DeviceUtils.Sleeper mSleeper = duration -> mCurrentTime += duration;
+
+ private DeviceUtils.Sleeper getSleeper() {
+ return mSleeper;
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return mCurrentTime += 1;
+ }
+ }
+
+ private static ArgumentMatcher<String[]> contains(String... args) {
+ return array -> Arrays.asList(array).containsAll(Arrays.asList(args));
+ }
+
+ private static CommandResult createSuccessfulCommandResultWithStdout(String stdout) {
+ CommandResult commandResult = new CommandResult(CommandStatus.SUCCESS);
+ commandResult.setExitCode(0);
+ commandResult.setStdout(stdout);
+ commandResult.setStderr("");
+ return commandResult;
+ }
+
+ private static CommandResult createFailedCommandResult() {
+ CommandResult commandResult = new CommandResult(CommandStatus.FAILED);
+ commandResult.setExitCode(1);
+ commandResult.setStdout("");
+ commandResult.setStderr("error");
+ return commandResult;
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/DirectoryBasedModuleInfoProviderTest.java b/harness/src/test/java/com/android/csuite/core/DirectoryBasedModuleInfoProviderTest.java
new file mode 100644
index 0000000..3adb4fa
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/DirectoryBasedModuleInfoProviderTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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 static java.util.stream.Collectors.toList;
+
+import com.android.csuite.core.ModuleInfoProvider.ModuleInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.OptionSetter;
+
+import com.google.common.truth.Correspondence;
+
+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.stream.Stream;
+
+@RunWith(JUnit4.class)
+public final class DirectoryBasedModuleInfoProviderTest {
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void get_directoryUnset_returnsEmptyStream() throws Exception {
+ DirectoryBasedModuleInfoProvider provider = createProvider();
+
+ Stream<ModuleInfo> modules = provider.get(createIConfig());
+
+ assertThat(modules.collect(toList())).isEmpty();
+ }
+
+ @Test
+ public void get_directoryIsEmpty_returnsEmptyStream() throws Exception {
+ DirectoryBasedModuleInfoProvider provider = createProvider(createDirectoryWithApkFiles());
+
+ Stream<ModuleInfo> modules = provider.get(createIConfig());
+
+ assertThat(modules.collect(toList())).isEmpty();
+ }
+
+ @Test
+ public void get_directoryContainsApks_returnsModule() throws Exception {
+ DirectoryBasedModuleInfoProvider provider =
+ createProvider(createDirectoryWithApkFiles("package1.apk", "package2.apk"));
+
+ Stream<ModuleInfo> modules = provider.get(createIConfig());
+
+ assertThat(modules.collect(toList()))
+ .comparingElementsUsing(MODULE_NAME_CORRESPONDENCE)
+ .containsExactly("package1.apk", "package2.apk");
+ }
+
+ @Test
+ public void get_directoryContainsNonApk_ignoreNonApk() throws Exception {
+ DirectoryBasedModuleInfoProvider provider =
+ createProvider(createDirectoryWithApkFiles("package.apk", "not_package.not_apk"));
+
+ Stream<ModuleInfo> modules = provider.get(createIConfig());
+
+ assertThat(modules.collect(toList()))
+ .comparingElementsUsing(MODULE_NAME_CORRESPONDENCE)
+ .containsExactly("package.apk");
+ }
+
+ @Test
+ public void get_directoryContainsApk_packageInstallFilePlaceholderIsSubstituted()
+ throws Exception {
+ String apkFileName = "package.apk";
+ Path apkDir = createDirectoryWithApkFiles(apkFileName);
+ DirectoryBasedModuleInfoProvider provider = createProvider(apkDir);
+ IConfiguration config =
+ createIConfigWithTemplate(
+ DirectoryBasedModuleInfoProvider.PACKAGE_INSTALL_FILE_PLACEHOLDER);
+
+ Stream<ModuleInfo> modules = provider.get(config);
+
+ assertThat(modules.collect(toList()))
+ .comparingElementsUsing(MODULE_CONTENT_CORRESPONDENCE)
+ .containsExactly(apkDir.resolve(apkFileName).toString());
+ }
+
+ @Test
+ public void get_directoryContainsApk_packagePlaceholderIsSubstituted() throws Exception {
+ String packageName = "package.name";
+ DirectoryBasedModuleInfoProvider provider =
+ createProviderWithParser(
+ apk -> packageName, createDirectoryWithApkFiles("package.apk"));
+ IConfiguration config =
+ createIConfigWithTemplate(DirectoryBasedModuleInfoProvider.PACKAGE_PLACEHOLDER);
+
+ Stream<ModuleInfo> modules = provider.get(config);
+
+ assertThat(modules.collect(toList()))
+ .comparingElementsUsing(MODULE_CONTENT_CORRESPONDENCE)
+ .containsExactly(packageName);
+ }
+
+ private DirectoryBasedModuleInfoProvider createProvider(Path... paths)
+ throws ConfigurationException {
+ return createProviderWithParser(apk -> "package.name", paths);
+ }
+
+ private DirectoryBasedModuleInfoProvider createProviderWithParser(
+ DirectoryBasedModuleInfoProvider.PackageNameParser parser, Path... paths)
+ throws ConfigurationException {
+ DirectoryBasedModuleInfoProvider provider = new DirectoryBasedModuleInfoProvider(parser);
+ OptionSetter optionSetter = new OptionSetter(provider);
+ for (Path dir : paths) {
+ optionSetter.setOptionValue(
+ DirectoryBasedModuleInfoProvider.DIRECTORY_OPTION, dir.toString());
+ }
+ return provider;
+ }
+
+ private Path createDirectoryWithApkFiles(String... apkFileNames) throws IOException {
+ Path tempDir = Files.createTempDirectory(tempFolder.getRoot().toPath(), "apks");
+ for (String apkFileName : apkFileNames) {
+ Files.createFile(tempDir.resolve(apkFileName));
+ }
+ return tempDir;
+ }
+
+ private IConfiguration createIConfig() throws ConfigurationException {
+ return createIConfigWithTemplate(MODULE_TEMPLATE_CONTENT);
+ }
+
+ private IConfiguration createIConfigWithTemplate(String template)
+ throws ConfigurationException {
+ IConfiguration configuration = new Configuration("name", "description");
+ configuration.setConfigurationObject(
+ ModuleTemplate.MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE,
+ createModuleTemplate(template));
+ return configuration;
+ }
+
+ private ModuleTemplate createModuleTemplate(String template) throws ConfigurationException {
+ ModuleTemplate moduleTemplate = new ModuleTemplate(resource -> template);
+ new OptionSetter(moduleTemplate)
+ .setOptionValue(ModuleTemplate.DEFAULT_TEMPLATE_OPTION, "path.xml.template");
+ new OptionSetter(moduleTemplate).setOptionValue(ModuleTemplate.TEMPLATE_ROOT_OPTION, "");
+ return moduleTemplate;
+ }
+
+ private static final Correspondence<ModuleInfo, String> MODULE_NAME_CORRESPONDENCE =
+ Correspondence.transforming(ModuleInfo::getName, "module name");
+ private static final Correspondence<ModuleInfo, String> MODULE_CONTENT_CORRESPONDENCE =
+ Correspondence.transforming(ModuleInfo::getContent, "module name");
+
+ 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/FileBasedPackageNameProviderTest.java b/harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java
deleted file mode 100644
index ee429b2..0000000
--- a/harness/src/test/java/com/android/csuite/core/FileBasedPackageNameProviderTest.java
+++ /dev/null
@@ -1,116 +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.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/FileBasedTemplateMappingProviderTest.java b/harness/src/test/java/com/android/csuite/core/FileBasedTemplateMappingProviderTest.java
new file mode 100644
index 0000000..681509b
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/FileBasedTemplateMappingProviderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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 static org.junit.Assert.assertThrows;
+
+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.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(JUnit4.class)
+public final class FileBasedTemplateMappingProviderTest {
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void get_emptyFile_returnsEmptyStream() throws Exception {
+ String filePath = createTemplateMappingFile("");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ Stream<Entry<String, String>> entries = sut.get();
+
+ assertThat(entries.collect(Collectors.toList())).isEmpty();
+ }
+
+ @Test
+ public void get_fileOptionNotSet_returnsEmptyStream() throws Exception {
+ FileBasedTemplateMappingProvider sut = new FileBasedTemplateMappingProvider();
+
+ Stream<Entry<String, String>> entries = sut.get();
+
+ assertThat(entries.collect(Collectors.toList())).isEmpty();
+ }
+
+ @Test
+ public void get_fileContainsCommentLines_ignoresComments() throws Exception {
+ String filePath =
+ createTemplateMappingFile(
+ FileBasedTemplateMappingProvider.COMMENT_LINE_PREFIX + " comments");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ Stream<Entry<String, String>> entries = sut.get();
+
+ assertThat(entries.collect(Collectors.toList())).isEmpty();
+ }
+
+ @Test
+ public void get_fileContainsEmptyLines_ignoresEmptyLines() throws Exception {
+ String filePath = createTemplateMappingFile("\n\n\n");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ Stream<Entry<String, String>> entries = sut.get();
+
+ assertThat(entries.collect(Collectors.toList())).isEmpty();
+ }
+
+ @Test
+ public void get_lineContainsTooMuchItems_throwsException() throws Exception {
+ String filePath =
+ createTemplateMappingFile(
+ "1"
+ + FileBasedTemplateMappingProvider.MODULE_TEMPLATE_SEPARATOR
+ + "2"
+ + FileBasedTemplateMappingProvider.MODULE_TEMPLATE_SEPARATOR
+ + "3");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ assertThrows(IllegalArgumentException.class, () -> sut.get());
+ }
+
+ @Test
+ public void get_lineContainsWrongSeparater_throwsException() throws Exception {
+ String filePath = createTemplateMappingFile("1::::2");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ assertThrows(IllegalArgumentException.class, () -> sut.get());
+ }
+
+ @Test
+ public void get_fileContainsValidMapping_returnsEntries() throws Exception {
+ String filePath =
+ createTemplateMappingFile(
+ "module1"
+ + FileBasedTemplateMappingProvider.MODULE_TEMPLATE_SEPARATOR
+ + "template1\n"
+ + "module2"
+ + FileBasedTemplateMappingProvider.MODULE_TEMPLATE_SEPARATOR
+ + "template2\n");
+ FileBasedTemplateMappingProvider sut = createSubjectUnderTest(filePath);
+
+ Stream<Entry<String, String>> entries = sut.get();
+
+ Map<String, String> map = convertToMap(entries);
+ assertThat(map.size()).isEqualTo(2);
+ assertThat(map.get("module1")).isEqualTo("template1");
+ assertThat(map.get("module2")).isEqualTo("template2");
+ }
+
+ public Map<String, String> convertToMap(Stream<Entry<String, String>> entryStream) {
+ HashMap<String, String> res = new HashMap<String, String>();
+ entryStream.forEach(entry -> res.put(entry.getKey(), entry.getValue()));
+ return res;
+ }
+
+ private FileBasedTemplateMappingProvider createSubjectUnderTest(String file)
+ throws ConfigurationException {
+ FileBasedTemplateMappingProvider sut = new FileBasedTemplateMappingProvider();
+ new OptionSetter(sut)
+ .setOptionValue(
+ FileBasedTemplateMappingProvider.TEMPLATE_MAPPING_FILE_OPTION, file);
+ return sut;
+ }
+
+ private String createTemplateMappingFile(String content) throws IOException {
+ Path tempFile = Files.createTempFile(tempFolder.getRoot().toPath(), "mapping", ".txt");
+ Files.write(tempFile, content.getBytes());
+ return tempFile.toString();
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/ModuleGeneratorTest.java b/harness/src/test/java/com/android/csuite/core/ModuleGeneratorTest.java
new file mode 100644
index 0000000..6949a83
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/ModuleGeneratorTest.java
@@ -0,0 +1,326 @@
+/*
+ * 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 static org.testng.Assert.assertThrows;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.IConfiguration;
+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.ImmutableList;
+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.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+
+@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 TEST_PACKAGE_NAME3 = "test.package.name3";
+ private static final Exception NO_EXCEPTION = null;
+
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+
+ @Test
+ public void tearDown_nonModuleFilesExist_doesNotDelete() throws Exception {
+ Path testsDir = createTestsDir();
+ Path nonModule = Files.createFile(testsDir.resolve("a"));
+ ModuleGenerator generator = new GeneratorBuilder().setTestsDir(testsDir).build();
+
+ generator.tearDown(createTestInfo(), NO_EXCEPTION);
+
+ assertThatListDirectory(testsDir).containsExactly(nonModule);
+ }
+
+ @Test
+ public void tearDown_nonGeneratedModuleFilesExist_doesNotDelete() throws Exception {
+ Path testsDir = createTestsDir();
+ Path nonGeneratedModule =
+ Files.createFile(
+ testsDir.resolve("b" + ModuleGenerator.MODULE_FILE_NAME_EXTENSION));
+ ModuleGenerator generator = new GeneratorBuilder().setTestsDir(testsDir).build();
+
+ generator.tearDown(createTestInfo(), NO_EXCEPTION);
+
+ assertThatListDirectory(testsDir).containsExactly(nonGeneratedModule);
+ }
+
+ @Test
+ public void tearDown_packageNamesProvided_deletesGeneratedModules() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator1 =
+ new GeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, "")))
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME2, "")))
+ .build();
+ generator1.split();
+ ModuleGenerator generator2 = new GeneratorBuilder().setTestsDir(testsDir).build();
+
+ generator2.tearDown(createTestInfo(), NO_EXCEPTION);
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ @Test
+ public void tearDown_moduleInfoNotProvided_doesNotThrowError() throws Exception {
+ ModuleGenerator generator = new GeneratorBuilder().setTestsDir(createTestsDir()).build();
+ generator.split();
+
+ generator.tearDown(createTestInfo(), NO_EXCEPTION);
+ }
+
+ @Test
+ public void split_moduleInfoStreamProvided_streamIsClosed() throws Exception {
+ AtomicBoolean wasClosed = new AtomicBoolean(false);
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(createTestsDir())
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, ""))
+ .onClose(() -> wasClosed.set(true)))
+ .build();
+
+ generator.split();
+
+ assertThat(wasClosed.get()).isTrue();
+ }
+
+ @Test
+ public void split_moduleInfoProvidersSpecified_contentIsWritten() throws Exception {
+ Path testsDir = createTestsDir();
+ String content1 = "a";
+ String content2 = "b";
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, content1)))
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME2, content2)))
+ .build();
+
+ generator.split();
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1).contains(content1);
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2).contains(content2);
+ }
+
+ @Test
+ public void split_emptyModuleNameProvided_throwsException() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addModuleInfoProvider(
+ config -> Stream.of(new ModuleInfoProvider.ModuleInfo(" ", "a")))
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_duplicatedModuleNamesProvided_throwsException() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, "a")))
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, "b")))
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_moduleInfoProvidersSpecified_generateModulesForAll() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(testsDir)
+ .addModuleInfoProvider(
+ config ->
+ Arrays.asList(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME1, ""),
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME2, ""))
+ .stream())
+ .addModuleInfoProvider(
+ config ->
+ Stream.of(
+ new ModuleInfoProvider.ModuleInfo(
+ TEST_PACKAGE_NAME3, "")))
+ .build();
+
+ generator.split();
+
+ assertThatListDirectory(testsDir)
+ .containsExactly(
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME1),
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME2),
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME3));
+ }
+
+ @Test
+ public void split_streamThrowsException_throwsException() throws Exception {
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(createTestsDir())
+ .addModuleInfoProvider(
+ config ->
+ Arrays.stream(new String[] {"a"})
+ .map(
+ i -> {
+ throw new UncheckedIOException(
+ new IOException());
+ }))
+ .build();
+
+ assertThrows(UncheckedIOException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_providerThrowsException_throwsException() throws Exception {
+ ModuleGenerator generator =
+ new GeneratorBuilder()
+ .setTestsDir(createTestsDir())
+ .addModuleInfoProvider(
+ config -> {
+ throw new UncheckedIOException(new IOException());
+ })
+ .build();
+
+ assertThrows(UncheckedIOException.class, () -> generator.split());
+ }
+
+ @Test
+ public void split_noProviders_doesNotGenerate() throws Exception {
+ Path testsDir = createTestsDir();
+ ModuleGenerator generator = new GeneratorBuilder().setTestsDir(testsDir).build();
+
+ generator.split();
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ 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 final class GeneratorBuilder {
+ private final List<ModuleInfoProvider> mModuleInfoProviders = new ArrayList<>();
+ private Path mTestsDir;
+
+ GeneratorBuilder addModuleInfoProvider(ModuleInfoProvider moduleInfoProviders) {
+ mModuleInfoProviders.add(moduleInfoProviders);
+ return this;
+ }
+
+ GeneratorBuilder setTestsDir(Path testsDir) {
+ mTestsDir = testsDir;
+ return this;
+ }
+
+ ModuleGenerator build() throws Exception {
+ ModuleGenerator generator = new ModuleGenerator(buildInfo -> mTestsDir);
+
+ IConfiguration configuration = new Configuration("name", "description");
+ configuration.setConfigurationObjectList(
+ ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE, mModuleInfoProviders);
+ generator.setConfiguration(configuration);
+
+ return generator;
+ }
+ }
+
+ 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();
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/ModuleTemplateTest.java b/harness/src/test/java/com/android/csuite/core/ModuleTemplateTest.java
new file mode 100644
index 0000000..cd3fcf2
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/ModuleTemplateTest.java
@@ -0,0 +1,284 @@
+/*
+ * 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 static org.junit.Assert.assertThrows;
+
+import com.android.csuite.core.ModuleTemplate.ResourceLoader;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.OptionSetter;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public final class ModuleTemplateTest {
+ @Test
+ public void substitute_multipleReplacementPairs_replaceAll() throws Exception {
+ String template = "-ab";
+ ModuleTemplate subject = createTestSubject(template);
+
+ String content = subject.substitute("any_name", Map.of("a", "c", "b", "d"));
+
+ assertThat(content).isEqualTo("-cd");
+ }
+
+ @Test
+ public void substitute_replacementKeyNotInTemplate_doesNotReplace() throws Exception {
+ String template = "-a";
+ ModuleTemplate subject = createTestSubject(template);
+
+ String content = subject.substitute("any_name", Map.of("b", ""));
+
+ assertThat(content).isEqualTo(template);
+ }
+
+ @Test
+ public void substitute_multipleReplacementKeyInTemplate_replaceTheKeys() throws Exception {
+ String template = "-aba";
+ ModuleTemplate subject = createTestSubject(template);
+
+ String content = subject.substitute("any_name", Map.of("a", "c"));
+
+ assertThat(content).isEqualTo("-cbc");
+ }
+
+ @Test
+ public void substitute_noReplacementPairs_returnTemplate() throws Exception {
+ String template = "-a";
+ ModuleTemplate subject = createTestSubject(template);
+
+ String content = subject.substitute("any_name", Map.of());
+
+ assertThat(content).isEqualTo(template);
+ }
+
+ @Test
+ public void substitute_templateContentIsEmpty_returnEmptyString() throws Exception {
+ String template = "";
+ ModuleTemplate subject = createTestSubject(template);
+
+ String content = subject.substitute("any_name", Map.of("a", "b"));
+
+ assertThat(content).isEqualTo(template);
+ }
+
+ @Test
+ public void substitute_templateMapsSpecified_useTemplateMaps() throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "template1"))
+ .addExtraTemplatePath("template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .setExtraTemplateContent(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION, "a")
+ .build();
+ ModuleTemplate sut = ModuleTemplate.loadFrom(config);
+
+ String content = sut.substitute("module1", Map.of("a", "b"));
+
+ assertThat(content).isEqualTo("b");
+ }
+
+ @Test
+ public void substitute_templateFileIsInADirectory_canFindTheTemplates() throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "dir/template1"))
+ .addExtraTemplatePath(
+ "dir/template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .setExtraTemplateContent(
+ "dir/template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION, "a")
+ .build();
+ ModuleTemplate sut = ModuleTemplate.loadFrom(config);
+
+ String content = sut.substitute("module1", Map.of("a", "b"));
+
+ assertThat(content).isEqualTo("b");
+ }
+
+ @Test
+ public void loadFrom_templateMappingContainsNonexistTemplates_throwsException()
+ throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "template1"))
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> ModuleTemplate.loadFrom(config));
+ }
+
+ @Test
+ public void loadFrom_templateMappingContainsExistingExtraTemplates_doesNotThrow()
+ throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "template1"))
+ .addExtraTemplatePath("template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .setExtraTemplateContent(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION, "")
+ .build();
+
+ ModuleTemplate.loadFrom(config);
+ }
+
+ @Test
+ public void loadFrom_templateMappingContainsXmlExtension_doesNotThrow() throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(
+ Map.of(
+ "module1",
+ "template1"
+ + ModuleTemplate.XML_FILE_EXTENSION.toUpperCase()))
+ .addExtraTemplatePath(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION.toLowerCase())
+ .setExtraTemplateContent(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION.toLowerCase(),
+ "")
+ .build();
+
+ ModuleTemplate.loadFrom(config);
+ }
+
+ @Test
+ public void loadFrom_templateMappingContainsCaseMismatchingXmlExtension_doesNotThrow()
+ throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(
+ Map.of("module1", "template1" + ModuleTemplate.XML_FILE_EXTENSION))
+ .addExtraTemplatePath("template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .setExtraTemplateContent(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION, "")
+ .build();
+
+ ModuleTemplate.loadFrom(config);
+ }
+
+ @Test
+ public void loadFrom_templateMappingContainsDefaultTemplate_doesNotThrow() throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "default_template"))
+ .build();
+
+ ModuleTemplate.loadFrom(config);
+ }
+
+ @Test
+ public void loadFrom_duplicateTemplateMappingEntries_throwsException() throws Exception {
+ IConfiguration config =
+ new ConfigurationBuilder(
+ "", "default_template" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .addTemplateMappings(Map.of("module1", "template1"))
+ .addTemplateMappings(Map.of("module1", "template1"))
+ .addExtraTemplatePath("template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .setExtraTemplateContent(
+ "template1" + ModuleTemplate.TEMPLATE_FILE_EXTENSION, "")
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> ModuleTemplate.loadFrom(config));
+ }
+
+ private static ModuleTemplate createTestSubject(String defaultTemplate)
+ throws IOException, ConfigurationException {
+ return ModuleTemplate.loadFrom(
+ new ConfigurationBuilder(
+ defaultTemplate,
+ "any_path" + ModuleTemplate.TEMPLATE_FILE_EXTENSION)
+ .build());
+ }
+
+ private static final class ConfigurationBuilder {
+ private final String mDefaultTemplateContent;
+ private final String mDefaultTemplatePath;
+ private String mTemplateRoot = "";
+ private Map<String, String> mExtraTemplateContents = new HashMap<>();
+ private List<String> mExtraTemplatePaths = new ArrayList<>();
+ private List<Map<String, String>> mTemplateMappings = new ArrayList<>();
+
+ ConfigurationBuilder(String defaultTemplateContent, String defaultTemplatePath) {
+ mDefaultTemplateContent = defaultTemplateContent;
+ mDefaultTemplatePath = defaultTemplatePath;
+ }
+
+ ConfigurationBuilder setExtraTemplateContent(String path, String content) {
+ mExtraTemplateContents.put(path, content);
+ return this;
+ }
+
+ ConfigurationBuilder addExtraTemplatePath(String path) {
+ mExtraTemplatePaths.add(path);
+ return this;
+ }
+
+ ConfigurationBuilder addTemplateMappings(Map<String, String> map) {
+ mTemplateMappings.add(map);
+ return this;
+ }
+
+ IConfiguration build() throws ConfigurationException {
+ IConfiguration configuration = new Configuration("name", "description");
+
+ ResourceLoader resourceLoader =
+ path -> {
+ if (mExtraTemplateContents.containsKey(path)) {
+ return mExtraTemplateContents.get(path);
+ }
+ return mDefaultTemplateContent;
+ };
+
+ ModuleTemplate moduleTemplate = new ModuleTemplate(resourceLoader);
+ OptionSetter optionSetter = new OptionSetter(moduleTemplate);
+ optionSetter.setOptionValue(
+ ModuleTemplate.DEFAULT_TEMPLATE_OPTION, mDefaultTemplatePath);
+ optionSetter.setOptionValue(ModuleTemplate.TEMPLATE_ROOT_OPTION, mTemplateRoot);
+ for (String extraTemplate : mExtraTemplatePaths) {
+ optionSetter.setOptionValue(ModuleTemplate.EXTRA_TEMPLATES_OPTION, extraTemplate);
+ }
+ configuration.setConfigurationObject(
+ ModuleTemplate.MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE, moduleTemplate);
+
+ if (!mTemplateMappings.isEmpty()) {
+ List<TemplateMappingProvider> list = new ArrayList<>();
+ mTemplateMappings.forEach(map -> list.add(() -> map.entrySet().stream()));
+ configuration.setConfigurationObjectList(
+ TemplateMappingProvider.TEMPLATE_MAPPING_PROVIDER_OBJECT_TYPE, list);
+ }
+
+ return configuration;
+ }
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/core/PackageModuleInfoProviderTest.java b/harness/src/test/java/com/android/csuite/core/PackageModuleInfoProviderTest.java
new file mode 100644
index 0000000..9c1c360
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/PackageModuleInfoProviderTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+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.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(JUnit4.class)
+public final class PackageModuleInfoProviderTest {
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void get_templateContainsPlaceholders_replacesPlaceholdersInOutput() throws Exception {
+ String content = "hello placeholder%s%s world";
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackageModuleInfoProvider provider =
+ new ProviderBuilder().addPackage(packageName1).addPackage(packageName2).build();
+ IConfiguration config =
+ createIConfigWithTemplate(
+ String.format(
+ content,
+ PackagesFileModuleInfoProvider.PACKAGE_PLACEHOLDER,
+ PackagesFileModuleInfoProvider.PACKAGE_PLACEHOLDER));
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(config);
+
+ assertThat(collectModuleContentStrings(modulesInfo))
+ .containsExactly(
+ String.format(content, packageName1, packageName1),
+ String.format(content, packageName2, packageName2));
+ }
+
+ @Test
+ public void get_containsDuplicatedPackageNames_ignoreDuplicates() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackageModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackage(packageName1)
+ .addPackage(packageName1)
+ .addPackage(packageName2)
+ .build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).containsExactly(packageName1, packageName2);
+ }
+
+ @Test
+ public void get_packageNamesProvided_returnsPackageNames() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackageModuleInfoProvider provider =
+ new ProviderBuilder().addPackage(packageName1).addPackage(packageName2).build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).containsExactly(packageName1, packageName2);
+ }
+
+ private List<String> collectModuleContentStrings(
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo) {
+ return modulesInfo
+ .map(ModuleInfoProvider.ModuleInfo::getContent)
+ .collect(Collectors.toList());
+ }
+
+ private List<String> collectModuleNames(Stream<ModuleInfoProvider.ModuleInfo> modulesInfo) {
+ return modulesInfo.map(ModuleInfoProvider.ModuleInfo::getName).collect(Collectors.toList());
+ }
+
+ private static final class ProviderBuilder {
+ private final Set<String> mPackages = new HashSet<>();
+
+ ProviderBuilder addPackage(String packageName) {
+ mPackages.add(packageName);
+ return this;
+ }
+
+ PackageModuleInfoProvider build() throws Exception {
+ PackageModuleInfoProvider provider = new PackageModuleInfoProvider();
+
+ OptionSetter optionSetter = new OptionSetter(provider);
+ for (String p : mPackages) {
+ optionSetter.setOptionValue(PackageModuleInfoProvider.PACKAGE_OPTION, p);
+ }
+ return provider;
+ }
+ }
+
+ private IConfiguration createIConfig() throws ConfigurationException {
+ return createIConfigWithTemplate(MODULE_TEMPLATE_CONTENT);
+ }
+
+ private IConfiguration createIConfigWithTemplate(String template)
+ throws ConfigurationException {
+ IConfiguration configuration = new Configuration("name", "description");
+ configuration.setConfigurationObject(
+ ModuleTemplate.MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE,
+ createModuleTemplate(template));
+ return configuration;
+ }
+
+ private ModuleTemplate createModuleTemplate(String template) throws ConfigurationException {
+ ModuleTemplate moduleTemplate = new ModuleTemplate(resource -> template);
+ new OptionSetter(moduleTemplate)
+ .setOptionValue(ModuleTemplate.DEFAULT_TEMPLATE_OPTION, "path.xml.template");
+ new OptionSetter(moduleTemplate).setOptionValue(ModuleTemplate.TEMPLATE_ROOT_OPTION, "");
+ return moduleTemplate;
+ }
+
+ 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/PackagesFileModuleInfoProviderTest.java b/harness/src/test/java/com/android/csuite/core/PackagesFileModuleInfoProviderTest.java
new file mode 100644
index 0000000..a4757ba
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/PackagesFileModuleInfoProviderTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+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.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@RunWith(JUnit4.class)
+public final class PackagesFileModuleInfoProviderTest {
+ @Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Test
+ public void get_templateContainsPlaceholders_replacesPlaceholdersInOutput() throws Exception {
+ String content = "hello placeholder%s%s world";
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackagesFileModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackagesFile(createPackagesFile(packageName1 + "\n" + packageName2))
+ .build();
+ IConfiguration config =
+ createIConfigWithTemplate(
+ String.format(
+ content,
+ PackagesFileModuleInfoProvider.PACKAGE_PLACEHOLDER,
+ PackagesFileModuleInfoProvider.PACKAGE_PLACEHOLDER));
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(config);
+
+ assertThat(collectModuleContentStrings(modulesInfo))
+ .containsExactly(
+ String.format(content, packageName1, packageName1),
+ String.format(content, packageName2, packageName2));
+ }
+
+ @Test
+ public void get_containsDuplicatedPackageNames_ignoreDuplicates() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackagesFileModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackagesFile(createPackagesFile(packageName1 + "\n" + packageName1))
+ .addPackagesFile(createPackagesFile(packageName1 + "\n" + packageName2))
+ .build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).containsExactly(packageName1, packageName2);
+ }
+
+ @Test
+ public void get_fileNotSpecified_returnsEmptySet() throws Exception {
+ PackagesFileModuleInfoProvider provider = new ProviderBuilder().build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).isEmpty();
+ }
+
+ @Test
+ public void get_multipleFileSpecified_returnsAllEntries() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ String packageName3 = "c";
+ String packageName4 = "d";
+ PackagesFileModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackagesFile(createPackagesFile(packageName1 + "\n" + packageName2))
+ .addPackagesFile(createPackagesFile(packageName3 + "\n" + packageName4))
+ .build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo))
+ .containsExactly(packageName1, packageName2, packageName3, packageName4);
+ }
+
+ @Test
+ public void get_fileContainsEmptyLines_ignoresEmptyLines() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackagesFileModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackagesFile(
+ createPackagesFile(packageName1 + "\n \n\n" + packageName2))
+ .build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).containsExactly(packageName1, packageName2);
+ }
+
+ @Test
+ public void get_fileContainsCommentLines_ignoresCommentLines() throws Exception {
+ String packageName1 = "a";
+ String packageName2 = "b";
+ PackagesFileModuleInfoProvider provider =
+ new ProviderBuilder()
+ .addPackagesFile(
+ createPackagesFile(
+ packageName1
+ + "\n"
+ + PackagesFileModuleInfoProvider.COMMENT_LINE_PREFIX
+ + " Some comments\n"
+ + packageName2))
+ .build();
+
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = provider.get(createIConfig());
+
+ assertThat(collectModuleNames(modulesInfo)).containsExactly(packageName1, packageName2);
+ }
+
+ private List<String> collectModuleContentStrings(
+ Stream<ModuleInfoProvider.ModuleInfo> modulesInfo) {
+ return modulesInfo
+ .map(ModuleInfoProvider.ModuleInfo::getContent)
+ .collect(Collectors.toList());
+ }
+
+ private List<String> collectModuleNames(Stream<ModuleInfoProvider.ModuleInfo> modulesInfo) {
+ return modulesInfo.map(ModuleInfoProvider.ModuleInfo::getName).collect(Collectors.toList());
+ }
+
+ private Path createPackagesFile(String content) throws IOException {
+ Path tempFile = Files.createTempFile(tempFolder.getRoot().toPath(), "packages", ".txt");
+ Files.write(tempFile, content.getBytes());
+ return tempFile;
+ }
+
+ private static final class ProviderBuilder {
+ private final Set<Path> mPackagesFiles = new HashSet<>();
+
+ ProviderBuilder addPackagesFile(Path packagesFile) {
+ mPackagesFiles.add(packagesFile);
+ return this;
+ }
+
+ PackagesFileModuleInfoProvider build() throws Exception {
+ PackagesFileModuleInfoProvider provider = new PackagesFileModuleInfoProvider();
+
+ OptionSetter optionSetter = new OptionSetter(provider);
+ for (Path p : mPackagesFiles) {
+ optionSetter.setOptionValue(
+ PackagesFileModuleInfoProvider.PACKAGES_FILE_OPTION, p.toString());
+ }
+ return provider;
+ }
+ }
+
+ private IConfiguration createIConfig() throws ConfigurationException {
+ return createIConfigWithTemplate(MODULE_TEMPLATE_CONTENT);
+ }
+
+ private IConfiguration createIConfigWithTemplate(String template)
+ throws ConfigurationException {
+ IConfiguration configuration = new Configuration("name", "description");
+ configuration.setConfigurationObject(
+ ModuleTemplate.MODULE_TEMPLATE_PROVIDER_OBJECT_TYPE,
+ createModuleTemplate(template));
+ return configuration;
+ }
+
+ private ModuleTemplate createModuleTemplate(String template) throws ConfigurationException {
+ ModuleTemplate moduleTemplate = new ModuleTemplate(resource -> template);
+ new OptionSetter(moduleTemplate)
+ .setOptionValue(ModuleTemplate.DEFAULT_TEMPLATE_OPTION, "path.xml.template");
+ new OptionSetter(moduleTemplate).setOptionValue(ModuleTemplate.TEMPLATE_ROOT_OPTION, "");
+ return moduleTemplate;
+ }
+
+ 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/SystemAppUninstallerTest.java b/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java
index 30b77db..1a8cb84 100644
--- a/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java
+++ b/harness/src/test/java/com/android/csuite/core/SystemAppUninstallerTest.java
@@ -33,7 +33,6 @@ 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 ";
@@ -154,7 +153,7 @@ public final class SystemAppUninstallerTest {
@Test
public void uninstallPackage_adbRootCommandFailed_throws() throws Exception {
ITestDevice device = createGoodDeviceWithSystemAppInstalled();
- Mockito.when(device.enableAdbRoot()).thenThrow(new DeviceNotAvailableException());
+ Mockito.when(device.enableAdbRoot()).thenThrow(new DeviceNotAvailableException("", ""));
assertThrows(
DeviceNotAvailableException.class,
@@ -174,7 +173,7 @@ public final class SystemAppUninstallerTest {
@Test
public void uninstallPackage_adbDisableRootCommandFailed_throws() throws Exception {
ITestDevice device = createGoodDeviceWithSystemAppInstalled();
- Mockito.when(device.disableAdbRoot()).thenThrow(new DeviceNotAvailableException());
+ Mockito.when(device.disableAdbRoot()).thenThrow(new DeviceNotAvailableException("", ""));
assertThrows(
DeviceNotAvailableException.class,
@@ -194,7 +193,9 @@ public final class SystemAppUninstallerTest {
@Test
public void uninstallPackage_adbRemountFailed_throws() throws Exception {
ITestDevice device = createGoodDeviceWithSystemAppInstalled();
- Mockito.doThrow(new DeviceNotAvailableException()).when(device).remountSystemWritable();
+ Mockito.doThrow(new DeviceNotAvailableException("", ""))
+ .when(device)
+ .remountSystemWritable();
assertThrows(
DeviceNotAvailableException.class,
diff --git a/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java b/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java
new file mode 100644
index 0000000..292c0e2
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/TestUtilsTest.java
@@ -0,0 +1,325 @@
+/*
+ * 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.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.csuite.core.TestUtils.TestArtifactReceiver;
+import com.android.tradefed.build.BuildInfo;
+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.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+
+import com.google.common.jimfs.Jimfs;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+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.Mockito;
+
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(JUnit4.class)
+public final class TestUtilsTest {
+ private final TestArtifactReceiver mMockTestArtifactReceiver =
+ Mockito.mock(TestArtifactReceiver.class);
+ private final ITestDevice mMockDevice = mock(ITestDevice.class);
+ private final DeviceUtils mMockDeviceUtils = Mockito.mock(DeviceUtils.class);
+ private static final String TEST_PACKAGE_NAME = "package_name";
+ @Rule public final TemporaryFolder mTempFolder = new TemporaryFolder();
+ private final FileSystem mFileSystem =
+ Jimfs.newFileSystem(com.google.common.jimfs.Configuration.unix());
+
+ @Test
+ public void listApks_withSplitApksInSubDirectory_returnsApks() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createDirectories(root.resolve("sub"));
+ Files.createFile(root.resolve("sub").resolve("base.apk"));
+ Files.createFile(root.resolve("sub").resolve("config.apk"));
+
+ List<Path> res = TestUtils.listApks(root);
+
+ List<String> fileNames =
+ res.stream()
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ assertThat(fileNames).containsExactly("base.apk", "config.apk");
+ }
+
+ @Test
+ public void listApks_withSingleSplitApkDirectory_returnsApks() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+
+ List<Path> res = TestUtils.listApks(root);
+
+ List<String> fileNames =
+ res.stream()
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ assertThat(fileNames).containsExactly("base.apk");
+ }
+
+ @Test
+ public void listApks_withSplitApkDirectory_returnsListWithBaseApkAsTheFirstElement()
+ throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("base.apk"));
+ Files.createFile(root.resolve("a.apk"));
+ Files.createFile(root.resolve("b.apk"));
+ Files.createFile(root.resolve("c.apk"));
+
+ List<Path> res = TestUtils.listApks(root);
+
+ assertThat(res.get(0).getFileName().toString()).isEqualTo("base.apk");
+ }
+
+ @Test
+ public void listApks_withSingleApkDirectory_returnsApks() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.apk"));
+
+ List<Path> res = TestUtils.listApks(root);
+
+ List<String> fileNames =
+ res.stream()
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ assertThat(fileNames).containsExactly("single.apk");
+ }
+
+ @Test
+ public void listApks_withSingleApkFile_returnsApks() throws Exception {
+ Path root = mFileSystem.getPath("single.apk");
+ Files.createFile(root);
+
+ List<Path> res = TestUtils.listApks(root);
+
+ List<String> fileNames =
+ res.stream()
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ assertThat(fileNames).containsExactly("single.apk");
+ }
+
+ @Test
+ public void listApks_withApkDirectoryContainingOtherFileTypes_returnsApksOnly()
+ throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.apk"));
+ Files.createFile(root.resolve("single.not_apk"));
+
+ List<Path> res = TestUtils.listApks(root);
+
+ List<String> fileNames =
+ res.stream()
+ .map(Path::getFileName)
+ .map(Path::toString)
+ .collect(Collectors.toList());
+ assertThat(fileNames).containsExactly("single.apk");
+ }
+
+ @Test
+ public void listApks_withApkDirectoryContainingNoApks_throwException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createFile(root.resolve("single.not_apk"));
+
+ assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root));
+ }
+
+ @Test
+ public void listApks_withNonApkFile_throwException() throws Exception {
+ Path root = mFileSystem.getPath("single.not_apk");
+ Files.createFile(root);
+
+ assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root));
+ }
+
+ @Test
+ public void listApks_withApksInMultipleDirectories_throwException() throws Exception {
+ Path root = mFileSystem.getPath("apk");
+ Files.createDirectories(root);
+ Files.createDirectories(root.resolve("1"));
+ Files.createDirectories(root.resolve("2"));
+ Files.createFile(root.resolve("1").resolve("single.apk"));
+ Files.createFile(root.resolve("2").resolve("single.apk"));
+
+ assertThrows(TestUtils.TestUtilsException.class, () -> TestUtils.listApks(root));
+ }
+
+ @Test
+ public void collectScreenshot_savesToTestLog() throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ InputStreamSource screenshotData = new FileInputStreamSource(mTempFolder.newFile());
+ when(mMockDevice.getScreenshot()).thenReturn(screenshotData);
+ when(mMockDevice.getSerialNumber()).thenReturn("SERIAL");
+
+ sut.collectScreenshot(TEST_PACKAGE_NAME);
+
+ Mockito.verify(mMockTestArtifactReceiver, times(1))
+ .addTestArtifact(
+ Mockito.contains("screenshot"),
+ Mockito.any(),
+ Mockito.any(InputStreamSource.class));
+ }
+
+ @Test
+ public void getDropboxPackageCrashLog_noEntries_returnsNull() throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ when(mMockDeviceUtils.getDropboxEntries(Mockito.any())).thenReturn(List.of());
+ DeviceTimestamp startTime = new DeviceTimestamp(0);
+
+ String result = sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, false);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void getDropboxPackageCrashLog_noEntries_doesNotSaveOutput() throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ when(mMockDeviceUtils.getDropboxEntries(Mockito.any())).thenReturn(List.of());
+ DeviceTimestamp startTime = new DeviceTimestamp(0);
+ boolean saveToFile = true;
+
+ sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, saveToFile);
+
+ Mockito.verify(mMockTestArtifactReceiver, Mockito.never())
+ .addTestArtifact(
+ Mockito.contains("dropbox"), Mockito.any(), Mockito.any(byte[].class));
+ }
+
+ @Test
+ public void getDropboxPackageCrashLog_appCrashed_saveOutput() throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ when(mMockDeviceUtils.getDropboxEntries(Mockito.any()))
+ .thenReturn(
+ List.of(
+ new DeviceUtils.DropboxEntry(
+ 2,
+ DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .toArray(
+ new String
+ [DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .size()])[0],
+ TEST_PACKAGE_NAME)));
+ DeviceTimestamp startTime = new DeviceTimestamp(0);
+ boolean saveToFile = true;
+
+ sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, saveToFile);
+
+ Mockito.verify(mMockTestArtifactReceiver, Mockito.times(1))
+ .addTestArtifact(
+ Mockito.contains("dropbox"), Mockito.any(), Mockito.any(byte[].class));
+ }
+
+ @Test
+ public void getDropboxPackageCrashLog_containsOldEntries_onlyReturnsNewEntries()
+ throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ DeviceTimestamp startTime = new DeviceTimestamp(1);
+ when(mMockDeviceUtils.getDropboxEntries(Mockito.any()))
+ .thenReturn(
+ List.of(
+ new DeviceUtils.DropboxEntry(
+ 0,
+ DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .toArray(
+ new String
+ [DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .size()])[0],
+ TEST_PACKAGE_NAME + "entry1"),
+ new DeviceUtils.DropboxEntry(
+ 2,
+ DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .toArray(
+ new String
+ [DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .size()])[0],
+ TEST_PACKAGE_NAME + "entry2")));
+
+ String result = sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, false);
+
+ assertThat(result).doesNotContain("entry1");
+ assertThat(result).contains("entry2");
+ }
+
+ @Test
+ public void getDropboxPackageCrashLog_containsOtherProcessEntries_onlyReturnsPackageEntries()
+ throws Exception {
+ TestUtils sut = createSubjectUnderTest();
+ DeviceTimestamp startTime = new DeviceTimestamp(1);
+ when(mMockDeviceUtils.getDropboxEntries(Mockito.any()))
+ .thenReturn(
+ List.of(
+ new DeviceUtils.DropboxEntry(
+ 2,
+ DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .toArray(
+ new String
+ [DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .size()])[0],
+ "other.package" + "entry1"),
+ new DeviceUtils.DropboxEntry(
+ 2,
+ DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .toArray(
+ new String
+ [DeviceUtils.DROPBOX_APP_CRASH_TAGS
+ .size()])[0],
+ TEST_PACKAGE_NAME + "entry2")));
+
+ String result = sut.getDropboxPackageCrashLog(TEST_PACKAGE_NAME, startTime, false);
+
+ assertThat(result).doesNotContain("entry1");
+ assertThat(result).contains("entry2");
+ }
+
+ private TestUtils createSubjectUnderTest() {
+ return new TestUtils(createTestInfo(), mMockTestArtifactReceiver, mMockDeviceUtils);
+ }
+
+ private TestInformation createTestInfo() {
+ IInvocationContext context = new InvocationContext();
+ context.addAllocatedDevice("device1", mMockDevice);
+ context.addDeviceBuildInfo("device1", new BuildInfo());
+ return TestInformation.newBuilder().setInvocationContext(context).build();
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java b/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java
index 306dad4..3e6df23 100644
--- a/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java
+++ b/harness/src/test/java/com/android/csuite/testing/MoreAsserts.java
@@ -38,8 +38,6 @@ public final class MoreAsserts {
* <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}
*/
diff --git a/instrumentation/launch/src/main/AndroidManifest.xml b/instrumentation/launch/src/main/AndroidManifest.xml
deleted file mode 100644
index 7d346b5..0000000
--- a/instrumentation/launch/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2012 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.compatibilitytest">
- <uses-sdk android:minSdkVersion="21"
- android:targetSdkVersion="21" />
- <application />
- <uses-permission android:name="android.permission.READ_LOGS" />
- <uses-permission android:name="android.permission.REAL_GET_TASKS" />
- <instrumentation
- android:name=".AppCompatibilityRunner"
- android:targetPackage="com.android.compatibilitytest"
- android:label="App Compatibility Test Runner" />
-</manifest>
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
deleted file mode 100644
index 870076c..0000000
--- a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibility.java
+++ /dev/null
@@ -1,371 +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.compatibilitytest;
-
-import android.app.ActivityManager;
-import android.app.ActivityManager.ProcessErrorStateInfo;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.app.IActivityController;
-import android.app.IActivityManager;
-import android.app.Instrumentation;
-import android.app.UiAutomation;
-import android.app.UiModeManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.os.DropBoxManager;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.util.Log;
-import 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;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.IntStream;
-
-/** Application Compatibility Test that launches an application and detects crashes. */
-@RunWith(AndroidJUnit4.class)
-public final class AppCompatibility {
-
- private static final String TAG = AppCompatibility.class.getSimpleName();
- private static final String PACKAGE_TO_LAUNCH = "package_to_launch";
- private static final String APP_LAUNCH_TIMEOUT_MSECS = "app_launch_timeout_ms";
- private static final String 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;
-
- private Context mContext;
- private ActivityManager mActivityManager;
- private PackageManager mPackageManager;
- private Bundle mArgs;
- private Instrumentation mInstrumentation;
- private String mLauncherPackageName;
- private IActivityController mCrashSupressor = new CrashSuppressor();
- private Map<String, List<String>> mAppErrors = new HashMap<>();
-
- static {
- DROPBOX_TAGS.add("SYSTEM_TOMBSTONE");
- DROPBOX_TAGS.add("system_app_anr");
- DROPBOX_TAGS.add("system_app_native_crash");
- DROPBOX_TAGS.add("system_app_crash");
- DROPBOX_TAGS.add("data_app_anr");
- DROPBOX_TAGS.add("data_app_native_crash");
- DROPBOX_TAGS.add("data_app_crash");
- }
-
- @Before
- public void setUp() throws Exception {
- mInstrumentation = InstrumentationRegistry.getInstrumentation();
-
- // Get permissions for privileged device operations.
- mInstrumentation.getUiAutomation().adoptShellPermissionIdentity();
-
- mContext = InstrumentationRegistry.getTargetContext();
- mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
- mPackageManager = mContext.getPackageManager();
- mArgs = InstrumentationRegistry.getArguments();
-
- // resolve launcher package name
- Intent intent = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
- ResolveInfo resolveInfo =
- mPackageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
- mLauncherPackageName = resolveInfo.activityInfo.packageName;
- Assert.assertNotNull("failed to resolve package name for launcher", mLauncherPackageName);
- Log.v(TAG, "Using launcher package name: " + mLauncherPackageName);
-
- // Parse optional inputs.
- String appLaunchTimeoutMsecs = mArgs.getString(APP_LAUNCH_TIMEOUT_MSECS);
- if (appLaunchTimeoutMsecs != null) {
- mAppLaunchTimeout = Integer.parseInt(appLaunchTimeoutMsecs);
- }
- mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
-
- // set activity controller to suppress crash dialogs and collects them by process name
- mAppErrors.clear();
- IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
- .setActivityController(mCrashSupressor, false);
- }
-
- @After
- public void tearDown() throws Exception {
- // unset activity controller
- IActivityManager.Stub.asInterface(ServiceManager.checkService(Context.ACTIVITY_SERVICE))
- .setActivityController(null, false);
- mInstrumentation.getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
- }
-
- /**
- * Actual test case that launches the package and throws an exception on the first error.
- *
- * @throws Exception
- */
- @Test
- public void testAppStability() throws Exception {
- String packageName = mArgs.getString(PACKAGE_TO_LAUNCH);
- 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));
- }
- 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));
- }
-
- /**
- * Truncate the text to at most the specified number of lines, and append a marker at the end
- * when truncated
- *
- * @param text
- * @param maxLines
- * @return
- */
- private static String truncate(String text, int maxLines) {
- String[] lines = text.split("\\r?\\n");
- StringBuilder ret = new StringBuilder();
- for (int i = 0; i < maxLines && i < lines.length; i++) {
- ret.append(lines[i]);
- ret.append('\n');
- }
- if (lines.length > maxLines) {
- ret.append("... ");
- ret.append(lines.length - maxLines);
- ret.append(" more lines truncated ...\n");
- }
- return ret.toString();
- }
-
- /**
- * Check dropbox for entries of interest regarding the specified process
- *
- * @param startTime if not 0, only check entries with timestamp later than the start time
- * @param processName the process name to check for
- */
- private void checkDropbox(long startTime, String processName) {
- DropBoxManager dropbox =
- (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE);
- DropBoxManager.Entry entry = null;
- while (null != (entry = dropbox.getNextEntry(null, startTime))) {
- try {
- // only check entries with tag that's of interest
- String tag = entry.getTag();
- if (DROPBOX_TAGS.contains(tag)) {
- String content = entry.getText(4096);
- if (content != null) {
- if (content.contains(processName)) {
- addProcessError(processName, "dropbox:" + tag, content);
- }
- }
- }
- startTime = entry.getTimeMillis();
- } finally {
- entry.close();
- }
- }
- }
-
- private Intent getLaunchIntentForPackage(String packageName) {
- UiModeManager umm = (UiModeManager) mContext.getSystemService(Context.UI_MODE_SERVICE);
- boolean isLeanback = umm.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
- Intent intent = null;
- if (isLeanback) {
- intent = mPackageManager.getLeanbackLaunchIntentForPackage(packageName);
- } else {
- intent = mPackageManager.getLaunchIntentForPackage(packageName);
- }
- return intent;
- }
-
- /**
- * Launches and activity and queries for errors.
- *
- * @param packageName {@link String} the package name of the application to launch.
- * @return {@link Collection} of {@link ProcessErrorStateInfo} detected during the app launch.
- */
- private void launchActivity(String packageName, Intent intent) {
- Log.d(
- TAG,
- String.format(
- "launching package \"%s\" with intent: %s",
- packageName, intent.toString()));
-
- // Launch Activity
- mContext.startActivity(intent);
-
- try {
- // artificial delay: in case app crashes after doing some work during launch
- Thread.sleep(mAppLaunchTimeout);
- } catch (InterruptedException e) {
- // ignore
- }
- }
-
- private void addProcessError(String processName, String errorType, String errorInfo) {
- // parse out the package name if necessary, for apps with multiple processes
- String pkgName = processName.split(":", 2)[0];
- List<String> errors;
- if (mAppErrors.containsKey(pkgName)) {
- errors = mAppErrors.get(pkgName);
- } else {
- errors = new ArrayList<>();
- }
- errors.add(String.format("### Type: %s, Details:\n%s", errorType, errorInfo));
- mAppErrors.put(pkgName, errors);
- }
-
- /**
- * Determine if a given package is still running.
- *
- * @param packageName {@link String} package to look for
- * @return True if package is running, false otherwise.
- */
- private boolean processStillUp(String packageName) {
- @SuppressWarnings("deprecation")
- List<RunningTaskInfo> infos = mActivityManager.getRunningTasks(100);
- for (RunningTaskInfo info : infos) {
- if (info.baseActivity.getPackageName().equals(packageName)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * An {@link IActivityController} that instructs framework to kill processes hitting crashes
- * directly without showing crash dialogs
- */
- private class CrashSuppressor extends IActivityController.Stub {
-
- @Override
- public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
- Log.d(TAG, "activity starting: " + intent.getComponent().toShortString());
- return true;
- }
-
- @Override
- public boolean activityResuming(String pkg) throws RemoteException {
- Log.d(TAG, "activity resuming: " + pkg);
- return true;
- }
-
- @Override
- public boolean appCrashed(
- String processName,
- int pid,
- String shortMsg,
- String longMsg,
- long timeMillis,
- String stackTrace)
- throws RemoteException {
- Log.d(TAG, "app crash: " + processName);
- addProcessError(processName, "crash", stackTrace);
- // don't show dialog
- return false;
- }
-
- @Override
- public int appEarlyNotResponding(String processName, int pid, String annotation)
- throws RemoteException {
- // ignore
- return 0;
- }
-
- @Override
- public int appNotResponding(String processName, int pid, String processStats)
- throws RemoteException {
- Log.d(TAG, "app ANR: " + processName);
- addProcessError(processName, "ANR", processStats);
- // don't show dialog
- return -1;
- }
-
- @Override
- public int systemNotResponding(String msg) throws RemoteException {
- // ignore
- return -1;
- }
- }
-}
diff --git a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java b/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
deleted file mode 100644
index 943cea9..0000000
--- a/instrumentation/launch/src/main/java/com/android/compatibilitytest/AppCompatibilityRunner.java
+++ /dev/null
@@ -1,22 +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.compatibilitytest;
-
-import androidx.test.runner.AndroidJUnitRunner;
-
-// empty subclass to maintain backwards compatibility on host-side harness
-public class AppCompatibilityRunner extends AndroidJUnitRunner {}
diff --git a/integration_tests/Android.bp b/integration_tests/Android.bp
index 84eaa2e..f4c703b 100644
--- a/integration_tests/Android.bp
+++ b/integration_tests/Android.bp
@@ -38,23 +38,21 @@ java_genrule_host {
"cp $${ANDROID_HOST_OUT}/framework/csuite-tradefed.jar $${CSUITE_TOOLS} && " +
"cp $(location :tradefed) $${CSUITE_TOOLS} && " +
"cp $(location :compatibility-host-util) $${CSUITE_TOOLS} && " +
+ "cp $(location :csuite-app-launch) $${CSUITE_TESTCASES} && " +
// 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",
+ ":csuite-app-launch",
],
tools: [
"soong_zip",
":csuite-tradefed",
- ":csuite_generate_module",
],
}
diff --git a/integration_tests/csuite_cli_test.py b/integration_tests/csuite_cli_test.py
index 84925e2..7d67542 100644
--- a/integration_tests/csuite_cli_test.py
+++ b/integration_tests/csuite_cli_test.py
@@ -25,8 +25,10 @@ class CSuiteCliTest(csuite_test_utils.TestCase):
completed_process = harness.run_and_wait(
['run', 'commandAndExit', 'version'])
- self.assertEqual(0, completed_process.returncode)
- self.assertIn('App Compatibility Test Suite', completed_process.stdout)
+ self.assertEqual(0, completed_process.returncode,
+ msg=str(completed_process))
+ self.assertIn('App Compatibility Test Suite', completed_process.stdout,
+ msg=str(completed_process))
if __name__ == '__main__':
diff --git a/integration_tests/csuite_crash_detection_test.py b/integration_tests/csuite_crash_detection_test.py
index 9dd8a00..b830a6b 100644
--- a/integration_tests/csuite_crash_detection_test.py
+++ b/integration_tests/csuite_crash_detection_test.py
@@ -39,9 +39,11 @@ class CrashDetectionTest(csuite_test_utils.TestCase):
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)
+ self.expect_regex(completed_process.stdout, r"""PASSED\s*:\s*1""",
+ msg=str(completed_process))
+ self.expect_app_launched(test_app_package, msg=str(completed_process))
+ self.expect_package_not_installed(test_app_package,
+ msg=str(completed_process))
def test_crash_on_launch_test_fails(self):
test_app_package = 'android.csuite.crashonlaunchtestapp'
@@ -51,9 +53,11 @@ class CrashDetectionTest(csuite_test_utils.TestCase):
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)
+ self.expect_regex(completed_process.stdout, r"""FAILED\s*:\s*1""",
+ msg=str(completed_process))
+ self.expect_app_launched(test_app_package, msg=str(completed_process))
+ self.expect_package_not_installed(test_app_package,
+ msg=str(completed_process))
def run_test(self, test_app_package, test_app_module):
"""Set up and run the launcher for a given test app."""
@@ -63,7 +67,6 @@ class CrashDetectionTest(csuite_test_utils.TestCase):
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))
@@ -74,30 +77,30 @@ class CrashDetectionTest(csuite_test_utils.TestCase):
csuite_test_utils.get_device_serial(),
'run',
'commandAndExit',
- 'launch',
- '-m',
- module_name,
+ 'csuite-app-launch',
'--enable-module-dynamic-download',
'--dynamic-download-args',
'%s:uri-template=file://%s/{package}' %
- (file_resolver_class, self.repo.get_path())
+ (file_resolver_class, self.repo.get_path()),
+ '--package',
+ test_app_package
])
- def expect_regex(self, s, regex):
+ def expect_regex(self, s, regex, msg=None):
with self.subTest():
- self.assertRegex(s, regex)
+ self.assertRegex(s, regex, msg=msg)
- def assert_package_not_installed(self, package_name):
- self.assertNotIn(package_name, self.adb.list_packages())
+ def assert_package_not_installed(self, package_name, msg=None):
+ self.assertNotIn(package_name, self.adb.list_packages(), msg=msg)
- def expect_package_not_installed(self, package_name):
+ def expect_package_not_installed(self, package_name, msg=None):
with self.subTest():
- self.assert_package_not_installed(package_name)
+ self.assert_package_not_installed(package_name, msg=msg)
- def expect_app_launched(self, tag):
+ def expect_app_launched(self, tag, msg=None):
logcat_process = self.adb.run(['logcat', '-d', '-v', 'brief', '-s', tag])
with self.subTest():
- self.assertIn('App launched', logcat_process.stdout)
+ self.assertIn('App launched', logcat_process.stdout, msg=msg)
if __name__ == '__main__':
diff --git a/integration_tests/csuite_test_utils.py b/integration_tests/csuite_test_utils.py
index e72535f..9bbc1d5 100644
--- a/integration_tests/csuite_test_utils.py
+++ b/integration_tests/csuite_test_utils.py
@@ -60,10 +60,6 @@ class CSuiteHarness(contextlib.AbstractContextManager):
'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):
@@ -74,25 +70,6 @@ class CSuiteHarness(contextlib.AbstractContextManager):
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."""
@@ -113,6 +90,10 @@ class CSuiteHarness(contextlib.AbstractContextManager):
# connected device that wasn't explicitly specified.
env.pop('ANDROID_SERIAL', None)
+ # Unset environment variables that might cause the TradeFed to load classes
+ # that weren't included in the standalone suite zip.
+ env.pop('TF_GLOBAL_CONFIG', None)
+
# Set the environment variable that TradeFed requires to find test modules.
env['ANDROID_TARGET_OUT_TESTCASES'] = self._testcases_dir
@@ -205,7 +186,7 @@ class Adb:
return [l.split(':')[1] for l in p.stdout.splitlines()]
-def _run_command(args, check=True, **kwargs) -> subprocess.CompletedProcess:
+def _run_command(args, check=False, **kwargs) -> subprocess.CompletedProcess:
"""A wrapper for subprocess.run that overrides defaults and adds logging."""
env = kwargs.get('env', {})
diff --git a/instrumentation/launch/Android.bp b/test_scripts/Android.bp
index 1b45c1a..62bfcf0 100644
--- a/instrumentation/launch/Android.bp
+++ b/test_scripts/Android.bp
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -16,17 +16,19 @@ 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: [
- "csuite",
+java_library_host {
+ name: "csuite-test-scripts",
+ srcs: [
+ "src/main/java/**/*.java",
+ ],
+ exclude_srcs: [
+ "src/main/java/com/android/pixel/**/*.java",
+ ],
+ java_resource_dirs: [
+ "src/main/resources",
+ ],
+ libs: [
+ "tradefed",
+ "csuite-harness",
],
}
diff --git a/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java b/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java
new file mode 100644
index 0000000..ae7bb09
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/csuite/tests/AppCrawlTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.tests;
+
+import com.android.csuite.core.AppCrawlTester;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import com.google.common.base.Preconditions;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/** A test that verifies that a single app can be successfully launched. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class AppCrawlTest extends BaseHostJUnit4Test {
+ private static final String COLLECT_APP_VERSION = "collect-app-version";
+ private static final String COLLECT_GMS_VERSION = "collect-gms-version";
+ private static final String RECORD_SCREEN = "record-screen";
+ @Rule public TestLogData mLogData = new TestLogData();
+ AppCrawlTester mCrawler;
+
+ @Option(name = RECORD_SCREEN, description = "Whether to record screen during test.")
+ private boolean mRecordScreen;
+
+ @Option(
+ name = COLLECT_APP_VERSION,
+ description =
+ "Whether to collect package version information and store the information in"
+ + " test log files.")
+ private boolean mCollectAppVersion;
+
+ @Option(
+ name = COLLECT_GMS_VERSION,
+ description =
+ "Whether to collect GMS core version information and store the information in"
+ + " test log files.")
+ private boolean mCollectGmsVersion;
+
+ @Option(
+ name = "apk",
+ mandatory = false,
+ description =
+ "Path to an apk file or a directory containing apk files of a single package.")
+ private File mApk;
+
+ @Option(name = "package-name", mandatory = true, description = "Package name of testing app.")
+ private String mPackageName;
+
+ @Option(
+ name = "ui-automator-mode",
+ mandatory = false,
+ description =
+ "Run the crawler with UIAutomator mode. Apk option is not required in this"
+ + " mode.")
+ private boolean mUiAutomatorMode = false;
+
+ @Before
+ public void setUp() {
+ if (!mUiAutomatorMode) {
+ Preconditions.checkNotNull(
+ mApk, "Apk file path is required when not running in UIAutomator mode");
+ }
+
+ mCrawler = AppCrawlTester.newInstance(mPackageName, getTestInformation(), mLogData);
+ mCrawler.setRecordScreen(mRecordScreen);
+ mCrawler.setCollectGmsVersion(mCollectGmsVersion);
+ mCrawler.setCollectAppVersion(mCollectAppVersion);
+ mCrawler.setUiAutomatorMode(mUiAutomatorMode);
+ mCrawler.setApkPath(mApk.toPath());
+ }
+
+ @Test
+ public void testAppCrash() throws DeviceNotAvailableException {
+ mCrawler.startAndAssertAppNoCrash();
+ }
+
+ @After
+ public void tearDown() throws DeviceNotAvailableException {
+ getDevice().uninstallPackage(mPackageName);
+ mCrawler.cleanUp();
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java b/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java
new file mode 100644
index 0000000..49f27fa
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/csuite/tests/AppLaunchTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.tests;
+
+import com.android.csuite.core.ApkInstaller;
+import com.android.csuite.core.ApkInstaller.ApkInstallerException;
+import com.android.csuite.core.DeviceUtils;
+import com.android.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.csuite.core.DeviceUtils.DeviceUtilsException;
+import com.android.csuite.core.TestUtils;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A test that verifies that a single app can be successfully launched. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class AppLaunchTest extends BaseHostJUnit4Test {
+ @VisibleForTesting static final String SCREENSHOT_AFTER_LAUNCH = "screenshot-after-launch";
+ @VisibleForTesting static final String COLLECT_APP_VERSION = "collect-app-version";
+ @VisibleForTesting static final String COLLECT_GMS_VERSION = "collect-gms-version";
+ @VisibleForTesting static final String RECORD_SCREEN = "record-screen";
+ @Rule public TestLogData mLogData = new TestLogData();
+ private ApkInstaller mApkInstaller;
+
+ @Option(name = RECORD_SCREEN, description = "Whether to record screen during test.")
+ private boolean mRecordScreen;
+
+ @Option(
+ name = SCREENSHOT_AFTER_LAUNCH,
+ description = "Whether to take a screenshost after a package is launched.")
+ private boolean mScreenshotAfterLaunch;
+
+ @Option(
+ name = COLLECT_APP_VERSION,
+ description =
+ "Whether to collect package version information and store the information in"
+ + " test log files.")
+ private boolean mCollectAppVersion;
+
+ @Option(
+ name = COLLECT_GMS_VERSION,
+ description =
+ "Whether to collect GMS core version information and store the information in"
+ + " test log files.")
+ private boolean mCollectGmsVersion;
+
+ @Option(
+ name = "install-apk",
+ description =
+ "The path to an apk file or a directory of apk files of a singe package to be"
+ + " installed on device. Can be repeated.")
+ private final List<File> mApkPaths = new ArrayList<>();
+
+ @Option(
+ name = "install-arg",
+ description = "Arguments for the 'adb install-multiple' package installation command.")
+ private final List<String> mInstallArgs = new ArrayList<>();
+
+ @Option(name = "package-name", description = "Package name of testing app.")
+ private String mPackageName;
+
+ @Option(
+ name = "app-launch-timeout-ms",
+ description = "Time to wait for app to launch in msecs.")
+ private int mAppLaunchTimeoutMs = 15000;
+
+ @Before
+ public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException {
+ Assert.assertNotNull("Package name cannot be null", mPackageName);
+
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ mApkInstaller = ApkInstaller.getInstance(getDevice());
+ for (File apkPath : mApkPaths) {
+ CLog.d("Installing " + apkPath);
+ mApkInstaller.install(
+ apkPath.toPath(), mInstallArgs.toArray(new String[mInstallArgs.size()]));
+ }
+
+ if (mCollectGmsVersion) {
+ testUtils.collectGmsVersion(mPackageName);
+ }
+
+ if (mCollectAppVersion) {
+ testUtils.collectAppVersion(mPackageName);
+ }
+
+ deviceUtils.resetPackage(mPackageName);
+ deviceUtils.freezeRotation();
+ }
+
+ @Test
+ public void testAppCrash() throws DeviceNotAvailableException {
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ if (mRecordScreen) {
+ testUtils.collectScreenRecord(
+ () -> {
+ launchPackageAndCheckForCrash();
+ },
+ mPackageName);
+ } else {
+ launchPackageAndCheckForCrash();
+ }
+ }
+
+ @After
+ public void tearDown() throws DeviceNotAvailableException, ApkInstallerException {
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ if (mScreenshotAfterLaunch) {
+ testUtils.collectScreenshot(mPackageName);
+ }
+
+ deviceUtils.stopPackage(mPackageName);
+ deviceUtils.unfreezeRotation();
+
+ mApkInstaller.uninstallAllInstalledPackages();
+ }
+
+ private void launchPackageAndCheckForCrash() throws DeviceNotAvailableException {
+ CLog.d("Launching package: %s.", mPackageName);
+
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ DeviceTimestamp startTime = deviceUtils.currentTimeMillis();
+ try {
+ deviceUtils.launchPackage(mPackageName);
+ } catch (DeviceUtilsException e) {
+ Assert.fail(e.getMessage());
+ }
+
+ CLog.d("Waiting %s milliseconds for the app to launch fully.", mAppLaunchTimeoutMs);
+ RunUtil.getDefault().sleep(mAppLaunchTimeoutMs);
+
+ CLog.d("Completed launching package: %s", mPackageName);
+
+ try {
+ String crashLog = testUtils.getDropboxPackageCrashLog(mPackageName, startTime, true);
+ Assert.assertNull(crashLog, crashLog);
+ } catch (IOException e) {
+ Assert.fail("Error while getting dropbox crash log: " + e);
+ }
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/Android.bp b/test_scripts/src/main/java/com/android/pixel/Android.bp
new file mode 100644
index 0000000..2f3e2e8
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "PixelAppCompTests",
+ compile_multilib: "both",
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ static_libs: [
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "collector-device-lib",
+ "ub-uiautomator",
+ ],
+ // Use multi-dex as the compatibility-common-util-devicesidelib dependency
+ // on compatibility-device-util-axt pushes us beyond 64k methods.
+ dxflags: ["--multi-dex"],
+ srcs: [
+ "tests/*.java",
+ "utils/*.java",
+ ],
+ platform_apis: true,
+ // Tag this module as a cts test artifact
+ test_suites: [
+ "device-tests",
+ "csuite",
+ ],
+ min_sdk_version: "29",
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/AndroidManifest.xml b/test_scripts/src/main/java/com/android/pixel/AndroidManifest.xml
new file mode 100644
index 0000000..2b88677
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.pixel.tests">
+ <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.pixel.tests" />
+
+</manifest>
diff --git a/test_scripts/src/main/java/com/android/pixel/OWNERS b/test_scripts/src/main/java/com/android/pixel/OWNERS
new file mode 100644
index 0000000..05ffe9a
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/OWNERS
@@ -0,0 +1,2 @@
+murphykuo@google.com
+huilingchi@google.com
diff --git a/test_scripts/src/main/java/com/android/pixel/README.md b/test_scripts/src/main/java/com/android/pixel/README.md
new file mode 100644
index 0000000..fc07aaf
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/README.md
@@ -0,0 +1,4 @@
+# Pixel App Compatibility Test Suite
+
+Pixel App Compatibility Test Suite consists of the testing utilities and test
+cases, designed to detect and report app compatibility issues on Pixel phones.
diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java
new file mode 100644
index 0000000..92c2f59
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchLockTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pixel.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AppLaunchLockTest extends PixelAppCompatTestBase {
+ private static final int LAUNCH_TIME_MS = 30000; // 30 seconds
+ private static final long WAIT_ONE_SECOND_IN_MS = 1000;
+ private static final String DISMISS_KEYGUARD = "wm dismiss-keyguard";
+
+ @Test
+ public void testLockDevice() throws Exception {
+ // Launch the 3P app
+ getDeviceUtils().launchApp(getPackage());
+
+ // Wait for the 3P app to appear
+ getUiDevice().wait(Until.hasObject(By.pkg(getPackage()).depth(0)), LAUNCH_TIME_MS);
+ getUiDevice().waitForIdle();
+ Assert.assertTrue(
+ "3P app main page should show up",
+ getUiDevice().hasObject(By.pkg(getPackage()).depth(0)));
+
+ if (getUiDevice().isScreenOn()) {
+ getUiDevice().sleep();
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ }
+ getDeviceUtils().takeScreenshot(getPackage(), "sleep_device");
+ Assert.assertFalse("The screen should be off", getUiDevice().isScreenOn());
+
+ getUiDevice().wakeUp();
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "wake_up_device");
+ Assert.assertTrue("The screen should be off", getUiDevice().isScreenOn());
+ Assert.assertTrue("The keyguard should show up", getKeyguardManager().isKeyguardLocked());
+
+ getUiDevice().executeShellCommand(DISMISS_KEYGUARD);
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "dismiss_keyguard");
+ getUiDevice().wait(Until.hasObject(By.pkg(getPackage()).depth(0)), LAUNCH_TIME_MS);
+ Assert.assertFalse(
+ "The keyguard should be dismissed", getKeyguardManager().isKeyguardLocked());
+ Assert.assertTrue(
+ "3P app main page should show up after unlocking the screen",
+ getUiDevice().hasObject(By.pkg(getPackage()).depth(0)));
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java
new file mode 100644
index 0000000..6c694c8
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRecentAppTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pixel.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AppLaunchRecentAppTest extends PixelAppCompatTestBase {
+ private static final int WAIT_FIFTEEN_SECONDS_IN_MS = 15000;
+ private static final long WAIT_ONE_SECOND_IN_MS = 1000;
+ private static final String CLEAR_ALL = "Clear all";
+ private static final String NO_RECENT_ITEMS = "No recent items";
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ getUiDevice().pressRecentApps();
+ getUiDevice()
+ .waitForWindowUpdate(
+ getUiDevice().getLauncherPackageName(), WAIT_FIFTEEN_SECONDS_IN_MS);
+ getUiDevice().wait(Until.findObject(By.text(NO_RECENT_ITEMS)), WAIT_FIFTEEN_SECONDS_IN_MS);
+ if (!getUiDevice().hasObject(By.text(NO_RECENT_ITEMS))) {
+ int midY = getUiDevice().getDisplayHeight() / 2;
+ int startX = getUiDevice().getDisplayWidth() * 1 / 10;
+ int endX = getUiDevice().getDisplayWidth() * 9 / 10;
+ for (int i = 0; i < 20; i++) {
+ getUiDevice().swipe(startX, midY, endX, midY, (endX - startX) / 100);
+ getUiDevice().waitForIdle();
+ if (getUiDevice().hasObject(By.text(CLEAR_ALL))) {
+ break;
+ }
+ }
+ if (getUiDevice().hasObject(By.text(CLEAR_ALL))) {
+ getUiDevice().findObject(By.text(CLEAR_ALL)).click();
+ }
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ }
+ getDeviceUtils().backToHome(getUiDevice().getLauncherPackageName());
+ }
+
+ @Test
+ public void testLaunchFromRecentApps() throws Exception {
+ // Launch the 3P app
+ getDeviceUtils().launchApp(getPackage());
+
+ // Wait for the 3P app to appear
+ getUiDevice()
+ .wait(Until.hasObject(By.pkg(getPackage()).depth(0)), WAIT_FIFTEEN_SECONDS_IN_MS);
+ getUiDevice().waitForIdle();
+ Assert.assertTrue(
+ "3P app main page should show up",
+ getUiDevice().hasObject(By.pkg(getPackage()).depth(0)));
+
+ getUiDevice().pressRecentApps();
+ getUiDevice().wait(Until.hasObject(By.text("Screenshot")), WAIT_FIFTEEN_SECONDS_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "press_recent_apps_1");
+ Assert.assertTrue(
+ "3P app should be in background", getUiDevice().hasObject(By.text("Screenshot")));
+
+ getUiDevice().pressRecentApps();
+ getUiDevice()
+ .wait(Until.hasObject(By.pkg(getPackage()).depth(0)), WAIT_FIFTEEN_SECONDS_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "press_recent_apps_2");
+ Assert.assertTrue(
+ "3P app main page should be re-launched",
+ getUiDevice().hasObject(By.pkg(getPackage()).depth(0)));
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java
new file mode 100644
index 0000000..62f6e1a
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/tests/AppLaunchRotateTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pixel.tests;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class AppLaunchRotateTest extends PixelAppCompatTestBase {
+ private static final String ROTATE_LANDSCAPE =
+ "content insert --uri content://settings/system"
+ + " --bind name:s:user_rotation --bind value:i:1";
+ private static final String ROTATE_PORTRAIT =
+ "content insert --uri content://settings/system"
+ + " --bind name:s:user_rotation --bind value:i:0";
+ private static final int LAUNCH_TIME_MS = 30000; // 30 seconds
+ private static final long WAIT_ONE_SECOND_IN_MS = 1000;
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ getUiDevice().unfreezeRotation();
+ }
+
+ @Test
+ public void testRotateDevice() throws Exception {
+ // Launch the 3P app
+ getDeviceUtils().launchApp(getPackage());
+
+ // Wait for the 3P app to appear
+ getUiDevice().wait(Until.hasObject(By.pkg(getPackage()).depth(0)), LAUNCH_TIME_MS);
+ getUiDevice().waitForIdle();
+ Assert.assertTrue(
+ "3P app main page should show up",
+ getUiDevice().hasObject(By.pkg(getPackage()).depth(0)));
+
+ // Turn off the automatic rotation
+ getUiDevice().freezeRotation();
+ getUiDevice().executeShellCommand(ROTATE_PORTRAIT);
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "set_portrait_mode");
+ Assert.assertTrue(
+ "Screen should be in portrait mode", getUiDevice().isNaturalOrientation());
+
+ getUiDevice().executeShellCommand(ROTATE_LANDSCAPE);
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "rotate_landscape");
+ Assert.assertFalse(
+ "Screen should be in landscape mode", getUiDevice().isNaturalOrientation());
+
+ getUiDevice().executeShellCommand(ROTATE_PORTRAIT);
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ getDeviceUtils().takeScreenshot(getPackage(), "rotate_portrait");
+ Assert.assertTrue(
+ "Screen should be in portrait mode", getUiDevice().isNaturalOrientation());
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java b/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java
new file mode 100644
index 0000000..d5fb892
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/tests/PixelAppCompatTestBase.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pixel.tests;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getArguments;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.KeyguardManager;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.pixel.utils.DeviceUtils;
+
+import org.junit.After;
+import org.junit.Before;
+
+/** Base class for Pixel app compatibility tests. */
+public abstract class PixelAppCompatTestBase {
+ private static final String KEY_PACKAGE_NAME = "package";
+
+ private DeviceUtils mDeviceUtils;
+ private UiDevice mDevice;
+ private KeyguardManager mKeyguardManager;
+ private String mPackage;
+
+ @Before
+ public void setUp() throws Exception {
+ getDeviceUtils().createLogDataDir();
+ getDeviceUtils().wakeAndUnlockScreen();
+ // Start from the home screen
+ getDeviceUtils().backToHome(getUiDevice().getLauncherPackageName());
+ getDeviceUtils().startRecording(getPackage());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ getDeviceUtils().stopRecording();
+ }
+
+ protected UiDevice getUiDevice() {
+ if (mDevice == null) {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ }
+ return mDevice;
+ }
+
+ protected DeviceUtils getDeviceUtils() {
+ if (mDeviceUtils == null) {
+ mDeviceUtils = new DeviceUtils(getUiDevice());
+ }
+ return mDeviceUtils;
+ }
+
+ protected KeyguardManager getKeyguardManager() {
+ if (mKeyguardManager == null) {
+ mKeyguardManager =
+ getInstrumentation().getContext().getSystemService(KeyguardManager.class);
+ }
+ return mKeyguardManager;
+ }
+
+ protected String getPackage() {
+ if (mPackage == null) {
+ mPackage = getArguments().getString(KEY_PACKAGE_NAME);
+ }
+ return mPackage;
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java b/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java
new file mode 100644
index 0000000..9d29dbc
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/pixel/utils/DeviceUtils.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.pixel.utils;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import org.junit.Assert;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+
+public class DeviceUtils {
+ private static final String TAG = DeviceUtils.class.getSimpleName();
+ private static final String LOG_DATA_DIR = "/sdcard/logData";
+ private static final int MAX_RECORDING_PARTS = 5;
+ private static final long WAIT_ONE_SECOND_IN_MS = 1000;
+ private static final long VIDEO_TAIL_BUFFER = 500;
+ private static final String DISMISS_KEYGUARD = "wm dismiss-keyguard";
+
+ private RecordingThread mCurrentThread;
+ private File mLogDataDir;
+ private UiDevice mDevice;
+
+ public DeviceUtils(UiDevice device) {
+ mDevice = device;
+ }
+
+ /** Create a directory to save test screenshots, screenrecord and text files. */
+ public void createLogDataDir() {
+ mLogDataDir = new File(LOG_DATA_DIR);
+ if (mLogDataDir.exists()) {
+ String[] children = mLogDataDir.list();
+ for (String file : children) {
+ new File(mLogDataDir, file).delete();
+ }
+ } else {
+ mLogDataDir.mkdirs();
+ }
+ }
+
+ /** Wake up the device and dismiss the keyguard. */
+ public void wakeAndUnlockScreen() throws Exception {
+ mDevice.wakeUp();
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ mDevice.executeShellCommand(DISMISS_KEYGUARD);
+ SystemClock.sleep(WAIT_ONE_SECOND_IN_MS);
+ }
+
+ /**
+ * Go back to home screen by pressing back key five times and home key to avoid the infinite
+ * loop since some apps' activities cannot be exited to home screen by back key event.
+ */
+ public void backToHome(String launcherPkg) {
+ for (int i = 0; i < 5; i++) {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ if (mDevice.hasObject(By.pkg(launcherPkg))) {
+ break;
+ }
+ }
+ mDevice.pressHome();
+ }
+
+ /**
+ * Launch an app with the given package name
+ *
+ * @param packageName Name of package to be launched
+ */
+ public void launchApp(String packageName) {
+ Context context = getInstrumentation().getContext();
+ Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+
+ /**
+ * Take a screenshot on the device and save it in {@code logDataDir}.
+ *
+ * @param packageName The package name of 3P apps screenshotted.
+ * @param description The description of actions or operations on the device.
+ */
+ public void takeScreenshot(String packageName, String description) {
+ File screenshot =
+ new File(
+ LOG_DATA_DIR,
+ String.format("%s_screenshot_%s.png", packageName, description));
+ mDevice.takeScreenshot(screenshot);
+ }
+
+ /**
+ * Start the screen recording.
+ *
+ * @param packageName The package name of 3P apps screenrecorded.
+ */
+ public void startRecording(String packageName) {
+ Log.v(TAG, "Started Recording");
+ mCurrentThread =
+ new RecordingThread(
+ "test-screen-record", String.format("%s_screenrecord", packageName));
+ mCurrentThread.start();
+ }
+
+ /** Stop already started screen recording. */
+ public void stopRecording() {
+ // Skip if not directory.
+ if (mLogDataDir == null) {
+ return;
+ }
+ // Add some extra time to the video end.
+ SystemClock.sleep(VIDEO_TAIL_BUFFER);
+ // Ctrl + C all screen record processes.
+ mCurrentThread.cancel();
+ // Wait for the thread to completely die.
+ try {
+ mCurrentThread.join();
+ } catch (InterruptedException ex) {
+ Log.e(TAG, "Interrupted when joining the recording thread.", ex);
+ }
+ Log.v(TAG, "Stopped Recording");
+ }
+
+ /** Returns the recording's name for {@code part} of launch description. */
+ public File getOutputFile(String description, int part) {
+ // Omit the iteration number for the first iteration.
+ final String fileName = String.format("%s-video%s.mp4", description, part == 1 ? "" : part);
+ return Paths.get(mLogDataDir.getAbsolutePath(), fileName).toFile();
+ }
+
+ /**
+ * Encapsulates the start and stop screen recording logic. Copied from ScreenRecordCollector.
+ */
+ private class RecordingThread extends Thread {
+ private final String mDescription;
+
+ private boolean mContinue;
+
+ RecordingThread(String name, String description) {
+ super(name);
+
+ mContinue = true;
+
+ Assert.assertNotNull("No test description provided for recording.", description);
+ mDescription = description;
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Start at i = 1 to encode parts as X.mp4, X2.mp4, X3.mp4, etc.
+ for (int i = 1; i <= MAX_RECORDING_PARTS && mContinue; i++) {
+ File output = getOutputFile(mDescription, i);
+ Log.d(TAG, String.format("Recording screen to %s", output.getAbsolutePath()));
+ // Make sure not to block on this background command in the main thread so
+ // that the test continues to run, but block in this thread so it does not
+ // trigger a new screen recording session before the prior one completes.
+ mDevice.executeShellCommand(
+ String.format("screenrecord %s", output.getAbsolutePath()));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Caught exception while screen recording.");
+ }
+ }
+
+ public void cancel() {
+ mContinue = false;
+ // Identify the screenrecord PIDs and send SIGINT 2 (Ctrl + C) to each.
+ try {
+ String[] pids = mDevice.executeShellCommand("pidof screenrecord").split(" ");
+ for (String pid : pids) {
+ // Avoid empty process ids, because of weird splitting behavior.
+ if (pid.isEmpty()) {
+ continue;
+ }
+ mDevice.executeShellCommand(String.format("kill -2 %s", pid));
+ Log.d(TAG, String.format("Sent SIGINT 2 to screenrecord process (%s)", pid));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to kill screen recording process.");
+ }
+ }
+ }
+}
diff --git a/test_scripts/src/main/java/com/android/webview/OWNERS b/test_scripts/src/main/java/com/android/webview/OWNERS
new file mode 100644
index 0000000..af3a7c8
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/webview/OWNERS
@@ -0,0 +1,2 @@
+amitku@google.com
+rmhasan@google.com \ No newline at end of file
diff --git a/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java
new file mode 100644
index 0000000..d08d2e3
--- /dev/null
+++ b/test_scripts/src/main/java/com/android/webview/tests/WebviewAppLaunchTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.webview.tests;
+
+import com.android.csuite.core.ApkInstaller;
+import com.android.csuite.core.ApkInstaller.ApkInstallerException;
+import com.android.csuite.core.DeviceUtils;
+import com.android.csuite.core.DeviceUtils.DeviceTimestamp;
+import com.android.csuite.core.DeviceUtils.DeviceUtilsException;
+import com.android.csuite.core.TestUtils;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.AaptParser;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** A test that verifies that a single app can be successfully launched. */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class WebviewAppLaunchTest extends BaseHostJUnit4Test {
+ @Rule public TestLogData mLogData = new TestLogData();
+ private ApkInstaller mApkInstaller;
+ private List<File> mOrderedWebviewApks = new ArrayList<>();
+
+ @Option(name = "record-screen", description = "Whether to record screen during test.")
+ private boolean mRecordScreen;
+
+ @Option(name = "package-name", description = "Package name of testing app.")
+ private String mPackageName;
+
+ @Option(
+ name = "install-apk",
+ description =
+ "The path to an apk file or a directory of apk files of a singe package to be"
+ + " installed on device. Can be repeated.")
+ private List<File> mApkPaths = new ArrayList<>();
+
+ @Option(
+ name = "install-arg",
+ description = "Arguments for the 'adb install-multiple' package installation command.")
+ private final List<String> mInstallArgs = new ArrayList<>();
+
+ @Option(
+ name = "app-launch-timeout-ms",
+ description = "Time to wait for an app to launch in msecs.")
+ private int mAppLaunchTimeoutMs = 20000;
+
+ @Option(
+ name = "webview-apk-dir",
+ description = "The path to the webview apk.",
+ importance = Importance.ALWAYS)
+ private File mWebviewApkDir;
+
+ @Before
+ public void setUp() throws DeviceNotAvailableException, ApkInstallerException, IOException {
+ Assert.assertNotNull("Package name cannot be null", mPackageName);
+
+ readWebviewApkDirectory();
+
+ mApkInstaller = ApkInstaller.getInstance(getDevice());
+ for (File apkPath : mApkPaths) {
+ CLog.d("Installing " + apkPath);
+ mApkInstaller.install(
+ apkPath.toPath(), mInstallArgs.toArray(new String[mInstallArgs.size()]));
+ }
+
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ deviceUtils.freezeRotation();
+
+ printWebviewVersion();
+ }
+
+ @Test
+ public void testAppLaunch()
+ throws DeviceNotAvailableException, ApkInstallerException, IOException {
+ AssertionError lastError = null;
+ // Try the latest webview version
+ WebviewPackage lastWebviewInstalled = installWebview(mOrderedWebviewApks.get(0));
+ try {
+ assertAppLaunchNoCrash();
+ } catch (AssertionError e) {
+ lastError = e;
+ } finally {
+ uninstallWebview();
+ }
+
+ // If the app doesn't crash, complete the test.
+ if (lastError == null) {
+ return;
+ }
+
+ // If the app crashes, try the app with the original webview version that comes with the
+ // device.
+ try {
+ assertAppLaunchNoCrash();
+ } catch (AssertionError newError) {
+ CLog.w(
+ "The app %s crashed both with and without the webview installation,"
+ + " ignoring the failure...",
+ mPackageName);
+ return;
+ }
+
+ for (int idx = 1; idx < mOrderedWebviewApks.size(); idx++) {
+ lastWebviewInstalled = installWebview(mOrderedWebviewApks.get(idx));
+ try {
+ assertAppLaunchNoCrash();
+ } catch (AssertionError e) {
+ lastError = e;
+ continue;
+ } finally {
+ uninstallWebview();
+ }
+ break;
+ }
+
+ throw new AssertionError(
+ String.format(
+ "Package %s crashed since webview version %s",
+ mPackageName, lastWebviewInstalled.getVersion()),
+ lastError);
+ }
+
+ @After
+ public void tearDown() throws DeviceNotAvailableException, ApkInstallerException {
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+ testUtils.collectScreenshot(mPackageName);
+
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ deviceUtils.stopPackage(mPackageName);
+ deviceUtils.unfreezeRotation();
+
+ mApkInstaller.uninstallAllInstalledPackages();
+ printWebviewVersion();
+ }
+
+ private void readWebviewApkDirectory() {
+ mOrderedWebviewApks = Arrays.asList(mWebviewApkDir.listFiles());
+ Collections.sort(
+ mOrderedWebviewApks,
+ new Comparator<File>() {
+ @Override
+ public int compare(File apk1, File apk2) {
+ return getVersionCode(apk2).compareTo(getVersionCode(apk1));
+ }
+
+ private Long getVersionCode(File apk) {
+ return Long.parseLong(AaptParser.parse(apk).getVersionCode());
+ }
+ });
+ }
+
+ private void printWebviewVersion(WebviewPackage currentWebview)
+ throws DeviceNotAvailableException {
+ CLog.i("Current webview implementation: %s", currentWebview.getPackageName());
+ CLog.i("Current webview version: %s", currentWebview.getVersion());
+ }
+
+ private void printWebviewVersion() throws DeviceNotAvailableException {
+ WebviewPackage currentWebview = getCurrentWebviewPackage();
+ printWebviewVersion(currentWebview);
+ }
+
+ private WebviewPackage installWebview(File apk)
+ throws ApkInstallerException, IOException, DeviceNotAvailableException {
+ ApkInstaller.getInstance(getDevice()).install(apk.toPath());
+ CommandResult res =
+ getDevice()
+ .executeShellV2Command(
+ "cmd webviewupdate set-webview-implementation com.android.webview");
+ Assert.assertEquals(
+ "Failed to set webview update: " + res, res.getStatus(), CommandStatus.SUCCESS);
+ WebviewPackage currentWebview = getCurrentWebviewPackage();
+ printWebviewVersion(currentWebview);
+ return currentWebview;
+ }
+
+ private void uninstallWebview() throws DeviceNotAvailableException {
+ getDevice()
+ .executeShellCommand(
+ "cmd webviewupdate set-webview-implementation com.google.android.webview");
+ getDevice().executeAdbCommand("uninstall", "com.android.webview");
+ }
+
+ private WebviewPackage getCurrentWebviewPackage() throws DeviceNotAvailableException {
+ String dumpsys = getDevice().executeShellCommand("dumpsys webviewupdate");
+ return WebviewPackage.parseFrom(dumpsys);
+ }
+
+ private static class WebviewPackage {
+ private final String mPackageName;
+ private final String mVersion;
+
+ private WebviewPackage(String packageName, String version) {
+ mPackageName = packageName;
+ mVersion = version;
+ }
+
+ static WebviewPackage parseFrom(String dumpsys) {
+ Pattern pattern =
+ Pattern.compile("Current WebView package \\(name, version\\): \\((.*?)\\)");
+ Matcher matcher = pattern.matcher(dumpsys);
+ Assert.assertTrue("Cannot parse webview package info from: " + dumpsys, matcher.find());
+ String[] packageInfo = matcher.group(1).split(",");
+ return new WebviewPackage(packageInfo[0].strip(), packageInfo[1].strip());
+ }
+
+ String getPackageName() {
+ return mPackageName;
+ }
+
+ String getVersion() {
+ return mVersion;
+ }
+ }
+
+ private void assertAppLaunchNoCrash() throws DeviceNotAvailableException {
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ deviceUtils.resetPackage(mPackageName);
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ if (mRecordScreen) {
+ testUtils.collectScreenRecord(
+ () -> {
+ launchPackageAndCheckForCrash();
+ },
+ mPackageName);
+ } else {
+ launchPackageAndCheckForCrash();
+ }
+ }
+
+ private void launchPackageAndCheckForCrash() throws DeviceNotAvailableException {
+ CLog.d("Launching package: %s.", mPackageName);
+
+ DeviceUtils deviceUtils = DeviceUtils.getInstance(getDevice());
+ TestUtils testUtils = TestUtils.getInstance(getTestInformation(), mLogData);
+
+ DeviceTimestamp startTime = deviceUtils.currentTimeMillis();
+ try {
+ deviceUtils.launchPackage(mPackageName);
+ } catch (DeviceUtilsException e) {
+ Assert.fail(e.getMessage());
+ }
+
+ CLog.d("Waiting %s milliseconds for the app to launch fully.", mAppLaunchTimeoutMs);
+ RunUtil.getDefault().sleep(mAppLaunchTimeoutMs);
+
+ CLog.d("Completed launching package: %s", mPackageName);
+
+ try {
+ String crashLog = testUtils.getDropboxPackageCrashLog(mPackageName, startTime, true);
+ if (crashLog != null) {
+ Assert.fail(crashLog);
+ }
+ } catch (IOException e) {
+ Assert.fail("Error while getting dropbox crash log: " + e);
+ }
+ }
+}
diff --git a/test_targets/csuite-app-crawl/Android.bp b/test_targets/csuite-app-crawl/Android.bp
new file mode 100644
index 0000000..6da9902
--- /dev/null
+++ b/test_targets/csuite-app-crawl/Android.bp
@@ -0,0 +1,23 @@
+// 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-crawl",
+ test_plan_include: "plan.xml",
+ test_config_template: "template.xml"
+}
diff --git a/test_targets/csuite-app-crawl/plan.xml b/test_targets/csuite-app-crawl/plan.xml
new file mode 100644
index 0000000..3ae9e50
--- /dev/null
+++ b/test_targets/csuite-app-crawl/plan.xml
@@ -0,0 +1,3 @@
+<configuration description="C-Suite Crawler Test Plan">
+ <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-app-crawl/template.xml b/test_targets/csuite-app-crawl/template.xml
new file mode 100644
index 0000000..406cf68
--- /dev/null
+++ b/test_targets/csuite-app-crawl/template.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="C-Suite Crawler test configuration">
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP"/>
+ <option name="run-command" value="input keyevent KEYCODE_MENU"/>
+ <option name="run-command" value="input keyevent KEYCODE_HOME"/>
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="set-option" value="apk:app\://{package}"/>
+ <option name="class" value="com.android.csuite.tests.AppCrawlTest" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-app-launch/Android.bp b/test_targets/csuite-app-launch/Android.bp
index 256d9f4..5a25f8a 100644
--- a/test_targets/csuite-app-launch/Android.bp
+++ b/test_targets/csuite-app-launch/Android.bp
@@ -18,5 +18,6 @@ package {
csuite_test {
name: "csuite-app-launch",
- test_config_template: "template.xml"
+ test_config_template: "default.xml",
+ extra_test_config_templates: ["pre-installed-apps.xml"]
}
diff --git a/test_targets/csuite-app-launch/template.xml b/test_targets/csuite-app-launch/default.xml
index 52c4611..e583002 100644
--- a/test_targets/csuite-app-launch/template.xml
+++ b/test_targets/csuite-app-launch/default.xml
@@ -14,15 +14,15 @@
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"/>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="set-option" value="install-apk:app\://{package}"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-app-launch/pre-installed-apps.xml b/test_targets/csuite-app-launch/pre-installed-apps.xml
new file mode 100644
index 0000000..5eaba02
--- /dev/null
+++ b/test_targets/csuite-app-launch/pre-installed-apps.xml
@@ -0,0 +1,27 @@
+<?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">
+ <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.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-pre-installed-app-launch/template.xml b/test_targets/csuite-pre-installed-app-launch/template.xml
index 2d20306..5eaba02 100644
--- a/test_targets/csuite-pre-installed-app-launch/template.xml
+++ b/test_targets/csuite-pre-installed-app-launch/template.xml
@@ -14,12 +14,14 @@
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"/>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-system-app-launch/template.xml b/test_targets/csuite-system-app-launch/template.xml
index 4d1181b..7782afe 100644
--- a/test_targets/csuite-system-app-launch/template.xml
+++ b/test_targets/csuite-system-app-launch/template.xml
@@ -24,5 +24,8 @@
<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"/>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
</configuration> \ No newline at end of file
diff --git a/test_targets/csuite-test-package-launch/template.xml b/test_targets/csuite-test-package-launch/template.xml
index 9c97fd3..34ba321 100644
--- a/test_targets/csuite-test-package-launch/template.xml
+++ b/test_targets/csuite-test-package-launch/template.xml
@@ -14,16 +14,16 @@
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"/>
+ <test class="com.android.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="set-option" value="install-apk:app\://{package}"/>
+ <option name="set-option" value="install-arg:-t"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
</configuration> \ No newline at end of file
diff --git a/test_targets/drm-app-launch/Android.bp b/test_targets/drm-app-launch/Android.bp
new file mode 100644
index 0000000..08d241b
--- /dev/null
+++ b/test_targets/drm-app-launch/Android.bp
@@ -0,0 +1,23 @@
+// 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: "drm-app-launch",
+ test_plan_include: "plan.xml",
+ test_config_template: "template.xml"
+} \ No newline at end of file
diff --git a/test_targets/drm-app-launch/plan.xml b/test_targets/drm-app-launch/plan.xml
new file mode 100644
index 0000000..6a33cdc
--- /dev/null
+++ b/test_targets/drm-app-launch/plan.xml
@@ -0,0 +1,3 @@
+<configuration description="DRM Test Plan">
+ <object type="MODULE_INFO_PROVIDER" class="com.android.csuite.core.DirectoryBasedModuleInfoProvider" />
+</configuration> \ No newline at end of file
diff --git a/test_targets/drm-app-launch/template.xml b/test_targets/drm-app-launch/template.xml
new file mode 100644
index 0000000..f7abcdd
--- /dev/null
+++ b/test_targets/drm-app-launch/template.xml
@@ -0,0 +1,29 @@
+<?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="DRM launch test configuration">
+ <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.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="set-option" value="install-apk:{package_install_file}"/>
+ <option name="set-option" value="install-arg:-t"/>
+ <option name="class" value="com.android.csuite.tests.AppLaunchTest" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/test_targets/pixel-app-launch-lock/Android.bp b/test_targets/pixel-app-launch-lock/Android.bp
new file mode 100644
index 0000000..8261309
--- /dev/null
+++ b/test_targets/pixel-app-launch-lock/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "pixel-app-launch-lock",
+ test_config_template: "template.xml",
+}
diff --git a/test_targets/pixel-app-launch-lock/OWNERS b/test_targets/pixel-app-launch-lock/OWNERS
new file mode 100644
index 0000000..05ffe9a
--- /dev/null
+++ b/test_targets/pixel-app-launch-lock/OWNERS
@@ -0,0 +1,2 @@
+murphykuo@google.com
+huilingchi@google.com
diff --git a/test_targets/pixel-app-launch-lock/template.xml b/test_targets/pixel-app-launch-lock/template.xml
new file mode 100644
index 0000000..7751426
--- /dev/null
+++ b/test_targets/pixel-app-launch-lock/template.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Launches an app and lock/unlock the device">
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="app://{package}"/>
+ <option name="test-file-name" value="PixelAppCompTests.apk" />
+ </target_preparer>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <!-- repeatable: The key of the DIRECTORY to pull -->
+ <option name = "directory-keys" value = "/sdcard/logData" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="package" value="com.android.pixel.tests" />
+ <option name="include-filter" value="com.android.pixel.tests.AppLaunchLockTest" />
+ <option name="instrumentation-arg" key="package" value="{package}" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
diff --git a/test_targets/pixel-app-launch-recentapp/Android.bp b/test_targets/pixel-app-launch-recentapp/Android.bp
new file mode 100644
index 0000000..4458cf3
--- /dev/null
+++ b/test_targets/pixel-app-launch-recentapp/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "pixel-app-launch-recentapp",
+ test_config_template: "template.xml",
+}
diff --git a/test_targets/pixel-app-launch-recentapp/OWNERS b/test_targets/pixel-app-launch-recentapp/OWNERS
new file mode 100644
index 0000000..05ffe9a
--- /dev/null
+++ b/test_targets/pixel-app-launch-recentapp/OWNERS
@@ -0,0 +1,2 @@
+murphykuo@google.com
+huilingchi@google.com
diff --git a/test_targets/pixel-app-launch-recentapp/template.xml b/test_targets/pixel-app-launch-recentapp/template.xml
new file mode 100644
index 0000000..86742b7
--- /dev/null
+++ b/test_targets/pixel-app-launch-recentapp/template.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Launches an app by tapping recent app key">
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="app://{package}"/>
+ <option name="test-file-name" value="PixelAppCompTests.apk" />
+ </target_preparer>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <!-- repeatable: The key of the DIRECTORY to pull -->
+ <option name = "directory-keys" value = "/sdcard/logData" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="package" value="com.android.pixel.tests" />
+ <option name="include-filter" value="com.android.pixel.tests.AppLaunchRecentAppTest" />
+ <option name="instrumentation-arg" key="package" value="{package}" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
diff --git a/test_targets/pixel-app-launch-rotate/Android.bp b/test_targets/pixel-app-launch-rotate/Android.bp
new file mode 100644
index 0000000..89419ae
--- /dev/null
+++ b/test_targets/pixel-app-launch-rotate/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "pixel-app-launch-rotate",
+ test_config_template: "template.xml",
+}
diff --git a/test_targets/pixel-app-launch-rotate/OWNERS b/test_targets/pixel-app-launch-rotate/OWNERS
new file mode 100644
index 0000000..05ffe9a
--- /dev/null
+++ b/test_targets/pixel-app-launch-rotate/OWNERS
@@ -0,0 +1,2 @@
+murphykuo@google.com
+huilingchi@google.com
diff --git a/test_targets/pixel-app-launch-rotate/template.xml b/test_targets/pixel-app-launch-rotate/template.xml
new file mode 100644
index 0000000..ff7871a
--- /dev/null
+++ b/test_targets/pixel-app-launch-rotate/template.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Launches an app, rotate screen, and check for crashes">
+ <target_preparer class="com.android.compatibility.targetprep.CheckGmsPreparer"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="app://{package}"/>
+ <option name="test-file-name" value="PixelAppCompTests.apk" />
+ </target_preparer>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <!-- repeatable: The key of the DIRECTORY to pull -->
+ <option name = "directory-keys" value = "/sdcard/logData" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="package" value="com.android.pixel.tests" />
+ <option name="include-filter" value="com.android.pixel.tests.AppLaunchRotateTest" />
+ <option name="instrumentation-arg" key="package" value="{package}" />
+ <option name="isolated-storage" value="false" />
+ </test>
+</configuration>
diff --git a/test_targets/webview-app-launch/Android.bp b/test_targets/webview-app-launch/Android.bp
new file mode 100644
index 0000000..3658018
--- /dev/null
+++ b/test_targets/webview-app-launch/Android.bp
@@ -0,0 +1,22 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+csuite_test {
+ name: "webview-app-launch",
+ test_config_template: "default.xml",
+}
diff --git a/test_targets/webview-app-launch/OWNERS b/test_targets/webview-app-launch/OWNERS
new file mode 100644
index 0000000..af3a7c8
--- /dev/null
+++ b/test_targets/webview-app-launch/OWNERS
@@ -0,0 +1,2 @@
+amitku@google.com
+rmhasan@google.com \ No newline at end of file
diff --git a/test_targets/webview-app-launch/default.xml b/test_targets/webview-app-launch/default.xml
new file mode 100644
index 0000000..0b5470b
--- /dev/null
+++ b/test_targets/webview-app-launch/default.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ 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">
+ <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.tradefed.testtype.HostTest" >
+ <option name="set-option" value="package-name:{package}"/>
+ <option name="set-option" value="install-apk:app\://{package}"/>
+ <option name="class" value="com.android.webview.tests.WebviewAppLaunchTest" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/tools/csuite-tradefed/Android.bp b/tools/csuite-tradefed/Android.bp
index a441726..f65410a 100644
--- a/tools/csuite-tradefed/Android.bp
+++ b/tools/csuite-tradefed/Android.bp
@@ -25,6 +25,7 @@ tradefed_binary_host {
static_libs: [
"cts-tradefed-harness",
"csuite-harness",
+ "csuite-test-scripts",
],
}
@@ -35,7 +36,7 @@ java_test_host {
],
static_libs: [
"tradefed",
- "csuite-tradefed",
+ "csuite-harness",
],
test_options: {
unit_test: true,
diff --git a/tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java b/tools/csuite-tradefed/src/test/java/com/android/csuite/tradefed/CSuiteTradefedTest.java
index 709798b..35d6fc3 100644
--- a/tools/csuite-tradefed/src/test/java/com/android/compatibility/tradefed/CSuiteTradefedTest.java
+++ b/tools/csuite-tradefed/src/test/java/com/android/csuite/tradefed/CSuiteTradefedTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.compatibility.tradefed;
+package com.android.csuite.tradefed;
import static org.junit.Assert.assertEquals;
diff --git a/tools/csuite_test/csuite_test.go b/tools/csuite_test/csuite_test.go
index 74373f2..3a14b96 100644
--- a/tools/csuite_test/csuite_test.go
+++ b/tools/csuite_test/csuite_test.go
@@ -29,10 +29,13 @@ func init() {
}
type csuiteTestProperties struct {
- // Local path to a module template xml file.
+ // Local path to a default 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 paths to extra module template xml files.
+ Extra_test_config_templates []string `android:"path"`
+
// Local path to a test plan config xml to be included in the generated plan.
Test_plan_include *string `android:"path"`
}
@@ -45,34 +48,53 @@ type CSuiteTest 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.`)
+func (cSuiteTest *CSuiteTest) buildCopyConfigTemplateCommand(ctx android.ModuleContext, rule *android.RuleBuilder, templatePath string) string {
+ if !strings.HasSuffix(templatePath, xmlFileExtension) {
+ ctx.ModuleErrorf(`Config template path should ends with ` + xmlFileExtension)
}
- inputPath := android.PathForModuleSrc(ctx, *cSuiteTest.csuiteTestProperties.Test_config_template)
- genPath := android.PathForModuleGen(ctx, planConfigDirName, ctx.ModuleName()+configTemplateFileExtension)
+
+ inputPath := android.PathForModuleSrc(ctx, templatePath)
+ genPath := android.PathForModuleGen(ctx, configDirName, ctx.ModuleName(), inputPath.Rel()+configTemplateFileExtension)
rule.Command().Textf("cp").Input(inputPath).Output(genPath)
cSuiteTest.AddExtraResource(genPath)
return genPath.Rel()
}
+func (cSuiteTest *CSuiteTest) buildCopyExtraConfigTemplatesCommand(ctx android.ModuleContext, rule *android.RuleBuilder) []string {
+ output := make([]string, len(cSuiteTest.csuiteTestProperties.Extra_test_config_templates))
+
+ for idx, templatePath := range cSuiteTest.csuiteTestProperties.Extra_test_config_templates {
+ output[idx] = cSuiteTest.buildCopyConfigTemplateCommand(ctx, rule, templatePath)
+ }
+
+ return output
+}
+
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")
+ genPath := android.PathForModuleGen(ctx, configDirName, "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) {
+func (cSuiteTest *CSuiteTest) buildWritePlanConfigRule(ctx android.ModuleContext, configTemplatePath string, extraConfigTemplatePaths []string, planIncludePath string) {
planName := ctx.ModuleName()
content := strings.Replace(planTemplate, "{planName}", planName, -1)
content = strings.Replace(content, "{templatePath}", configTemplatePath, -1)
+ content = strings.Replace(content, "{templateRoot}", android.PathForModuleGen(ctx, configDirName, ctx.ModuleName()).Rel(), -1)
content = strings.Replace(content, "{planInclude}", planIncludePath, -1)
- genPath := android.PathForModuleGen(ctx, planConfigDirName, planName+planFileExtension)
+
+ extraTemplateConfigLines := ""
+ for _, extraPath := range extraConfigTemplatePaths {
+ extraTemplateConfigLines += strings.Replace(extraTemplatePathsTemplate, "{templatePath}", extraPath, -1)
+ }
+ content = strings.Replace(content, "{extraTemplatePaths}", extraTemplateConfigLines, -1)
+
+ genPath := android.PathForModuleGen(ctx, configDirName, planName+xmlFileExtension)
android.WriteFileRule(ctx, genPath, content)
cSuiteTest.AddExtraResource(genPath)
}
@@ -80,9 +102,14 @@ func (cSuiteTest *CSuiteTest) buildWritePlanConfigRule(ctx android.ModuleContext
func (cSuiteTest *CSuiteTest) GenerateAndroidBuildActions(ctx android.ModuleContext) {
rule := android.NewRuleBuilder(pctx, ctx)
- configTemplatePath := cSuiteTest.buildCopyConfigTemplateCommand(ctx, rule)
+ if cSuiteTest.csuiteTestProperties.Test_config_template == nil {
+ ctx.ModuleErrorf(`'test_config_template' is missing.`)
+ }
+
+ configTemplatePath := cSuiteTest.buildCopyConfigTemplateCommand(ctx, rule, *cSuiteTest.csuiteTestProperties.Test_config_template)
+ extraConfigTemplatePaths := cSuiteTest.buildCopyExtraConfigTemplatesCommand(ctx, rule)
planIncludePath := cSuiteTest.buildCopyPlanIncludeCommand(ctx, rule)
- cSuiteTest.buildWritePlanConfigRule(ctx, configTemplatePath, planIncludePath)
+ cSuiteTest.buildWritePlanConfigRule(ctx, configTemplatePath, extraConfigTemplatePaths, planIncludePath)
rule.Build("CSuite", "generate C-Suite config files")
cSuiteTest.TestHost.GenerateAndroidBuildActions(ctx)
@@ -102,10 +129,12 @@ func CSuiteTestFactory() android.Module {
const (
emptyPlanIncludePath = `empty`
- planConfigDirName = `config`
- configTemplateFileExtension = `.xml.template`
- planFileExtension = `.xml`
- planTemplate = `<?xml version="1.0" encoding="utf-8"?>
+ configDirName = `config`
+ configTemplateFileExtension = `.template`
+ xmlFileExtension = `.xml`
+ extraTemplatePathsTemplate = `
+ <option name="extra-templates" value="{templatePath}"/>`
+ 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");
@@ -121,9 +150,15 @@ const (
limitations under the License.
-->
<configuration>
- <test class="com.android.csuite.config.ModuleGenerator">
- <option name="template" value="{templatePath}" />
- </test>
+ <!-- Generates module files in the beginning of the test. -->
+ <test class="com.android.csuite.core.ModuleGenerator" />
+ <!-- Cleans the generated module files after the test. -->
+ <target_preparer class="com.android.csuite.core.ModuleGenerator" />
+ <object type="MODULE_TEMPLATE_PROVIDER" class="com.android.csuite.core.ModuleTemplate" >
+ <option name="template-root" value="{templateRoot}" />
+ <option name="default-template" value="{templatePath}" />{extraTemplatePaths}
+ </object>
+
<include name="csuite-base" />
<include name="{planInclude}" />
<option name="plan" value="{planName}" />
diff --git a/tools/csuite_test/csuite_test_test.go b/tools/csuite_test/csuite_test_test.go
index daf07b0..9da6ffd 100644
--- a/tools/csuite_test/csuite_test_test.go
+++ b/tools/csuite_test/csuite_test_test.go
@@ -26,12 +26,12 @@ 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"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ data_native_bins: "bin"
+ }
+ `)
_, errs := ctx.ParseBlueprintsFiles("Android.bp")
@@ -40,12 +40,12 @@ func TestBpContainsTestHostPropsThrowsError(t *testing.T) {
func TestBpContainsManifestThrowsError(t *testing.T) {
ctx, _ := createContextAndConfig(t, `
- csuite_test {
- name: "plan_name",
- test_config_template: "test_config.xml.template",
- test_config: "AndroidTest.xml"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ test_config: "AndroidTest.xml"
+ }
+ `)
_, errs := ctx.ParseBlueprintsFiles("Android.bp")
@@ -54,10 +54,10 @@ func TestBpContainsManifestThrowsError(t *testing.T) {
func TestBpMissingNameThrowsError(t *testing.T) {
ctx, _ := createContextAndConfig(t, `
- csuite_test {
- test_config_template: "test_config.xml.template"
- }
- `)
+ csuite_test {
+ test_config_template: "config_template.xml"
+ }
+ `)
_, errs := ctx.ParseBlueprintsFiles("Android.bp")
@@ -66,10 +66,10 @@ func TestBpMissingNameThrowsError(t *testing.T) {
func TestBpMissingTemplatePathThrowsError(t *testing.T) {
ctx, config := createContextAndConfig(t, `
- csuite_test {
- name: "plan_name",
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ }
+ `)
ctx.ParseBlueprintsFiles("Android.bp")
_, errs := ctx.PrepareBuildActions(config)
@@ -77,28 +77,69 @@ func TestBpMissingTemplatePathThrowsError(t *testing.T) {
android.FailIfNoMatchingErrors(t, `'test_config_template' is missing`, errs)
}
+func TestBpTemplatePathUnexpectedFileExtensionThrowsError(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml.template"
+ }
+ `)
+
+ ctx.ParseBlueprintsFiles("Android.bp")
+ _, errs := ctx.PrepareBuildActions(config)
+
+ android.FailIfNoMatchingErrors(t, `Config template path should ends with .xml`, errs)
+}
+
+func TestBpExtraTemplateUnexpectedFileExtensionThrowsError(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ extra_test_config_templates: ["another.xml.template"]
+ }
+ `)
+
+ ctx.ParseBlueprintsFiles("Android.bp")
+ _, errs := ctx.PrepareBuildActions(config)
+
+ android.FailIfNoMatchingErrors(t, `Config template path should ends with .xml`, errs)
+}
+
+func TestBpValidExtraTemplateDoesNotThrowError(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ extra_test_config_templates: ["another.xml"]
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+}
+
func TestValidBpMissingPlanIncludeDoesNotThrowError(t *testing.T) {
ctx, config := createContextAndConfig(t, `
- csuite_test {
- name: "plan_name",
- test_config_template: "test_config.xml.template"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
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"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
parseBpAndBuild(t, ctx, config)
- module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
if strings.Contains(content, "{") || strings.Contains(content, "}") {
t.Errorf("The generated plan name contains a placeholder: %s", content)
@@ -107,15 +148,15 @@ func TestValidBpMissingPlanIncludeGeneratesPlanXmlWithoutPlaceholders(t *testing
func TestGeneratedTestPlanContainsPlanName(t *testing.T) {
ctx, config := createContextAndConfig(t, `
- csuite_test {
- name: "plan_name",
- test_config_template: "test_config.xml.template"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
parseBpAndBuild(t, ctx, config)
- module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
if !strings.Contains(content, "plan_name") {
t.Errorf("The plan name is missing from the generated plan: %s", content)
@@ -124,47 +165,100 @@ func TestGeneratedTestPlanContainsPlanName(t *testing.T) {
func TestGeneratedTestPlanContainsTemplatePath(t *testing.T) {
ctx, config := createContextAndConfig(t, `
- csuite_test {
- name: "plan_name",
- test_config_template: "test_config.xml.template"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
parseBpAndBuild(t, ctx, config)
- module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
- if !strings.Contains(content, "config/plan_name.xml.template") {
+ if !strings.Contains(content, "config/plan_name/config_template.xml.template") {
t.Errorf("The template path is missing from the generated plan: %s", content)
}
}
+func TestGeneratedTestPlanContainsExtraTemplatePath(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ extra_test_config_templates: ["extra.xml"]
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if !strings.Contains(content, "config/plan_name/extra.xml.template") {
+ t.Errorf("The extra template path is missing from the generated plan: %s", content)
+ }
+ if !strings.Contains(content, "extra-templates") {
+ t.Errorf("The extra-templates param is missing from the generated plan: %s", content)
+ }
+}
+
+func TestGeneratedTestPlanDoesNotContainExtraTemplatePath(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
+ content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
+ if strings.Contains(content, "extra-templates") {
+ t.Errorf("The extra-templates param should not be included in 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"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml"
+ }
+ `)
+
+ parseBpAndBuild(t, ctx, config)
+
+ params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite")
+ assertFileCopyRuleExists(t, params, "config_template.xml", "config/plan_name/config_template.xml.template")
+}
+
+func TestExtraTemplateFileCopyRuleExists(t *testing.T) {
+ ctx, config := createContextAndConfig(t, `
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ extra_test_config_templates: ["extra.xml"]
+ }
+ `)
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")
+ params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite")
+ assertFileCopyRuleExists(t, params, "config_template.xml", "config/plan_name/extra.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"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ test_plan_include: "include.xml"
+ }
+ `)
parseBpAndBuild(t, ctx, config)
- module := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common")
+ module := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common")
content := android.ContentFromFileRuleForTests(t, module.Output("config/plan_name.xml"))
if !strings.Contains(content, `"includes/plan_name.xml"`) {
t.Errorf("The plan include path is missing from the generated plan: %s", content)
@@ -173,16 +267,16 @@ func TestGeneratedTestPlanContainsPlanInclude(t *testing.T) {
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"
- }
- `)
+ csuite_test {
+ name: "plan_name",
+ test_config_template: "config_template.xml",
+ test_plan_include: "include.xml"
+ }
+ `)
parseBpAndBuild(t, ctx, config)
- params := ctx.ModuleForTests("plan_name", android.BuildOs.String()+"_common").Rule("CSuite")
+ params := ctx.ModuleForTests("plan_name", config.BuildOS.String()+"_common").Rule("CSuite")
assertFileCopyRuleExists(t, params, "include.xml", "config/includes/plan_name.xml")
}
diff --git a/tools/csuite_test/go.mod b/tools/csuite_test/go.mod
index a373cd1..2ab342d 100644
--- a/tools/csuite_test/go.mod
+++ b/tools/csuite_test/go.mod
@@ -1,14 +1,22 @@
module android/soong/csuite
require (
- android/soong v0.0.0
- github.com/google/blueprint v0.0.0
+ 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 google.golang.org/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
+// Indirect deps from golang-protobuf
+exclude github.com/golang/protobuf v1.5.0
+
+replace github.com/google/go-cmp v0.5.5 => ../../../../../external/go-cmp
+
+// Indirect dep from go-cmp
+exclude golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
+
+go 1.13