diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-10 07:17:42 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-10 07:17:42 +0000 |
commit | 00e1bcad2e7eee10f5bca946afa1ae54f5596408 (patch) | |
tree | 34a81af095a69c5e94e06bc4bdedd0b0a9238b82 | |
parent | b5feecdd78a099d81ac3348e335e73b53fe2ab89 (diff) | |
parent | 2e0bc257c2235d2b807187d0652b76412afc559e (diff) | |
download | csuite-00e1bcad2e7eee10f5bca946afa1ae54f5596408.tar.gz |
Snap for 8564071 from 2e0bc257c2235d2b807187d0652b76412afc559e to mainline-sdkext-releaseaml_sdk_331111000aml_sdk_330810050aml_sdk_330810010
Change-Id: If9829ebb92eb704950b012bfe5278ca32292fc46
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><object type="PACKAGE_NAME_PROVIDER" class="</b><i>provider_class_name</i><b>"/></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><object type="MODULE_INFO_PROVIDER" class="</b><i>provider_class_name</i><b>"/></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 |