Bluetooth: Add CddTest annotation am: 9b9d522e09 am: b358db438e am: 83267281aa Original change: https://android-review.googlesource.com/c/platform/test/cts-root/+/2526351 Change-Id: I96f19e4d53e5720d2a77612ae26c5c72161766fc Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/tests/bugreport/Android.bp b/tests/bugreport/Android.bp new file mode 100644 index 0000000..3f49763 --- /dev/null +++ b/tests/bugreport/Android.bp
@@ -0,0 +1,44 @@ +// Copyright (C) 2023 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 { + name: "CtsRootBugreportTestCases", + defaults: ["cts_defaults"], + static_libs: [ + "androidx.test.rules", + "androidx.test.uiautomator_uiautomator", + "compatibility-device-util-axt", + "truth-prebuilt", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + data: [":ctsroot-bugreport-artifacts"], + srcs: ["src/**/*.java"], + sdk_version: "test_current", + test_suites: [ + "cts_root", + "general-tests", + ], +} + +filegroup { + name: "ctsroot-bugreport-artifacts", + srcs: ["config/test-sysconfig.xml"], +}
diff --git a/tests/bugreport/AndroidManifest.xml b/tests/bugreport/AndroidManifest.xml new file mode 100644 index 0000000..b29094b --- /dev/null +++ b/tests/bugreport/AndroidManifest.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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.bugreport.cts_root"> + <uses-permission android:name="android.permission.DUMP" /> + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.bugreport.cts_root" /> + +</manifest>
diff --git a/tests/bugreport/AndroidTest.xml b/tests/bugreport/AndroidTest.xml new file mode 100644 index 0000000..968bdde --- /dev/null +++ b/tests/bugreport/AndroidTest.xml
@@ -0,0 +1,37 @@ +<?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="Bugreport Manager CTS Root tests"> + <option name="test-suite-tag" value="cts_root" /> + <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" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="CtsRootBugreportTestCases.apk" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" key="test-sysconfig.xml" value="/system/etc/sysconfig/allow-br-from-tests.xml" /> + <option name="cleanup" value="true" /> + <option name="remount-system" value="true" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="android.bugreport.cts_root" /> + </test> +</configuration>
diff --git a/tests/bugreport/OWNERS b/tests/bugreport/OWNERS new file mode 100644 index 0000000..fca58a6 --- /dev/null +++ b/tests/bugreport/OWNERS
@@ -0,0 +1,3 @@ +# Bug component: 153446 +gavincorkery@google.com +nandana@google.com
diff --git a/tests/bugreport/TEST_MAPPING b/tests/bugreport/TEST_MAPPING new file mode 100644 index 0000000..c37dda4 --- /dev/null +++ b/tests/bugreport/TEST_MAPPING
@@ -0,0 +1,17 @@ +{ + "presubmit": [ + { + "name": "CtsRootBugreportTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.LargeTest" + } + ] + } + ], + "postsubmit": [ + { + "name": "CtsRootBugreportTestCases" + } + ] +}
diff --git a/tests/bugreport/config/test-sysconfig.xml b/tests/bugreport/config/test-sysconfig.xml new file mode 100644 index 0000000..ddf840a --- /dev/null +++ b/tests/bugreport/config/test-sysconfig.xml
@@ -0,0 +1,21 @@ +<?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. + --> + +<!-- WARNING: This is a test config. --> +<config> + <bugreport-whitelisted package="android.bugreport.cts_root" /> +</config> \ No newline at end of file
diff --git a/tests/bugreport/src/android/bugreport/cts_root/BugreportManagerTest.java b/tests/bugreport/src/android/bugreport/cts_root/BugreportManagerTest.java new file mode 100644 index 0000000..e85de59 --- /dev/null +++ b/tests/bugreport/src/android/bugreport/cts_root/BugreportManagerTest.java
@@ -0,0 +1,293 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bugreport.cts_root; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; + +import android.content.Context; +import android.os.BugreportManager; +import android.os.BugreportManager.BugreportCallback; +import android.os.BugreportParams; +import android.os.ParcelFileDescriptor; + +import androidx.annotation.NonNull; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.BySelector; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; + +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 java.io.File; +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Device-side tests for Bugreport Manager API. + * + * <p>These tests require root to allowlist the test package to use the BugreportManager APIs. + */ +@RunWith(AndroidJUnit4.class) +public class BugreportManagerTest { + + private Context mContext; + private BugreportManager mBugreportManager; + + @Rule + public TestName name = new TestName(); + + private static final long UIAUTOMATOR_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); + + @Before + public void setup() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + mBugreportManager = mContext.getSystemService(BugreportManager.class); + // Kill current bugreport, so that it does not interfere with future bugreports. + runShellCommand("setprop ctl.stop bugreportd"); + } + + @After + public void tearDown() { + // Kill current bugreport, so that it does not interfere with future bugreports. + runShellCommand("setprop ctl.stop bugreportd"); + } + + @LargeTest + @Test + public void testRetrieveBugreportConsentGranted() throws Exception { + File bugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); + File startBugreportFile = createTempFile("startbugreport", ".zip"); + CountDownLatch latch = new CountDownLatch(1); + BugreportCallbackImpl callback = new BugreportCallbackImpl(latch); + mBugreportManager.startBugreport(parcelFd(startBugreportFile), null, + new BugreportParams( + BugreportParams.BUGREPORT_MODE_INTERACTIVE, + BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT), + mContext.getMainExecutor(), callback); + latch.await(4, TimeUnit.MINUTES); + assertThat(callback.isSuccess()).isTrue(); + // No data should be passed to the FD used to call startBugreport. + assertThat(startBugreportFile.length()).isEqualTo(0); + String bugreportFileLocation = callback.getBugreportFile(); + waitForDumpstateServiceToStop(); + + + + // Trying to retrieve an unknown bugreport should fail + latch = new CountDownLatch(1); + callback = new BugreportCallbackImpl(latch); + File bugreportFile2 = createTempFile("bugreport2_" + name.getMethodName(), ".zip"); + mBugreportManager.retrieveBugreport( + "unknown/file.zip", parcelFd(bugreportFile2), + mContext.getMainExecutor(), callback); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); + + // A bugreport was previously generated for this caller. When the consent dialog is invoked + // and accepted, the bugreport files should be passed to the calling package. + ParcelFileDescriptor bugreportFd = parcelFd(bugreportFile); + assertThat(bugreportFd).isNotNull(); + latch = new CountDownLatch(1); + mBugreportManager.retrieveBugreport(bugreportFileLocation, bugreportFd, + mContext.getMainExecutor(), new BugreportCallbackImpl(latch)); + shareConsentDialog(ConsentReply.ALLOW); + assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); + assertThat(bugreportFile.length()).isGreaterThan(0); + } + + + @LargeTest + @Test + public void testRetrieveBugreportConsentDenied() throws Exception { + File bugreportFile = createTempFile("bugreport_" + name.getMethodName(), ".zip"); + + // User denies consent, therefore no data should be passed back to the bugreport file. + CountDownLatch latch = new CountDownLatch(1); + BugreportCallbackImpl callback = new BugreportCallbackImpl(latch); + mBugreportManager.startBugreport(parcelFd(new File("/dev/null")), + null, new BugreportParams(BugreportParams.BUGREPORT_MODE_INTERACTIVE, + BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT), + mContext.getMainExecutor(), callback); + latch.await(4, TimeUnit.MINUTES); + assertThat(callback.isSuccess()).isTrue(); + String bugreportFileLocation = callback.getBugreportFile(); + waitForDumpstateServiceToStop(); + + latch = new CountDownLatch(1); + ParcelFileDescriptor bugreportFd = parcelFd(bugreportFile); + assertThat(bugreportFd).isNotNull(); + mBugreportManager.retrieveBugreport( + bugreportFileLocation, + bugreportFd, + mContext.getMainExecutor(), + callback); + shareConsentDialog(ConsentReply.DENY); + latch.await(1, TimeUnit.MINUTES); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_USER_DENIED_CONSENT); + assertThat(bugreportFile.length()).isEqualTo(0); + + // Since consent has already been denied, this call should fail because consent cannot + // be requested twice for the same bugreport. + latch = new CountDownLatch(1); + callback = new BugreportCallbackImpl(latch); + mBugreportManager.retrieveBugreport(bugreportFileLocation, parcelFd(bugreportFile), + mContext.getMainExecutor(), callback); + latch.await(1, TimeUnit.MINUTES); + assertThat(callback.getErrorCode()).isEqualTo( + BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); + } + + private ParcelFileDescriptor parcelFd(File file) throws Exception { + return ParcelFileDescriptor.open(file, + ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND); + } + + private static File createTempFile(String prefix, String extension) throws Exception { + final File f = File.createTempFile(prefix, extension); + f.setReadable(true, true); + f.setWritable(true, true); + + f.deleteOnExit(); + return f; + } + + private static final class BugreportCallbackImpl extends BugreportCallback { + private int mErrorCode = -1; + private boolean mSuccess = false; + private String mBugreportFile; + private final Object mLock = new Object(); + + private final CountDownLatch mLatch; + + BugreportCallbackImpl(CountDownLatch latch) { + mLatch = latch; + } + + @Override + public void onError(int errorCode) { + synchronized (mLock) { + mErrorCode = errorCode; + mLatch.countDown(); + } + } + + @Override + public void onFinished(String bugreportFile) { + synchronized (mLock) { + mBugreportFile = bugreportFile; + mLatch.countDown(); + mSuccess = true; + } + } + + @Override + public void onFinished() { + synchronized (mLock) { + mLatch.countDown(); + mSuccess = true; + } + } + + public int getErrorCode() { + synchronized (mLock) { + return mErrorCode; + } + } + + public boolean isSuccess() { + synchronized (mLock) { + return mSuccess; + } + } + + public String getBugreportFile() { + synchronized (mLock) { + return mBugreportFile; + } + } + } + + private enum ConsentReply { + ALLOW, + DENY, + TIMEOUT + } + + /* + * Ensure the consent dialog is shown and take action according to <code>consentReply<code/>. + * It will fail if the dialog is not shown when <code>ignoreNotFound<code/> is false. + */ + private void shareConsentDialog(@NonNull ConsentReply consentReply) throws Exception { + final UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + // Unlock before finding/clicking an object. + device.wakeUp(); + device.executeShellCommand("wm dismiss-keyguard"); + + final BySelector consentTitleObj = By.res("android", "alertTitle"); + if (!device.wait(Until.hasObject(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)) { + fail("The consent dialog is not found"); + } + if (consentReply.equals(ConsentReply.TIMEOUT)) { + return; + } + final BySelector selector; + if (consentReply.equals(ConsentReply.ALLOW)) { + selector = By.res("android", "button1"); + } else { // ConsentReply.DENY + selector = By.res("android", "button2"); + } + final UiObject2 btnObj = device.findObject(selector); + assertThat(btnObj).isNotNull(); + btnObj.click(); + + assertThat(device.wait(Until.gone(consentTitleObj), UIAUTOMATOR_TIMEOUT_MS)).isTrue(); + } + + + /** Waits for the dumpstate service to stop, for up to 5 seconds. */ + private void waitForDumpstateServiceToStop() throws Exception { + int pollingIntervalMillis = 100; + int numPolls = 50; + Method method = Class.forName("android.os.ServiceManager").getMethod( + "getService", String.class); + while (numPolls-- > 0) { + // If getService() returns null, the service has stopped. + if (method.invoke(null, "dumpstate") == null) { + return; + } + Thread.sleep(pollingIntervalMillis); + } + fail("Dumpstate did not stop within 5 seconds"); + } +}