From e53e85f6051d20cbd477bc25d446a41996411fab Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Tue, 17 Nov 2015 17:37:53 -0800 Subject: [PATCH] Initial integration test for BugReportReceiver. These tests rely in the UI Automator to interact with the UI and follow the workflow below: * creates the bug report files * generates the BUGREPORT_FINISHED intent * emulate user actions to share the intent with a custom activity * asserts the extras received by the custom activity It still have some limitations, like requiring the phone to be unlocked and having the bugreport warning already checked, and those will be addressed in future CLs. BUG: 25752530 Change-Id: I01d7fad9f94daf156b728cbb9ef228bbfa6ee0f5 --- packages/Shell/Android.mk | 4 +- packages/Shell/AndroidManifest.xml | 2 +- .../src/com/android/shell/BugreportReceiver.java | 4 +- packages/Shell/tests/Android.mk | 20 ++ packages/Shell/tests/AndroidManifest.xml | 43 ++++ .../shell/ActionSendMultipleConsumerActivity.java | 126 +++++++++++ .../com/android/shell/BugreportReceiverTest.java | 242 +++++++++++++++++++++ .../Shell/tests/src/com/android/shell/UiBot.java | 130 +++++++++++ 8 files changed, 567 insertions(+), 4 deletions(-) create mode 100644 packages/Shell/tests/Android.mk create mode 100644 packages/Shell/tests/AndroidManifest.xml create mode 100644 packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java create mode 100644 packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java create mode 100644 packages/Shell/tests/src/com/android/shell/UiBot.java diff --git a/packages/Shell/Android.mk b/packages/Shell/Android.mk index 5bd48c63433c..f8c13d63327f 100644 --- a/packages/Shell/Android.mk +++ b/packages/Shell/Android.mk @@ -3,7 +3,7 @@ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional -LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 @@ -12,3 +12,5 @@ LOCAL_CERTIFICATE := platform LOCAL_PRIVILEGED_MODULE := true include $(BUILD_PACKAGE) + +include $(LOCAL_PATH)/tests/Android.mk diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 8c39ee654e6d..4f087db136e9 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java new file mode 100644 index 000000000000..e3e99b0d5711 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/ActionSendMultipleConsumerActivity.java @@ -0,0 +1,126 @@ +/* + * 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.shell; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; + +/** + * Activity responsible for handling ACTION_SEND_MULTIPLE intents and passing them back to the test + * case class (through a {@link CustomActionSendMultipleListener}). + */ +public class ActionSendMultipleConsumerActivity extends Activity { + + private static final String CUSTOM_ACTION_SEND_MULTIPLE_INTENT = + "com.android.shell.tests.CUSTOM_ACTION_SEND_MULTIPLE"; + + private static CustomActionSendMultipleListener sListener; + + static final String UI_NAME = "ActionSendMultipleConsumer"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // The original intent cannot be broadcasted, it will fail due to security violations. + // Since the test case is only interested in the extras, we need to create a new custom + // intent with just them. + final Intent intent = getIntent(); + final Intent customIntent = new Intent(CUSTOM_ACTION_SEND_MULTIPLE_INTENT); + customIntent.putExtras(intent.getExtras()); + + getApplicationContext().sendBroadcast(customIntent); + } + + @Override + protected void onResume() { + super.onResume(); + /* + * TODO: if finish() is not called, app will crash with an exception such as: + * AndroidRuntime: java.lang.RuntimeException: Unable to resume activity + * {com.android.shell.tests/com.android.shell.SendMultipleActivity}: + * java.lang.IllegalStateException: Activity + * {com.android.shell.tests/com.android.shell.SendMultipleActivity} did not call finish() + * prior to onResume() completing. That seems to be a problem on M: + * https://code.google.com/p/android-developer-preview/issues/detail?id=2353 + */ + finish(); + } + + /** + * Gets the {@link CustomActionSendMultipleListener} singleton. + */ + static CustomActionSendMultipleListener getListener(Context context) { + synchronized (ActionSendMultipleConsumerActivity.class) { + if (sListener == null) { + sListener = new CustomActionSendMultipleListener(context); + } + } + return sListener; + } + + /** + * Listener of custom ACTION_SEND_MULTIPLE_INTENTS. + */ + static class CustomActionSendMultipleListener { + + private static final int TIMEOUT = 10; + private final BlockingQueue mQueue = new SynchronousQueue<>(); + + public CustomActionSendMultipleListener(Context context) { + BroadcastReceiver receiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + try { + mQueue.put(intent.getExtras()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + + final IntentFilter filter = new IntentFilter(); + filter.addAction(CUSTOM_ACTION_SEND_MULTIPLE_INTENT); + context.registerReceiver(receiver, filter); + } + + /** + * Gets the extras from the custom intent, blocking until it's received. + */ + Bundle getExtras() { + Bundle bundle = null; + try { + bundle = mQueue.poll(TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + if (bundle == null) { + throw new IllegalStateException("Intent not received after " + TIMEOUT + "s"); + } + return bundle; + } + } +} diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java new file mode 100644 index 000000000000..06565c0b4ae6 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java @@ -0,0 +1,242 @@ +/* + * 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.shell; + +import static android.test.MoreAsserts.assertContainsRegex; +import static com.android.shell.ActionSendMultipleConsumerActivity.UI_NAME; +import static com.android.shell.BugreportReceiver.EXTRA_BUGREPORT; +import static com.android.shell.BugreportReceiver.EXTRA_SCREENSHOT; + +import java.io.BufferedOutputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import libcore.io.Streams; +import android.app.Instrumentation; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemProperties; +import android.service.notification.StatusBarNotification; +import android.support.test.uiautomator.UiDevice; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.shell.ActionSendMultipleConsumerActivity.CustomActionSendMultipleListener; + +/** + * Integration tests for {@link BugreportReceiver}. + *

+ * These tests don't mock any component and rely on external UI components (like the notification + * bar and activity chooser), which can make them unreliable and slow. + *

+ * The general workflow is: + *

+ *

+ * TODO: currently, these tests only work if the bug report sharing warning is disabled and the + * device screen is unlocked. + */ +public class BugreportReceiverTest extends InstrumentationTestCase { + + private static final String TAG = "BugreportReceiverTest"; + + // Timeout for UI operations, in milliseconds. + private static final int TIMEOUT = 1000; + + private static final String ROOT_DIR = "/data/data/com.android.shell/files/bugreports"; + private static final String BUGREPORT_FILE = "test_bugreport.txt"; + private static final String ZIP_FILE = "test_bugreport.zip"; + private static final String PLAIN_TEXT_PATH = ROOT_DIR + "/" + BUGREPORT_FILE; + private static final String ZIP_PATH = ROOT_DIR + "/" + ZIP_FILE; + private static final String SCREENSHOT_PATH = ROOT_DIR + "/test_screenshot.png"; + + private static final String BUGREPORT_CONTENT = "Dump, might as well dump!\n"; + private static final String SCREENSHOT_CONTENT = "A picture is worth a thousand words!\n"; + + private Context mContext; + private UiBot mUiBot; + private CustomActionSendMultipleListener mListener; + + @Override + protected void setUp() throws Exception { + Instrumentation instrumentation = getInstrumentation(); + mContext = instrumentation.getTargetContext(); + mUiBot = new UiBot(UiDevice.getInstance(instrumentation), TIMEOUT); + mListener = ActionSendMultipleConsumerActivity.getListener(mContext); + cancelExistingNotifications(); + } + + public void testBugreportFinished_plainBugreportAndScreenshot() throws Exception { + createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); + createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, SCREENSHOT_PATH); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + } + + public void testBugreportFinished_zippedBugreportAndScreenshot() throws Exception { + createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT); + createTextFile(SCREENSHOT_PATH, SCREENSHOT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, SCREENSHOT_PATH); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, SCREENSHOT_CONTENT); + } + + public void testBugreportFinished_plainBugreportAndNoScreenshot() throws Exception { + createTextFile(PLAIN_TEXT_PATH, BUGREPORT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(PLAIN_TEXT_PATH, null); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, null); + } + + public void testBugreportFinished_zippedBugreportAndNoScreenshot() throws Exception { + createZipFile(ZIP_PATH, BUGREPORT_FILE, BUGREPORT_CONTENT); + Bundle extras = sendBugreportFinishedIntent(ZIP_PATH, null); + assertActionSendMultiple(extras, BUGREPORT_CONTENT, null); + } + + private void cancelExistingNotifications() { + NotificationManager nm = NotificationManager.from(mContext); + for (StatusBarNotification notification : nm.getActiveNotifications()) { + int id = notification.getId(); + Log.i(TAG, "Canceling existing notification (id=" + id + ")"); + nm.cancel(id); + } + } + + /** + * Sends a "bugreport finished" intent and waits for the result. + * + * @return extras sent to the bugreport finished consumer. + */ + private Bundle sendBugreportFinishedIntent(String bugreportPath, String screenshotPath) { + Intent intent = new Intent("android.intent.action.BUGREPORT_FINISHED"); + if (bugreportPath != null) { + intent.putExtra(EXTRA_BUGREPORT, bugreportPath); + } + if (screenshotPath != null) { + intent.putExtra(EXTRA_SCREENSHOT, screenshotPath); + } + + mContext.sendBroadcast(intent); + + mUiBot.clickOnNotification(mContext.getString(R.string.bugreport_finished_title)); + mUiBot.chooseActivity(UI_NAME); + return mListener.getExtras(); + } + + /** + * Asserts the proper ACTION_SEND_MULTIPLE intent was sent. + */ + private void assertActionSendMultiple(Bundle extras, String bugreportContent, + String screenshotContent) throws IOException { + String body = extras.getString(Intent.EXTRA_TEXT); + assertContainsRegex("missing build info", + SystemProperties.get("ro.build.description"), body); + assertContainsRegex("missing serial number", + SystemProperties.get("ro.serialno"), body); + + assertEquals("wrong subject", ZIP_FILE, extras.getString(Intent.EXTRA_SUBJECT)); + + List attachments = extras.getParcelableArrayList(Intent.EXTRA_STREAM); + int expectedSize = screenshotContent != null ? 2 : 1; + assertEquals("wrong number of attachments", expectedSize, attachments.size()); + + // Need to interact through all attachments, since order is not guaranteed. + Uri zipUri = null, screenshotUri = null; + for (Uri attachment : attachments) { + if (attachment.getPath().endsWith(".zip")) { + zipUri = attachment; + } + if (attachment.getPath().endsWith(".png")) { + screenshotUri = attachment; + } + } + assertNotNull("did not get .zip attachment", zipUri); + assertZipContent(zipUri, BUGREPORT_FILE, BUGREPORT_CONTENT); + + if (screenshotContent != null) { + assertNotNull("did not get .png attachment", screenshotUri); + assertContent(screenshotUri, SCREENSHOT_CONTENT); + } else { + assertNull("should not have .png attachment", screenshotUri); + } + } + + private void assertContent(Uri uri, String expectedContent) throws IOException { + Log.v(TAG, "assertContents(uri=" + uri); + try (InputStream is = mContext.getContentResolver().openInputStream(uri)) { + String actualContent = new String(Streams.readFully(is)); + assertEquals("wrong content for '" + uri + "'", expectedContent, actualContent); + } + } + + private void assertZipContent(Uri uri, String entryName, String expectedContent) + throws IOException, IOException { + Log.v(TAG, "assertZipEntry(uri=" + uri + ", entryName=" + entryName); + try (ZipInputStream zis = new ZipInputStream(mContext.getContentResolver().openInputStream( + uri))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + Log.v(TAG, "Zip entry: " + entry.getName()); + if (entry.getName().equals(entryName)) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Streams.copy(zis, bos); + String actualContent = new String(bos.toByteArray(), "UTF-8"); + bos.close(); + assertEquals("wrong content for zip entry'" + entryName + "' on '" + uri + "'", + expectedContent, actualContent); + return; + } + } + } + fail("Did not find entry '" + entryName + "' on file '" + uri + "'"); + } + + private static void createTextFile(String path, String content) throws IOException { + Log.v(TAG, "createFile(" + path + ")"); + try (Writer writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(path)))) { + writer.write(content); + } + } + + private void createZipFile(String path, String entryName, String content) throws IOException { + Log.v(TAG, "createZipFile(" + path + ", " + entryName + ")"); + try (ZipOutputStream zos = new ZipOutputStream( + new BufferedOutputStream(new FileOutputStream(path)))) { + ZipEntry entry = new ZipEntry(entryName); + zos.putNextEntry(entry); + byte[] data = content.getBytes(); + zos.write(data, 0, data.length); + zos.closeEntry(); + } + } +} diff --git a/packages/Shell/tests/src/com/android/shell/UiBot.java b/packages/Shell/tests/src/com/android/shell/UiBot.java new file mode 100644 index 000000000000..f5dd31c6fdc0 --- /dev/null +++ b/packages/Shell/tests/src/com/android/shell/UiBot.java @@ -0,0 +1,130 @@ +/* + * 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.shell; + +import android.support.test.uiautomator.By; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiObjectNotFoundException; +import android.support.test.uiautomator.UiSelector; +import android.support.test.uiautomator.Until; +import android.util.Log; +import static junit.framework.Assert.assertTrue; + +/** + * A helper class for UI-related testing tasks. + */ +final class UiBot { + + private static final String TAG = "UiBot"; + private static final String SYSTEMUI_PACKAGED = "com.android.systemui"; + + private final UiDevice mDevice; + private final int mTimeout; + + public UiBot(UiDevice device, int timeout) { + mDevice = device; + mTimeout = timeout; + } + + /** + * Opens the system notification and clicks a given notification. + * + * @param text Notificaton's text as displayed by the UI. + */ + public void clickOnNotification(String text) { + boolean opened = mDevice.openNotification(); + Log.v(TAG, "openNotification(): " + opened); + boolean gotIt = mDevice.wait(Until.hasObject(By.pkg(SYSTEMUI_PACKAGED)), mTimeout); + assertTrue("could not get system ui (" + SYSTEMUI_PACKAGED + ")", gotIt); + + gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); + assertTrue("object with text '(" + text + "') not visible yet", gotIt); + + UiObject notification = getVisibleObject(text); + + click(notification, "bug report notification"); + } + + /** + * Gets an object which is guaranteed to be present in the current UI.\ + * + * @param text Object's text as displayed by the UI. + */ + public UiObject getVisibleObject(String text) { + UiObject uiObject = mDevice.findObject(new UiSelector().text(text)); + assertTrue("could not find object with text '(" + text + "')", uiObject.exists()); + return uiObject; + } + + /** + * Clicks on a UI element. + * + * @param uiObject UI element to be clicked. + * @param description Elements's description used on logging statements. + */ + public void click(UiObject uiObject, String description) { + try { + boolean clicked = uiObject.click(); + // TODO: assertion below fails sometimes, even though the click succeeded, + // (specially when clicking the "Just Once" button), so it's currently just logged. + // assertTrue("could not click on object '" + description + "'", clicked); + + Log.v(TAG, "onClick for " + description + ": " + clicked); + } catch (UiObjectNotFoundException e) { + throw new IllegalStateException("exception when clicking on object '" + description + + "'", e); + } + } + + /** + * Chooses a given activity to handle an Intent, using the "Just Once" button. + * + * @param name name of the activity as displayed in the UI (typically the value set by + * {@code android:label} in the manifest). + */ + // TODO: UI Automator should provide such logic. + public void chooseActivity(String name) { + // First select activity if it's not the default option. + boolean gotIt = mDevice.wait(Until.hasObject(By.text(name)), mTimeout); + // TODO: if the activity is indeed the default option, call above will timeout, which will + // make the tests run slower. It might be better to change the logic to assume the default + // first. + if (gotIt) { + Log.v(TAG, "Found activity " + name + ", it's not default action"); + UiObject activityChooser = getVisibleObject(name); + click(activityChooser, "activity chooser"); + } else { + String text = String.format("Share with %s", name); + Log.v(TAG, "Didn't find activity " + name + + ", assuming it's the default action and search for '" + text + "'"); + gotIt = mDevice.wait(Until.hasObject(By.text(text)), mTimeout); + assertTrue("did not find text '" + text + "'", gotIt); + } + + // Then clicks the "Just Once" button. + gotIt = mDevice + .wait(Until.hasObject(By.res("android", "button_once")), mTimeout); + assertTrue("'Just Once' button not visible yet", gotIt); + + UiObject justOnce = mDevice + .findObject(new UiSelector().resourceId("android:id/button_once")); + assertTrue("'Just Once' button not found", justOnce.exists()); + + click(justOnce, "Just Once"); + } +} -- 2.11.0