Merge "Initial integration test for BugReportReceiver."

This commit is contained in:
Felipe Leme
2015-11-26 00:14:03 +00:00
committed by Android (Google) Code Review
8 changed files with 567 additions and 4 deletions

View File

@@ -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

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2014 Google Inc.
* 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.

View File

@@ -61,8 +61,8 @@ public class BugreportReceiver extends BroadcastReceiver {
private static final String AUTHORITY = "com.android.shell";
private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
/**
* Always keep the newest 8 bugreport files; 4 reports and 4 screenshots are

View File

@@ -0,0 +1,20 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := android.test.runner
# TODO: update and/or remove
LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator
#LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator
LOCAL_PACKAGE_NAME := ShellTests
LOCAL_INSTRUMENTATION_FOR := Shell
LOCAL_CERTIFICATE := platform
include $(BUILD_PACKAGE)

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* 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.
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.shell.tests">
<application>
<uses-library android:name="android.test.runner" />
<activity
android:name="com.android.shell.ActionSendMultipleConsumerActivity"
android:label="ActionSendMultipleConsumer"
android:theme="@android:style/Theme.NoDisplay"
android:noHistory="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
</application>
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.shell"
android:label="Tests for Shell" />
</manifest>

View File

@@ -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<Bundle> 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;
}
}
}

View File

@@ -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}.
* <p>
* 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.
* <p>
* The general workflow is:
* <ul>
* <li>creates the bug report files
* <li>generates the BUGREPORT_FINISHED intent
* <li>emulate user actions to share the intent with a custom activity
* <li>asserts the extras received by the custom activity
* </ul>
* <p>
* 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<Uri> 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();
}
}
}

View File

@@ -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");
}
}