diff options
| -rw-r--r-- | core/tests/bugreports/Android.bp | 27 | ||||
| -rw-r--r-- | core/tests/bugreports/AndroidManifest.xml | 24 | ||||
| -rw-r--r-- | core/tests/bugreports/AndroidTest.xml | 35 | ||||
| -rw-r--r-- | core/tests/bugreports/config/test-sysconfig.xml | 20 | ||||
| -rwxr-xr-x | core/tests/bugreports/run.sh | 61 | ||||
| -rw-r--r-- | core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java | 341 |
6 files changed, 508 insertions, 0 deletions
diff --git a/core/tests/bugreports/Android.bp b/core/tests/bugreports/Android.bp new file mode 100644 index 000000000000..d3bf0dd7a7e8 --- /dev/null +++ b/core/tests/bugreports/Android.bp @@ -0,0 +1,27 @@ +// 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. + +android_test { + name: "BugreportManagerTestCases", + srcs: ["src/**/*.java"], + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: ["androidx.test.rules", "truth-prebuilt"], + test_suites: ["general-tests"], + sdk_version: "test_current", + platform_apis: true, +} + diff --git a/core/tests/bugreports/AndroidManifest.xml b/core/tests/bugreports/AndroidManifest.xml new file mode 100644 index 000000000000..0cfb8747e629 --- /dev/null +++ b/core/tests/bugreports/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:installLocation="internalOnly" + package="com.android.os.bugreports.tests"> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.os.bugreports.tests" + android:label="Unit tests of BugreportManager" /> +</manifest> diff --git a/core/tests/bugreports/AndroidTest.xml b/core/tests/bugreports/AndroidTest.xml new file mode 100644 index 000000000000..410ca6043583 --- /dev/null +++ b/core/tests/bugreports/AndroidTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Config for BugreportManager test cases"> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <option name="config-descriptor:metadata" key="component" value="framework"/> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="BugreportManagerTestCases.apk"/> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.os.bugreports.tests"/> + <!-- test-timeout unit is ms, value = 30 min --> + <option name="test-timeout" value="1800000" /> + <option name="runtime-hint" value="30m" /> + </test> +</configuration> diff --git a/core/tests/bugreports/config/test-sysconfig.xml b/core/tests/bugreports/config/test-sysconfig.xml new file mode 100644 index 000000000000..09c69ba699de --- /dev/null +++ b/core/tests/bugreports/config/test-sysconfig.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- WARNING: This is a test config. --> +<config> + <bugreport-whitelisted package="com.android.os.bugreports.tests" /> +</config> diff --git a/core/tests/bugreports/run.sh b/core/tests/bugreports/run.sh new file mode 100755 index 000000000000..010339836538 --- /dev/null +++ b/core/tests/bugreports/run.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Copyright (C) 2019 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script to run bugreport unitests +# Must run on a rooted device. +# Must run lunch before running the script +# Usage: ${ANDROID_BUILD_TOP}/frameworks/base/core/tests/bugreports/run.sh + +# NOTE: This script replaces the framework-sysconfig.xml on your device, so use with caution. +# It tries to replace it when done, but if the script does not finish cleanly +# (for e.g. force stopped mid-way) your device will be left in an inconsistent state. +# Reflashing will restore the right config. + +TMP_SYS_CONFIG=/var/tmp/framework-sysconfig.xml + +if [[ -z $ANDROID_PRODUCT_OUT ]]; then + echo "Please lunch before running this test." + exit 0 +fi + +# Print every command to console. +set -x + +make -j BugreportManagerTestCases && + adb root && + adb remount && + adb wait-for-device && + # Save the sysconfig file in a tmp location and push the test config in + adb pull /system/etc/sysconfig/framework-sysconfig.xml "${TMP_SYS_CONFIG}" && + adb push $ANDROID_BUILD_TOP/frameworks/base/core/tests/bugreports/config/test-sysconfig.xml /system/etc/sysconfig/framework-sysconfig.xml && + # The test app needs to be a priv-app. + adb push $OUT/testcases/BugreportManagerTestCases/*/BugreportManagerTestCases.apk /system/priv-app || + exit 1 + +adb reboot && +adb wait-for-device && +atest BugreportManagerTest || echo "Tests FAILED!" + +# Restore the saved config file +if [ -f "${TMP_SYS_CONFIG}" ]; then + SIZE=$(stat --printf="%s" "${TMP_SYS_CONFIG}") + if [ SIZE > 0 ]; then + adb remount && + adb wait-for-device && + adb push "${TMP_SYS_CONFIG}" /system/etc/sysconfig/framework-sysconfig.xml && + rm "${TMP_SYS_CONFIG}" + fi +fi diff --git a/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java new file mode 100644 index 000000000000..220f854c337a --- /dev/null +++ b/core/tests/bugreports/src/android/server/bugreports/BugreportManagerTest.java @@ -0,0 +1,341 @@ +/* + * 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.os.bugreports.tests; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.Manifest; +import android.content.Context; +import android.os.BugreportManager; +import android.os.BugreportManager.BugreportCallback; +import android.os.BugreportParams; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + + +/** + * Tests for BugreportManager API. + */ +@RunWith(JUnit4.class) +public class BugreportManagerTest { + @Rule public TestName name = new TestName(); + + private static final String TAG = "BugreportManagerTest"; + private static final long BUGREPORT_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10); + + private Handler mHandler; + private Executor mExecutor; + private BugreportManager mBrm; + private ParcelFileDescriptor mBugreportFd; + private ParcelFileDescriptor mScreenshotFd; + + @Before + public void setup() throws Exception { + mHandler = createHandler(); + mExecutor = (runnable) -> { + if (mHandler != null) { + mHandler.post(() -> { + runnable.run(); + }); + } + }; + + mBrm = getBugreportManager(); + mBugreportFd = parcelFd("bugreport_" + name.getMethodName(), ".zip"); + mScreenshotFd = parcelFd("screenshot_" + name.getMethodName(), ".png"); + + getPermissions(); + } + + @After + public void teardown() throws Exception { + dropPermissions(); + } + + + @Test + public void normalFlow_wifi() throws Exception { + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); + waitTillDoneOrTimeout(callback); + + assertThat(callback.isDone()).isTrue(); + // Wifi bugreports should not receive any progress. + assertThat(callback.hasReceivedProgress()).isFalse(); + // TODO: Because of b/130234145, consent dialog is not shown; so we get a timeout error. + // When the bug is fixed, accept consent via UIAutomator and verify contents + // of mBugreportFd. + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test + public void normalFlow_interactive() throws Exception { + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, interactive(), mExecutor, callback); + + waitTillDoneOrTimeout(callback); + assertThat(callback.isDone()).isTrue(); + // Interactive bugreports show progress updates. + assertThat(callback.hasReceivedProgress()).isTrue(); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test + public void simultaneousBugreportsNotAllowed() throws Exception { + // Start bugreport #1 + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); + + // Before #1 is done, try to start #2. + assertThat(callback.isDone()).isFalse(); + BugreportCallbackImpl callback2 = new BugreportCallbackImpl(); + ParcelFileDescriptor bugreportFd2 = parcelFd("bugreport_2_" + name.getMethodName(), ".zip"); + ParcelFileDescriptor screenshotFd2 = + parcelFd("screenshot_2_" + name.getMethodName(), ".png"); + mBrm.startBugreport(bugreportFd2, screenshotFd2, wifi(), mExecutor, callback2); + Thread.sleep(500 /* .5s */); + + // Verify #2 encounters an error. + assertThat(callback2.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); + assertFdsAreClosed(bugreportFd2, screenshotFd2); + + // Cancel #1 so we can move on to the next test. + mBrm.cancelBugreport(); + Thread.sleep(500 /* .5s */); + assertThat(callback.isDone()).isTrue(); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test + public void cancelBugreport() throws Exception { + // Start a bugreport. + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); + + // Verify it's not finished yet. + assertThat(callback.isDone()).isFalse(); + + // Try to cancel it, but first without DUMP permission. + dropPermissions(); + try { + mBrm.cancelBugreport(); + fail("Expected cancelBugreport to throw SecurityException without DUMP permission"); + } catch (SecurityException expected) { + } + assertThat(callback.isDone()).isFalse(); + + // Try again, with DUMP permission. + getPermissions(); + mBrm.cancelBugreport(); + Thread.sleep(500 /* .5s */); + assertThat(callback.isDone()).isTrue(); + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test + public void insufficientPermissions_throwsException() throws Exception { + dropPermissions(); + + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + try { + mBrm.startBugreport(mBugreportFd, mScreenshotFd, wifi(), mExecutor, callback); + fail("Expected startBugreport to throw SecurityException without DUMP permission"); + } catch (SecurityException expected) { + } + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + @Test + public void invalidBugreportMode_throwsException() throws Exception { + BugreportCallbackImpl callback = new BugreportCallbackImpl(); + + try { + mBrm.startBugreport(mBugreportFd, mScreenshotFd, + new BugreportParams(25) /* unknown bugreport mode */, mExecutor, callback); + fail("Expected to throw IllegalArgumentException with unknown bugreport mode"); + } catch (IllegalArgumentException expected) { + } + assertFdsAreClosed(mBugreportFd, mScreenshotFd); + } + + private Handler createHandler() { + HandlerThread handlerThread = new HandlerThread("BugreportManagerTest"); + handlerThread.start(); + return new Handler(handlerThread.getLooper()); + } + + /* Implementatiion of {@link BugreportCallback} that offers wrappers around execution result */ + private static final class BugreportCallbackImpl extends BugreportCallback { + private int mErrorCode = -1; + private boolean mSuccess = false; + private boolean mReceivedProgress = false; + private final Object mLock = new Object(); + + @Override + public void onProgress(float progress) { + synchronized (mLock) { + mReceivedProgress = true; + } + } + + @Override + public void onError(int errorCode) { + synchronized (mLock) { + mErrorCode = errorCode; + Log.d(TAG, "bugreport errored."); + } + } + + @Override + public void onFinished() { + synchronized (mLock) { + Log.d(TAG, "bugreport finished."); + mSuccess = true; + } + } + + /* Indicates completion; and ended up with a success or error. */ + public boolean isDone() { + synchronized (mLock) { + return (mErrorCode != -1) || mSuccess; + } + } + + public int getErrorCode() { + synchronized (mLock) { + return mErrorCode; + } + } + + public boolean isSuccess() { + synchronized (mLock) { + return mSuccess; + } + } + + public boolean hasReceivedProgress() { + synchronized (mLock) { + return mReceivedProgress; + } + } + } + + public static BugreportManager getBugreportManager() { + Context context = InstrumentationRegistry.getContext(); + BugreportManager bm = + (BugreportManager) context.getSystemService(Context.BUGREPORT_SERVICE); + if (bm == null) { + throw new AssertionError("Failed to get BugreportManager"); + } + return bm; + } + + private static ParcelFileDescriptor parcelFd(String prefix, String extension) throws Exception { + File f = File.createTempFile(prefix, extension); + f.setReadable(true, true); + f.setWritable(true, true); + + return ParcelFileDescriptor.open(f, + ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); + } + + private static void dropPermissions() { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + + private static void getPermissions() { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(Manifest.permission.DUMP); + } + + private static void assertFdIsClosed(ParcelFileDescriptor pfd) { + try { + int fd = pfd.getFd(); + fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd); + } catch (IllegalStateException expected) { + } + } + + private static void assertFdsAreClosed(ParcelFileDescriptor... pfds) { + for (int i = 0; i < pfds.length; i++) { + assertFdIsClosed(pfds[i]); + } + } + + private static long now() { + return System.currentTimeMillis(); + } + + private static boolean shouldTimeout(long startTimeMs) { + return now() - startTimeMs >= BUGREPORT_TIMEOUT_MS; + } + + private static void waitTillDoneOrTimeout(BugreportCallbackImpl callback) throws Exception { + long startTimeMs = now(); + while (!callback.isDone()) { + Thread.sleep(1000 /* 1s */); + if (shouldTimeout(startTimeMs)) { + break; + } + Log.d(TAG, "Waited " + (now() - startTimeMs) + "ms"); + } + } + + /* + * Returns a {@link BugreportParams} for wifi only bugreport. + * + * <p>Wifi bugreports have minimal content and are fast to run. They also suppress progress + * updates. + */ + private static BugreportParams wifi() { + return new BugreportParams(BugreportParams.BUGREPORT_MODE_WIFI); + } + + /* + * Returns a {@link BugreportParams} for interactive bugreport that offers progress updates. + * + * <p>This is the typical bugreport taken by users. This can take on the order of minutes to + * finish. + */ + private static BugreportParams interactive() { + return new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE); + } +} |